Skip to content

Commit

Permalink
Improved workaround for backward compatibility with renderers built f…
Browse files Browse the repository at this point in the history
…or < React 18 support (#2010)

Summary:
Pull Request resolved: facebookexperimental/Recoil#2010

An improved workaround for Recoil to have backward compatibility with React renderers built before React 18 support when used with React 18.  While this may technically be a user error we would like to avoid breaking internal or open source applications that are using React 18 and have not yet upgraded all of their renderers.

Log detected Renderer version mismatches using `recoverableViolation()`, but only once per module runtime.

Reviewed By: davidmccabe

Differential Revision: D39586744

fbshipit-source-id: 1dbe5dbf3b33f30d49891ae74e4c078fe7c4d05f
  • Loading branch information
eagle2722 committed Sep 22, 2022
1 parent b19eaa2 commit 1fc4fb1
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 24 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG-recoil.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
32 changes: 32 additions & 0 deletions packages/recoil/core/Recoil_ReactMode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};

Expand Down Expand Up @@ -42,6 +43,36 @@ const useSyncExternalStore: <T>(
// 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'
Expand Down Expand Up @@ -94,6 +125,7 @@ module.exports = {
createMutableSource,
useMutableSource,
useSyncExternalStore,
currentRendererSupportsUseSyncExternalStore,
reactMode,
isFastRefreshEnabled,
};
35 changes: 12 additions & 23 deletions packages/recoil/hooks/Recoil_Hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -309,7 +310,7 @@ function useRecoilInterface_DEPRECATED(): RecoilInterface {

const recoilComponentGetRecoilValueCount_FOR_TESTING = {current: 0};

function useRecoilValueLoadable_SYNC_EXTERNAL_STORE_IMPL<T>(
function useRecoilValueLoadable_SYNC_EXTERNAL_STORE<T>(
recoilValue: RecoilValue<T>,
): Loadable<T> {
const storeRef = useStoreRef();
Expand Down Expand Up @@ -581,27 +582,6 @@ function useRecoilValueLoadable_LEGACY<T>(
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<T>(
recoilValue: RecoilValue<T>,
): Loadable<T> {
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.
Expand All @@ -616,7 +596,16 @@ function useRecoilValueLoadable<T>(recoilValue: RecoilValue<T>): Loadable<T> {
}
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);
Expand Down

0 comments on commit 1fc4fb1

Please sign in to comment.