From e6fa1b82509bb2d66b88d0ef62f1fd7e2295c7bc Mon Sep 17 00:00:00 2001 From: Jay Meistrich Date: Thu, 12 Oct 2023 15:37:54 +0200 Subject: [PATCH] useSelector warns if it causes a hook error --- src/react/useSelector.ts | 75 +++++++++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 21 deletions(-) diff --git a/src/react/useSelector.ts b/src/react/useSelector.ts index a07a68be6..4b6f4c53f 100644 --- a/src/react/useSelector.ts +++ b/src/react/useSelector.ts @@ -1,4 +1,14 @@ -import { computeSelector, isPrimitive, isPromise, ListenerParams, Selector, trackSelector } from '@legendapp/state'; +import { + computeSelector, + isObservable, + isPrimitive, + isPromise, + ListenerParams, + Observable, + Selector, + trackSelector, + WithState, +} from '@legendapp/state'; import React, { useRef } from 'react'; import type { UseSelectorOptions } from './reactInterfaces'; import { useSyncExternalStore } from 'use-sync-external-store/shim'; @@ -84,28 +94,51 @@ export function useSelector(selector: Selector, options?: UseSelectorOptio return computeSelector(selector); } - const ref = useRef>(); - if (!ref.current) { - ref.current = createSelectorFunctions(options); - } - const { subscribe, getVersion, run } = ref.current; - - // Run the selector - // Note: The selector needs to run on every render because it may have different results - // than the previous run if it uses local state - const value = run(selector) as any; - - useSyncExternalStore(subscribe, getVersion, getVersion); - - // Suspense support - if (options?.suspense) { - if (isPromise(value)) { - if (React.use) { - React.use(value); - } else { - throw value; + let value; + + try { + const ref = useRef>(); + if (!ref.current) { + ref.current = createSelectorFunctions(options); + } + const { subscribe, getVersion, run } = ref.current; + + // Run the selector + // Note: The selector needs to run on every render because it may have different results + // than the previous run if it uses local state + value = run(selector) as any; + + useSyncExternalStore(subscribe, getVersion, getVersion); + + // Suspense support + if (options?.suspense) { + // Note: Although it's not possible for an observable to be a promise, the selector may be a + // function that returns a Promise, so we handle that case too. + if ( + isPromise(value) || + (!value && + isObservable(selector) && + !(selector as unknown as Observable).state.isLoaded.get()) + ) { + if (React.use) { + React.use(value); + } else { + throw value; + } } } + } catch (err: unknown) { + if ( + (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') && + (err as Error)?.message?.includes('Rendered more') + ) { + console.warn( + `[legend-state]: You may want to wrap this component in \`observer\` to fix the error of ${ + (err as Error).message + }`, + ); + throw err; + } } return value;