@samitouri / QOS-React-2 / commits / 4da03c9fbd

useSyncExternalStore React Native version (#22367)

salazarm committed Sep 21, 2021 at 11:07 UTC 4da03c9fbdaf955c637ea95679c32e9ac242992c
8 files changed +397 -166
packages/use-sync-external-store/index.native.js new
+12
@@ -0,0 +1,12 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +'use strict';
11 +
12 +export * from './src/useSyncExternalStoreClient';
packages/use-sync-external-store/npm/index.native.js new
+7
@@ -0,0 +1,7 @@
1 +'use strict';
2 +
3 +if (process.env.NODE_ENV === 'production') {
4 + module.exports = require('./cjs/use-sync-external-store.native.production.min.js');
5 +} else {
6 + module.exports = require('./cjs/use-sync-external-store.native.development.js');
7 +}
packages/use-sync-external-store/package.json
+1
@@ -13,6 +13,7 @@
13 "build-info.json",
14 "index.js",
15 "extra.js",
16 + "index.native.js",
17 "cjs/"
18 ],
19 "license": "MIT",
packages/use-sync-external-store/src/__tests__/useSyncExternalStoreNative-test.js new
+186
@@ -0,0 +1,186 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + *
9 + * @jest-environment node
10 + */
11 +
12 +'use strict';
13 +
14 +let React;
15 +let ReactNoop;
16 +let Scheduler;
17 +let useSyncExternalStore;
18 +let useSyncExternalStoreExtra;
19 +let act;
20 +
21 +// This tests the userspace shim of `useSyncExternalStore` in a server-rendering
22 +// (Node) environment
23 +describe('useSyncExternalStore (userspace shim, server rendering)', () => {
24 + beforeEach(() => {
25 + jest.resetModules();
26 +
27 + // Remove useSyncExternalStore from the React imports so that we use the
28 + // shim instead. Also removing startTransition, since we use that to detect
29 + // outdated 18 alphas that don't yet include useSyncExternalStore.
30 + //
31 + // Longer term, we'll probably test this branch using an actual build of
32 + // React 17.
33 + jest.mock('react', () => {
34 + const {
35 + // eslint-disable-next-line no-unused-vars
36 + startTransition: _,
37 + // eslint-disable-next-line no-unused-vars
38 + useSyncExternalStore: __,
39 + // eslint-disable-next-line no-unused-vars
40 + unstable_useSyncExternalStore: ___,
41 + ...otherExports
42 + } = jest.requireActual('react');
43 + return otherExports;
44 + });
45 +
46 + jest.mock('use-sync-external-store', () =>
47 + jest.requireActual('use-sync-external-store/index.native'),
48 + );
49 +
50 + React = require('react');
51 + ReactNoop = require('react-noop-renderer');
52 + Scheduler = require('scheduler');
53 + act = require('jest-react').act;
54 + useSyncExternalStore = require('use-sync-external-store')
55 + .useSyncExternalStore;
56 + useSyncExternalStoreExtra = require('use-sync-external-store/extra')
57 + .useSyncExternalStoreExtra;
58 + });
59 +
60 + function Text({text}) {
61 + Scheduler.unstable_yieldValue(text);
62 + return text;
63 + }
64 +
65 + function createExternalStore(initialState) {
66 + const listeners = new Set();
67 + let currentState = initialState;
68 + return {
69 + set(text) {
70 + currentState = text;
71 + ReactNoop.batchedUpdates(() => {
72 + listeners.forEach(listener => listener());
73 + });
74 + },
75 + subscribe(listener) {
76 + listeners.add(listener);
77 + return () => listeners.delete(listener);
78 + },
79 + getState() {
80 + return currentState;
81 + },
82 + getSubscriberCount() {
83 + return listeners.size;
84 + },
85 + };
86 + }
87 +
88 + test('native version', async () => {
89 + const store = createExternalStore('client');
90 +
91 + function App() {
92 + const text = useSyncExternalStore(
93 + store.subscribe,
94 + store.getState,
95 + () => 'server',
96 + );
97 + return <Text text={text} />;
98 + }
99 +
100 + const root = ReactNoop.createRoot();
101 + await act(() => {
102 + root.render(<App />);
103 + });
104 + expect(Scheduler).toHaveYielded(['client']);
105 + expect(root).toMatchRenderedOutput('client');
106 + });
107 +
108 + test('native version', async () => {
109 + const store = createExternalStore('client');
110 +
111 + function App() {
112 + const text = useSyncExternalStore(
113 + store.subscribe,
114 + store.getState,
115 + () => 'server',
116 + );
117 + return <Text text={text} />;
118 + }
119 +
120 + const root = ReactNoop.createRoot();
121 + await act(() => {
122 + root.render(<App />);
123 + });
124 + expect(Scheduler).toHaveYielded(['client']);
125 + expect(root).toMatchRenderedOutput('client');
126 + });
127 +
128 + // @gate !(enableUseRefAccessWarning && __DEV__)
129 + test('Using isEqual to bailout', async () => {
130 + const store = createExternalStore({a: 0, b: 0});
131 +
132 + function A() {
133 + const {a} = useSyncExternalStoreExtra(
134 + store.subscribe,
135 + store.getState,
136 + null,
137 + state => ({a: state.a}),
138 + (state1, state2) => state1.a === state2.a,
139 + );
140 + return <Text text={'A' + a} />;
141 + }
142 + function B() {
143 + const {b} = useSyncExternalStoreExtra(
144 + store.subscribe,
145 + store.getState,
146 + null,
147 + state => {
148 + return {b: state.b};
149 + },
150 + (state1, state2) => state1.b === state2.b,
151 + );
152 + return <Text text={'B' + b} />;
153 + }
154 +
155 + function App() {
156 + return (
157 + <>
158 + <A />
159 + <B />
160 + </>
161 + );
162 + }
163 +
164 + const root = ReactNoop.createRoot();
165 + act(() => root.render(<App />));
166 +
167 + expect(Scheduler).toHaveYielded(['A0', 'B0']);
168 + expect(root).toMatchRenderedOutput('A0B0');
169 +
170 + // Update b but not a
171 + await act(() => {
172 + store.set({a: 0, b: 1});
173 + });
174 + // Only b re-renders
175 + expect(Scheduler).toHaveYielded(['B1']);
176 + expect(root).toMatchRenderedOutput('A0B1');
177 +
178 + // Update a but not b
179 + await act(() => {
180 + store.set({a: 1, b: 1});
181 + });
182 + // Only a re-renders
183 + expect(Scheduler).toHaveYielded(['A1']);
184 + expect(root).toMatchRenderedOutput('A1B1');
185 + });
186 +});
packages/use-sync-external-store/src/useSyncExternalStore.js
+3 -166
@@ -7,171 +7,8 @@
7 * @flow
8 */
9
10 -import * as React from 'react';
11 -import is from 'shared/objectIs';
12 -import invariant from 'shared/invariant';
10 import {canUseDOM} from 'shared/ExecutionEnvironment';
11 +import {useSyncExternalStore as client} from './useSyncExternalStoreClient';
12 +import {useSyncExternalStore as server} from './useSyncExternalStoreServer';
13
15 -// Intentionally not using named imports because Rollup uses dynamic
16 -// dispatch for CommonJS interop named imports.
17 -const {
18 - useState,
19 - useEffect,
20 - useLayoutEffect,
21 - useDebugValue,
22 - // The built-in API is still prefixed.
23 - unstable_useSyncExternalStore: builtInAPI,
24 -} = React;
25 -
26 -// TODO: This heuristic doesn't work in React Native. We'll need to provide a
27 -// special build, using the `.native` extension.
28 -const isServerEnvironment = !canUseDOM;
29 -
30 -// Prefer the built-in API, if it exists. If it doesn't exist, then we assume
31 -// we're in version 16 or 17, so rendering is always synchronous. The shim
32 -// does not support concurrent rendering, only the built-in API.
33 -export const useSyncExternalStore =
34 - builtInAPI !== undefined
35 - ? ((builtInAPI: any): typeof useSyncExternalStore_client)
36 - : isServerEnvironment
37 - ? useSyncExternalStore_server
38 - : useSyncExternalStore_client;
39 -
40 -let didWarnOld18Alpha = false;
41 -let didWarnUncachedGetSnapshot = false;
42 -
43 -function useSyncExternalStore_server<T>(
44 - subscribe: (() => void) => () => void,
45 - getSnapshot: () => T,
46 - getServerSnapshot?: () => T,
47 -): T {
48 - if (getServerSnapshot === undefined) {
49 - invariant(
50 - false,
51 - 'Missing getServerSnapshot, which is required for server-' +
52 - 'rendered content.',
53 - );
54 - }
55 - return getServerSnapshot();
56 -}
57 -
58 -// Disclaimer: This shim breaks many of the rules of React, and only works
59 -// because of a very particular set of implementation details and assumptions
60 -// -- change any one of them and it will break. The most important assumption
61 -// is that updates are always synchronous, because concurrent rendering is
62 -// only available in versions of React that also have a built-in
63 -// useSyncExternalStore API. And we only use this shim when the built-in API
64 -// does not exist.
65 -//
66 -// Do not assume that the clever hacks used by this hook also work in general.
67 -// The point of this shim is to replace the need for hacks by other libraries.
68 -function useSyncExternalStore_client<T>(
69 - subscribe: (() => void) => () => void,
70 - getSnapshot: () => T,
71 - // Note: The client shim does not use getServerSnapshot, because pre-18
72 - // versions of React do not expose a way to check if we're hydrating. So
73 - // users of the shim will need to track that themselves and return the
74 - // correct value from `getSnapshot`.
75 - getServerSnapshot?: () => T,
76 -): T {
77 - if (__DEV__) {
78 - if (!didWarnOld18Alpha) {
79 - if (React.startTransition !== undefined) {
80 - didWarnOld18Alpha = true;
81 - console.error(
82 - 'You are using an outdated, pre-release alpha of React 18 that ' +
83 - 'does not support useSyncExternalStore. The ' +
84 - 'use-sync-external-store shim will not work correctly. Upgrade ' +
85 - 'to a newer pre-release.',
86 - );
87 - }
88 - }
89 - }
90 -
91 - // Read the current snapshot from the store on every render. Again, this
92 - // breaks the rules of React, and only works here because of specific
93 - // implementation details, most importantly that updates are
94 - // always synchronous.
95 - const value = getSnapshot();
96 - if (__DEV__) {
97 - if (!didWarnUncachedGetSnapshot) {
98 - if (value !== getSnapshot()) {
99 - console.error(
100 - 'The result of getSnapshot should be cached to avoid an infinite loop',
101 - );
102 - didWarnUncachedGetSnapshot = true;
103 - }
104 - }
105 - }
106 -
107 - // Because updates are synchronous, we don't queue them. Instead we force a
108 - // re-render whenever the subscribed state changes by updating an some
109 - // arbitrary useState hook. Then, during render, we call getSnapshot to read
110 - // the current value.
111 - //
112 - // Because we don't actually use the state returned by the useState hook, we
113 - // can save a bit of memory by storing other stuff in that slot.
114 - //
115 - // To implement the early bailout, we need to track some things on a mutable
116 - // object. Usually, we would put that in a useRef hook, but we can stash it in
117 - // our useState hook instead.
118 - //
119 - // To force a re-render, we call forceUpdate({inst}). That works because the
120 - // new object always fails an equality check.
121 - const [{inst}, forceUpdate] = useState({inst: {value, getSnapshot}});
122 -
123 - // Track the latest getSnapshot function with a ref. This needs to be updated
124 - // in the layout phase so we can access it during the tearing check that
125 - // happens on subscribe.
126 - useLayoutEffect(() => {
127 - inst.value = value;
128 - inst.getSnapshot = getSnapshot;
129 -
130 - // Whenever getSnapshot or subscribe changes, we need to check in the
131 - // commit phase if there was an interleaved mutation. In concurrent mode
132 - // this can happen all the time, but even in synchronous mode, an earlier
133 - // effect may have mutated the store.
134 - if (checkIfSnapshotChanged(inst)) {
135 - // Force a re-render.
136 - forceUpdate({inst});
137 - }
138 - }, [subscribe, value, getSnapshot]);
139 -
140 - useEffect(() => {
141 - // Check for changes right before subscribing. Subsequent changes will be
142 - // detected in the subscription handler.
143 - if (checkIfSnapshotChanged(inst)) {
144 - // Force a re-render.
145 - forceUpdate({inst});
146 - }
147 - const handleStoreChange = () => {
148 - // TODO: Because there is no cross-renderer API for batching updates, it's
149 - // up to the consumer of this library to wrap their subscription event
150 - // with unstable_batchedUpdates. Should we try to detect when this isn't
151 - // the case and print a warning in development?
152 -
153 - // The store changed. Check if the snapshot changed since the last time we
154 - // read from the store.
155 - if (checkIfSnapshotChanged(inst)) {
156 - // Force a re-render.
157 - forceUpdate({inst});
158 - }
159 - };
160 - // Subscribe to the store and return a clean-up function.
161 - return subscribe(handleStoreChange);
162 - }, [subscribe]);
163 -
164 - useDebugValue(value);
165 - return value;
166 -}
167 -
168 -function checkIfSnapshotChanged(inst) {
169 - const latestGetSnapshot = inst.getSnapshot;
170 - const prevValue = inst.value;
171 - try {
172 - const nextValue = latestGetSnapshot();
173 - return !is(prevValue, nextValue);
174 - } catch (error) {
175 - return true;
176 - }
177 -}
14 +export const useSyncExternalStore = canUseDOM ? client : server;
packages/use-sync-external-store/src/useSyncExternalStoreClient.js new
+154
@@ -0,0 +1,154 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import * as React from 'react';
11 +import is from 'shared/objectIs';
12 +
13 +// Intentionally not using named imports because Rollup uses dynamic
14 +// dispatch for CommonJS interop named imports.
15 +const {
16 + useState,
17 + useEffect,
18 + useLayoutEffect,
19 + useDebugValue,
20 + // The built-in API is still prefixed.
21 + unstable_useSyncExternalStore: builtInAPI,
22 +} = React;
23 +
24 +// Prefer the built-in API, if it exists. If it doesn't exist, then we assume
25 +// we're in version 16 or 17, so rendering is always synchronous. The shim
26 +// does not support concurrent rendering, only the built-in API.
27 +export const useSyncExternalStore =
28 + builtInAPI !== undefined
29 + ? ((builtInAPI: any): typeof useSyncExternalStore_client)
30 + : useSyncExternalStore_client;
31 +
32 +let didWarnOld18Alpha = false;
33 +let didWarnUncachedGetSnapshot = false;
34 +
35 +// Disclaimer: This shim breaks many of the rules of React, and only works
36 +// because of a very particular set of implementation details and assumptions
37 +// -- change any one of them and it will break. The most important assumption
38 +// is that updates are always synchronous, because concurrent rendering is
39 +// only available in versions of React that also have a built-in
40 +// useSyncExternalStore API. And we only use this shim when the built-in API
41 +// does not exist.
42 +//
43 +// Do not assume that the clever hacks used by this hook also work in general.
44 +// The point of this shim is to replace the need for hacks by other libraries.
45 +function useSyncExternalStore_client<T>(
46 + subscribe: (() => void) => () => void,
47 + getSnapshot: () => T,
48 + // Note: The client shim does not use getServerSnapshot, because pre-18
49 + // versions of React do not expose a way to check if we're hydrating. So
50 + // users of the shim will need to track that themselves and return the
51 + // correct value from `getSnapshot`.
52 + getServerSnapshot?: () => T,
53 +): T {
54 + if (__DEV__) {
55 + if (!didWarnOld18Alpha) {
56 + if (React.startTransition !== undefined) {
57 + didWarnOld18Alpha = true;
58 + console.error(
59 + 'You are using an outdated, pre-release alpha of React 18 that ' +
60 + 'does not support useSyncExternalStore. The ' +
61 + 'use-sync-external-store shim will not work correctly. Upgrade ' +
62 + 'to a newer pre-release.',
63 + );
64 + }
65 + }
66 + }
67 +
68 + // Read the current snapshot from the store on every render. Again, this
69 + // breaks the rules of React, and only works here because of specific
70 + // implementation details, most importantly that updates are
71 + // always synchronous.
72 + const value = getSnapshot();
73 + if (__DEV__) {
74 + if (!didWarnUncachedGetSnapshot) {
75 + if (value !== getSnapshot()) {
76 + console.error(
77 + 'The result of getSnapshot should be cached to avoid an infinite loop',
78 + );
79 + didWarnUncachedGetSnapshot = true;
80 + }
81 + }
82 + }
83 +
84 + // Because updates are synchronous, we don't queue them. Instead we force a
85 + // re-render whenever the subscribed state changes by updating an some
86 + // arbitrary useState hook. Then, during render, we call getSnapshot to read
87 + // the current value.
88 + //
89 + // Because we don't actually use the state returned by the useState hook, we
90 + // can save a bit of memory by storing other stuff in that slot.
91 + //
92 + // To implement the early bailout, we need to track some things on a mutable
93 + // object. Usually, we would put that in a useRef hook, but we can stash it in
94 + // our useState hook instead.
95 + //
96 + // To force a re-render, we call forceUpdate({inst}). That works because the
97 + // new object always fails an equality check.
98 + const [{inst}, forceUpdate] = useState({inst: {value, getSnapshot}});
99 +
100 + // Track the latest getSnapshot function with a ref. This needs to be updated
101 + // in the layout phase so we can access it during the tearing check that
102 + // happens on subscribe.
103 + useLayoutEffect(() => {
104 + inst.value = value;
105 + inst.getSnapshot = getSnapshot;
106 +
107 + // Whenever getSnapshot or subscribe changes, we need to check in the
108 + // commit phase if there was an interleaved mutation. In concurrent mode
109 + // this can happen all the time, but even in synchronous mode, an earlier
110 + // effect may have mutated the store.
111 + if (checkIfSnapshotChanged(inst)) {
112 + // Force a re-render.
113 + forceUpdate({inst});
114 + }
115 + }, [subscribe, value, getSnapshot]);
116 +
117 + useEffect(() => {
118 + // Check for changes right before subscribing. Subsequent changes will be
119 + // detected in the subscription handler.
120 + if (checkIfSnapshotChanged(inst)) {
121 + // Force a re-render.
122 + forceUpdate({inst});
123 + }
124 + const handleStoreChange = () => {
125 + // TODO: Because there is no cross-renderer API for batching updates, it's
126 + // up to the consumer of this library to wrap their subscription event
127 + // with unstable_batchedUpdates. Should we try to detect when this isn't
128 + // the case and print a warning in development?
129 +
130 + // The store changed. Check if the snapshot changed since the last time we
131 + // read from the store.
132 + if (checkIfSnapshotChanged(inst)) {
133 + // Force a re-render.
134 + forceUpdate({inst});
135 + }
136 + };
137 + // Subscribe to the store and return a clean-up function.
138 + return subscribe(handleStoreChange);
139 + }, [subscribe]);
140 +
141 + useDebugValue(value);
142 + return value;
143 +}
144 +
145 +function checkIfSnapshotChanged(inst) {
146 + const latestGetSnapshot = inst.getSnapshot;
147 + const prevValue = inst.value;
148 + try {
149 + const nextValue = latestGetSnapshot();
150 + return !is(prevValue, nextValue);
151 + } catch (error) {
152 + return true;
153 + }
154 +}
packages/use-sync-external-store/src/useSyncExternalStoreServer.js new
+25
@@ -0,0 +1,25 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import invariant from 'shared/invariant';
11 +
12 +export function useSyncExternalStore<T>(
13 + subscribe: (() => void) => () => void,
14 + getSnapshot: () => T,
15 + getServerSnapshot?: () => T,
16 +): T {
17 + if (getServerSnapshot === undefined) {
18 + invariant(
19 + false,
20 + 'Missing getServerSnapshot, which is required for server-' +
21 + 'rendered content.',
22 + );
23 + }
24 + return getServerSnapshot();
25 +}
scripts/rollup/bundles.js
+9
@@ -702,6 +702,15 @@ const bundles = [
702 externals: ['react', 'use-sync-external-store'],
703 },
704
705 + /******* Shim for useSyncExternalStore ReactNative *******/
706 + {
707 + bundleTypes: [NODE_DEV, NODE_PROD],
708 + moduleType: ISOMORPHIC,
709 + entry: 'use-sync-external-store/index.native',
710 + global: 'useSyncExternalStoreNative',
711 + externals: ['react', 'ReactNativeInternalFeatureFlags'],
712 + },
713 +
714 /******* React Scheduler (experimental) *******/
715 {
716 bundleTypes: [