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: [