diff --git a/packages/use-sync-external-store/index.native.js b/packages/use-sync-external-store/index.native.js
new file mode 100644
index 0000000000000..cac5c1c2bf710
--- /dev/null
+++ b/packages/use-sync-external-store/index.native.js
@@ -0,0 +1,12 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+'use strict';
+
+export * from './src/useSyncExternalStoreClient';
diff --git a/packages/use-sync-external-store/npm/index.native.js b/packages/use-sync-external-store/npm/index.native.js
new file mode 100644
index 0000000000000..22546b9c0ebba
--- /dev/null
+++ b/packages/use-sync-external-store/npm/index.native.js
@@ -0,0 +1,7 @@
+'use strict';
+
+if (process.env.NODE_ENV === 'production') {
+ module.exports = require('./cjs/use-sync-external-store.native.production.min.js');
+} else {
+ module.exports = require('./cjs/use-sync-external-store.native.development.js');
+}
diff --git a/packages/use-sync-external-store/package.json b/packages/use-sync-external-store/package.json
index 7f5e5b0e8c0d3..b43b3a0ec67d2 100644
--- a/packages/use-sync-external-store/package.json
+++ b/packages/use-sync-external-store/package.json
@@ -13,6 +13,7 @@
"build-info.json",
"index.js",
"extra.js",
+ "index.native.js",
"cjs/"
],
"license": "MIT",
diff --git a/packages/use-sync-external-store/src/__tests__/useSyncExternalStoreNative-test.js b/packages/use-sync-external-store/src/__tests__/useSyncExternalStoreNative-test.js
new file mode 100644
index 0000000000000..0902e7554c450
--- /dev/null
+++ b/packages/use-sync-external-store/src/__tests__/useSyncExternalStoreNative-test.js
@@ -0,0 +1,186 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ *
+ * @jest-environment node
+ */
+
+'use strict';
+
+let React;
+let ReactNoop;
+let Scheduler;
+let useSyncExternalStore;
+let useSyncExternalStoreExtra;
+let act;
+
+// This tests the userspace shim of `useSyncExternalStore` in a server-rendering
+// (Node) environment
+describe('useSyncExternalStore (userspace shim, server rendering)', () => {
+ beforeEach(() => {
+ jest.resetModules();
+
+ // Remove useSyncExternalStore from the React imports so that we use the
+ // shim instead. Also removing startTransition, since we use that to detect
+ // outdated 18 alphas that don't yet include useSyncExternalStore.
+ //
+ // Longer term, we'll probably test this branch using an actual build of
+ // React 17.
+ jest.mock('react', () => {
+ const {
+ // eslint-disable-next-line no-unused-vars
+ startTransition: _,
+ // eslint-disable-next-line no-unused-vars
+ useSyncExternalStore: __,
+ // eslint-disable-next-line no-unused-vars
+ unstable_useSyncExternalStore: ___,
+ ...otherExports
+ } = jest.requireActual('react');
+ return otherExports;
+ });
+
+ jest.mock('use-sync-external-store', () =>
+ jest.requireActual('use-sync-external-store/index.native'),
+ );
+
+ React = require('react');
+ ReactNoop = require('react-noop-renderer');
+ Scheduler = require('scheduler');
+ act = require('jest-react').act;
+ useSyncExternalStore = require('use-sync-external-store')
+ .useSyncExternalStore;
+ useSyncExternalStoreExtra = require('use-sync-external-store/extra')
+ .useSyncExternalStoreExtra;
+ });
+
+ function Text({text}) {
+ Scheduler.unstable_yieldValue(text);
+ return text;
+ }
+
+ function createExternalStore(initialState) {
+ const listeners = new Set();
+ let currentState = initialState;
+ return {
+ set(text) {
+ currentState = text;
+ ReactNoop.batchedUpdates(() => {
+ listeners.forEach(listener => listener());
+ });
+ },
+ subscribe(listener) {
+ listeners.add(listener);
+ return () => listeners.delete(listener);
+ },
+ getState() {
+ return currentState;
+ },
+ getSubscriberCount() {
+ return listeners.size;
+ },
+ };
+ }
+
+ test('native version', async () => {
+ const store = createExternalStore('client');
+
+ function App() {
+ const text = useSyncExternalStore(
+ store.subscribe,
+ store.getState,
+ () => 'server',
+ );
+ return ;
+ }
+
+ const root = ReactNoop.createRoot();
+ await act(() => {
+ root.render();
+ });
+ expect(Scheduler).toHaveYielded(['client']);
+ expect(root).toMatchRenderedOutput('client');
+ });
+
+ test('native version', async () => {
+ const store = createExternalStore('client');
+
+ function App() {
+ const text = useSyncExternalStore(
+ store.subscribe,
+ store.getState,
+ () => 'server',
+ );
+ return ;
+ }
+
+ const root = ReactNoop.createRoot();
+ await act(() => {
+ root.render();
+ });
+ expect(Scheduler).toHaveYielded(['client']);
+ expect(root).toMatchRenderedOutput('client');
+ });
+
+ // @gate !(enableUseRefAccessWarning && __DEV__)
+ test('Using isEqual to bailout', async () => {
+ const store = createExternalStore({a: 0, b: 0});
+
+ function A() {
+ const {a} = useSyncExternalStoreExtra(
+ store.subscribe,
+ store.getState,
+ null,
+ state => ({a: state.a}),
+ (state1, state2) => state1.a === state2.a,
+ );
+ return ;
+ }
+ function B() {
+ const {b} = useSyncExternalStoreExtra(
+ store.subscribe,
+ store.getState,
+ null,
+ state => {
+ return {b: state.b};
+ },
+ (state1, state2) => state1.b === state2.b,
+ );
+ return ;
+ }
+
+ function App() {
+ return (
+ <>
+
+
+ >
+ );
+ }
+
+ const root = ReactNoop.createRoot();
+ act(() => root.render());
+
+ expect(Scheduler).toHaveYielded(['A0', 'B0']);
+ expect(root).toMatchRenderedOutput('A0B0');
+
+ // Update b but not a
+ await act(() => {
+ store.set({a: 0, b: 1});
+ });
+ // Only b re-renders
+ expect(Scheduler).toHaveYielded(['B1']);
+ expect(root).toMatchRenderedOutput('A0B1');
+
+ // Update a but not b
+ await act(() => {
+ store.set({a: 1, b: 1});
+ });
+ // Only a re-renders
+ expect(Scheduler).toHaveYielded(['A1']);
+ expect(root).toMatchRenderedOutput('A1B1');
+ });
+});
diff --git a/packages/use-sync-external-store/src/useSyncExternalStore.js b/packages/use-sync-external-store/src/useSyncExternalStore.js
index 6b30854aea03a..8a1a5c7191135 100644
--- a/packages/use-sync-external-store/src/useSyncExternalStore.js
+++ b/packages/use-sync-external-store/src/useSyncExternalStore.js
@@ -7,171 +7,8 @@
* @flow
*/
-import * as React from 'react';
-import is from 'shared/objectIs';
-import invariant from 'shared/invariant';
import {canUseDOM} from 'shared/ExecutionEnvironment';
+import {useSyncExternalStore as client} from './useSyncExternalStoreClient';
+import {useSyncExternalStore as server} from './useSyncExternalStoreServer';
-// Intentionally not using named imports because Rollup uses dynamic
-// dispatch for CommonJS interop named imports.
-const {
- useState,
- useEffect,
- useLayoutEffect,
- useDebugValue,
- // The built-in API is still prefixed.
- unstable_useSyncExternalStore: builtInAPI,
-} = React;
-
-// TODO: This heuristic doesn't work in React Native. We'll need to provide a
-// special build, using the `.native` extension.
-const isServerEnvironment = !canUseDOM;
-
-// Prefer the built-in API, if it exists. If it doesn't exist, then we assume
-// we're in version 16 or 17, so rendering is always synchronous. The shim
-// does not support concurrent rendering, only the built-in API.
-export const useSyncExternalStore =
- builtInAPI !== undefined
- ? ((builtInAPI: any): typeof useSyncExternalStore_client)
- : isServerEnvironment
- ? useSyncExternalStore_server
- : useSyncExternalStore_client;
-
-let didWarnOld18Alpha = false;
-let didWarnUncachedGetSnapshot = false;
-
-function useSyncExternalStore_server(
- subscribe: (() => void) => () => void,
- getSnapshot: () => T,
- getServerSnapshot?: () => T,
-): T {
- if (getServerSnapshot === undefined) {
- invariant(
- false,
- 'Missing getServerSnapshot, which is required for server-' +
- 'rendered content.',
- );
- }
- return getServerSnapshot();
-}
-
-// Disclaimer: This shim breaks many of the rules of React, and only works
-// because of a very particular set of implementation details and assumptions
-// -- change any one of them and it will break. The most important assumption
-// is that updates are always synchronous, because concurrent rendering is
-// only available in versions of React that also have a built-in
-// useSyncExternalStore API. And we only use this shim when the built-in API
-// does not exist.
-//
-// Do not assume that the clever hacks used by this hook also work in general.
-// The point of this shim is to replace the need for hacks by other libraries.
-function useSyncExternalStore_client(
- subscribe: (() => void) => () => void,
- getSnapshot: () => T,
- // Note: The client shim does not use getServerSnapshot, because pre-18
- // versions of React do not expose a way to check if we're hydrating. So
- // users of the shim will need to track that themselves and return the
- // correct value from `getSnapshot`.
- getServerSnapshot?: () => T,
-): T {
- if (__DEV__) {
- if (!didWarnOld18Alpha) {
- if (React.startTransition !== undefined) {
- didWarnOld18Alpha = true;
- console.error(
- 'You are using an outdated, pre-release alpha of React 18 that ' +
- 'does not support useSyncExternalStore. The ' +
- 'use-sync-external-store shim will not work correctly. Upgrade ' +
- 'to a newer pre-release.',
- );
- }
- }
- }
-
- // Read the current snapshot from the store on every render. Again, this
- // breaks the rules of React, and only works here because of specific
- // implementation details, most importantly that updates are
- // always synchronous.
- const value = getSnapshot();
- if (__DEV__) {
- if (!didWarnUncachedGetSnapshot) {
- if (value !== getSnapshot()) {
- console.error(
- 'The result of getSnapshot should be cached to avoid an infinite loop',
- );
- didWarnUncachedGetSnapshot = true;
- }
- }
- }
-
- // Because updates are synchronous, we don't queue them. Instead we force a
- // re-render whenever the subscribed state changes by updating an some
- // arbitrary useState hook. Then, during render, we call getSnapshot to read
- // the current value.
- //
- // Because we don't actually use the state returned by the useState hook, we
- // can save a bit of memory by storing other stuff in that slot.
- //
- // To implement the early bailout, we need to track some things on a mutable
- // object. Usually, we would put that in a useRef hook, but we can stash it in
- // our useState hook instead.
- //
- // To force a re-render, we call forceUpdate({inst}). That works because the
- // new object always fails an equality check.
- const [{inst}, forceUpdate] = useState({inst: {value, getSnapshot}});
-
- // Track the latest getSnapshot function with a ref. This needs to be updated
- // in the layout phase so we can access it during the tearing check that
- // happens on subscribe.
- useLayoutEffect(() => {
- inst.value = value;
- inst.getSnapshot = getSnapshot;
-
- // Whenever getSnapshot or subscribe changes, we need to check in the
- // commit phase if there was an interleaved mutation. In concurrent mode
- // this can happen all the time, but even in synchronous mode, an earlier
- // effect may have mutated the store.
- if (checkIfSnapshotChanged(inst)) {
- // Force a re-render.
- forceUpdate({inst});
- }
- }, [subscribe, value, getSnapshot]);
-
- useEffect(() => {
- // Check for changes right before subscribing. Subsequent changes will be
- // detected in the subscription handler.
- if (checkIfSnapshotChanged(inst)) {
- // Force a re-render.
- forceUpdate({inst});
- }
- const handleStoreChange = () => {
- // TODO: Because there is no cross-renderer API for batching updates, it's
- // up to the consumer of this library to wrap their subscription event
- // with unstable_batchedUpdates. Should we try to detect when this isn't
- // the case and print a warning in development?
-
- // The store changed. Check if the snapshot changed since the last time we
- // read from the store.
- if (checkIfSnapshotChanged(inst)) {
- // Force a re-render.
- forceUpdate({inst});
- }
- };
- // Subscribe to the store and return a clean-up function.
- return subscribe(handleStoreChange);
- }, [subscribe]);
-
- useDebugValue(value);
- return value;
-}
-
-function checkIfSnapshotChanged(inst) {
- const latestGetSnapshot = inst.getSnapshot;
- const prevValue = inst.value;
- try {
- const nextValue = latestGetSnapshot();
- return !is(prevValue, nextValue);
- } catch (error) {
- return true;
- }
-}
+export const useSyncExternalStore = canUseDOM ? client : server;
diff --git a/packages/use-sync-external-store/src/useSyncExternalStoreClient.js b/packages/use-sync-external-store/src/useSyncExternalStoreClient.js
new file mode 100644
index 0000000000000..76e7fda36f831
--- /dev/null
+++ b/packages/use-sync-external-store/src/useSyncExternalStoreClient.js
@@ -0,0 +1,154 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import * as React from 'react';
+import is from 'shared/objectIs';
+
+// Intentionally not using named imports because Rollup uses dynamic
+// dispatch for CommonJS interop named imports.
+const {
+ useState,
+ useEffect,
+ useLayoutEffect,
+ useDebugValue,
+ // The built-in API is still prefixed.
+ unstable_useSyncExternalStore: builtInAPI,
+} = React;
+
+// Prefer the built-in API, if it exists. If it doesn't exist, then we assume
+// we're in version 16 or 17, so rendering is always synchronous. The shim
+// does not support concurrent rendering, only the built-in API.
+export const useSyncExternalStore =
+ builtInAPI !== undefined
+ ? ((builtInAPI: any): typeof useSyncExternalStore_client)
+ : useSyncExternalStore_client;
+
+let didWarnOld18Alpha = false;
+let didWarnUncachedGetSnapshot = false;
+
+// Disclaimer: This shim breaks many of the rules of React, and only works
+// because of a very particular set of implementation details and assumptions
+// -- change any one of them and it will break. The most important assumption
+// is that updates are always synchronous, because concurrent rendering is
+// only available in versions of React that also have a built-in
+// useSyncExternalStore API. And we only use this shim when the built-in API
+// does not exist.
+//
+// Do not assume that the clever hacks used by this hook also work in general.
+// The point of this shim is to replace the need for hacks by other libraries.
+function useSyncExternalStore_client(
+ subscribe: (() => void) => () => void,
+ getSnapshot: () => T,
+ // Note: The client shim does not use getServerSnapshot, because pre-18
+ // versions of React do not expose a way to check if we're hydrating. So
+ // users of the shim will need to track that themselves and return the
+ // correct value from `getSnapshot`.
+ getServerSnapshot?: () => T,
+): T {
+ if (__DEV__) {
+ if (!didWarnOld18Alpha) {
+ if (React.startTransition !== undefined) {
+ didWarnOld18Alpha = true;
+ console.error(
+ 'You are using an outdated, pre-release alpha of React 18 that ' +
+ 'does not support useSyncExternalStore. The ' +
+ 'use-sync-external-store shim will not work correctly. Upgrade ' +
+ 'to a newer pre-release.',
+ );
+ }
+ }
+ }
+
+ // Read the current snapshot from the store on every render. Again, this
+ // breaks the rules of React, and only works here because of specific
+ // implementation details, most importantly that updates are
+ // always synchronous.
+ const value = getSnapshot();
+ if (__DEV__) {
+ if (!didWarnUncachedGetSnapshot) {
+ if (value !== getSnapshot()) {
+ console.error(
+ 'The result of getSnapshot should be cached to avoid an infinite loop',
+ );
+ didWarnUncachedGetSnapshot = true;
+ }
+ }
+ }
+
+ // Because updates are synchronous, we don't queue them. Instead we force a
+ // re-render whenever the subscribed state changes by updating an some
+ // arbitrary useState hook. Then, during render, we call getSnapshot to read
+ // the current value.
+ //
+ // Because we don't actually use the state returned by the useState hook, we
+ // can save a bit of memory by storing other stuff in that slot.
+ //
+ // To implement the early bailout, we need to track some things on a mutable
+ // object. Usually, we would put that in a useRef hook, but we can stash it in
+ // our useState hook instead.
+ //
+ // To force a re-render, we call forceUpdate({inst}). That works because the
+ // new object always fails an equality check.
+ const [{inst}, forceUpdate] = useState({inst: {value, getSnapshot}});
+
+ // Track the latest getSnapshot function with a ref. This needs to be updated
+ // in the layout phase so we can access it during the tearing check that
+ // happens on subscribe.
+ useLayoutEffect(() => {
+ inst.value = value;
+ inst.getSnapshot = getSnapshot;
+
+ // Whenever getSnapshot or subscribe changes, we need to check in the
+ // commit phase if there was an interleaved mutation. In concurrent mode
+ // this can happen all the time, but even in synchronous mode, an earlier
+ // effect may have mutated the store.
+ if (checkIfSnapshotChanged(inst)) {
+ // Force a re-render.
+ forceUpdate({inst});
+ }
+ }, [subscribe, value, getSnapshot]);
+
+ useEffect(() => {
+ // Check for changes right before subscribing. Subsequent changes will be
+ // detected in the subscription handler.
+ if (checkIfSnapshotChanged(inst)) {
+ // Force a re-render.
+ forceUpdate({inst});
+ }
+ const handleStoreChange = () => {
+ // TODO: Because there is no cross-renderer API for batching updates, it's
+ // up to the consumer of this library to wrap their subscription event
+ // with unstable_batchedUpdates. Should we try to detect when this isn't
+ // the case and print a warning in development?
+
+ // The store changed. Check if the snapshot changed since the last time we
+ // read from the store.
+ if (checkIfSnapshotChanged(inst)) {
+ // Force a re-render.
+ forceUpdate({inst});
+ }
+ };
+ // Subscribe to the store and return a clean-up function.
+ return subscribe(handleStoreChange);
+ }, [subscribe]);
+
+ useDebugValue(value);
+ return value;
+}
+
+function checkIfSnapshotChanged(inst) {
+ const latestGetSnapshot = inst.getSnapshot;
+ const prevValue = inst.value;
+ try {
+ const nextValue = latestGetSnapshot();
+ return !is(prevValue, nextValue);
+ } catch (error) {
+ return true;
+ }
+}
diff --git a/packages/use-sync-external-store/src/useSyncExternalStoreServer.js b/packages/use-sync-external-store/src/useSyncExternalStoreServer.js
new file mode 100644
index 0000000000000..1bf2a752273db
--- /dev/null
+++ b/packages/use-sync-external-store/src/useSyncExternalStoreServer.js
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import invariant from 'shared/invariant';
+
+export function useSyncExternalStore(
+ subscribe: (() => void) => () => void,
+ getSnapshot: () => T,
+ getServerSnapshot?: () => T,
+): T {
+ if (getServerSnapshot === undefined) {
+ invariant(
+ false,
+ 'Missing getServerSnapshot, which is required for server-' +
+ 'rendered content.',
+ );
+ }
+ return getServerSnapshot();
+}
diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js
index 047d27e918070..b9ae507b1a305 100644
--- a/scripts/rollup/bundles.js
+++ b/scripts/rollup/bundles.js
@@ -702,6 +702,15 @@ const bundles = [
externals: ['react', 'use-sync-external-store'],
},
+ /******* Shim for useSyncExternalStore ReactNative *******/
+ {
+ bundleTypes: [NODE_DEV, NODE_PROD],
+ moduleType: ISOMORPHIC,
+ entry: 'use-sync-external-store/index.native',
+ global: 'useSyncExternalStoreNative',
+ externals: ['react', 'ReactNativeInternalFeatureFlags'],
+ },
+
/******* React Scheduler (experimental) *******/
{
bundleTypes: [