diff --git a/CHANGELOG-recoil.md b/CHANGELOG-recoil.md index ba5c789..f63ec14 100644 --- a/CHANGELOG-recoil.md +++ b/CHANGELOG-recoil.md @@ -3,7 +3,7 @@ ## UPCOMING **_Add new changes here as they land_** -- Workaround for React 18 environments with nested renderers that don't support `useSyncExternalStore()` (#2001) +- Workaround for React 18 environments with nested renderers that don't support `useSyncExternalStore()` (#2001, #2010) ## 0.7.5 (2022-08-11) diff --git a/packages/recoil/core/Recoil_ReactMode.js b/packages/recoil/core/Recoil_ReactMode.js index d7ade8f..3f73ba6 100644 --- a/packages/recoil/core/Recoil_ReactMode.js +++ b/packages/recoil/core/Recoil_ReactMode.js @@ -13,6 +13,7 @@ const React = require('react'); const gkx = require('recoil-shared/util/Recoil_gkx'); +const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation'); export opaque type MutableSource = {}; @@ -42,6 +43,36 @@ const useSyncExternalStore: ( // flowlint-next-line unclear-type:off (React: any).unstable_useSyncExternalStore; +let ReactRendererVersionMismatchWarnOnce = false; + +// Check if the current renderer supports `useSyncExternalStore()`. +// Since React goes through a proxy dispatcher and the current renderer can +// change we can't simply check if `React.useSyncExternalStore()` is defined. +function currentRendererSupportsUseSyncExternalStore(): boolean { + // $FlowFixMe[incompatible-use] + const {ReactCurrentDispatcher, ReactCurrentOwner} = + /* $FlowFixMe[prop-missing] This workaround was approved as a safer mechanism + * to detect if the current renderer supports useSyncExternalStore() + * https://fb.workplace.com/groups/reactjs/posts/9558682330846963/ */ + React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED; + const dispatcher = + ReactCurrentDispatcher?.current ?? ReactCurrentOwner.currentDispatcher; + const isUseSyncExternalStoreSupported = + dispatcher.useSyncExternalStore != null; + if ( + useSyncExternalStore && + !isUseSyncExternalStoreSupported && + !ReactRendererVersionMismatchWarnOnce + ) { + ReactRendererVersionMismatchWarnOnce = true; + recoverableViolation( + 'A React renderer without React 18+ API support is being used with React 18+.', + 'recoil', + ); + } + return isUseSyncExternalStoreSupported; +} + type ReactMode = | 'TRANSITION_SUPPORT' | 'SYNC_EXTERNAL_STORE' @@ -94,6 +125,7 @@ module.exports = { createMutableSource, useMutableSource, useSyncExternalStore, + currentRendererSupportsUseSyncExternalStore, reactMode, isFastRefreshEnabled, }; diff --git a/packages/recoil/hooks/Recoil_Hooks.js b/packages/recoil/hooks/Recoil_Hooks.js index 5bd83e2..43b8714 100644 --- a/packages/recoil/hooks/Recoil_Hooks.js +++ b/packages/recoil/hooks/Recoil_Hooks.js @@ -20,6 +20,7 @@ import type {NodeKey, StoreRef} from '../core/Recoil_State'; const {batchUpdates} = require('../core/Recoil_Batching'); const {DEFAULT_VALUE} = require('../core/Recoil_Node'); const { + currentRendererSupportsUseSyncExternalStore, reactMode, useMutableSource, useSyncExternalStore, @@ -309,7 +310,7 @@ function useRecoilInterface_DEPRECATED(): RecoilInterface { const recoilComponentGetRecoilValueCount_FOR_TESTING = {current: 0}; -function useRecoilValueLoadable_SYNC_EXTERNAL_STORE_IMPL( +function useRecoilValueLoadable_SYNC_EXTERNAL_STORE( recoilValue: RecoilValue, ): Loadable { const storeRef = useStoreRef(); @@ -581,27 +582,6 @@ function useRecoilValueLoadable_LEGACY( return loadable; } -// Recoil will attemp to detect if `useSyncExternalStore()` is supported in -// Recoil_ReactMode.js before calling it. However, sometimes the host -// environment supports it but creates additional React renderers, such as with -// `react-three-fiber`, which do not. Since React goes through a proxy -// dispatcher we can't simply check if `useSyncExternalStore()` is defined. -// Thus, this workaround will catch the situation and fallback to using -// just `useState()` and `useEffect()`. -function useRecoilValueLoadable_SYNC_EXTERNAL_STORE( - recoilValue: RecoilValue, -): Loadable { - try { - return useRecoilValueLoadable_SYNC_EXTERNAL_STORE_IMPL(recoilValue); - } catch (e) { - if (e.message.includes('useSyncExternalStore is not a function')) { - // eslint-disable-next-line fb-www/react-hooks - return useRecoilValueLoadable_TRANSITION_SUPPORT(recoilValue); - } - throw e; - } -} - /** Like useRecoilValue(), but either returns the value if available or just undefined if not available for any reason, such as pending or error. @@ -616,7 +596,16 @@ function useRecoilValueLoadable(recoilValue: RecoilValue): Loadable { } return { TRANSITION_SUPPORT: useRecoilValueLoadable_TRANSITION_SUPPORT, - SYNC_EXTERNAL_STORE: useRecoilValueLoadable_SYNC_EXTERNAL_STORE, + // Recoil will attemp to detect if `useSyncExternalStore()` is supported with + // `reactMode()` before calling it. However, sometimes the host React + // environment supports it but uses additional React renderers (such as with + // `react-three-fiber`) which do not. While this is technically a user issue + // by using a renderer with React 18+ that doesn't fully support React 18 we + // don't want to break users if it can be avoided. As the current renderer can + // change at runtime, we need to dynamically check and fallback if necessary. + SYNC_EXTERNAL_STORE: currentRendererSupportsUseSyncExternalStore() + ? useRecoilValueLoadable_SYNC_EXTERNAL_STORE + : useRecoilValueLoadable_TRANSITION_SUPPORT, MUTABLE_SOURCE: useRecoilValueLoadable_MUTABLE_SOURCE, LEGACY: useRecoilValueLoadable_LEGACY, }[reactMode().mode](recoilValue);