main
js 182 lines 4.86 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and 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 useSyncExternalStoreWithSelector;
19 let act;
20 let assertLog;
21
22 // This tests the userspace shim of `useSyncExternalStore` in a server-rendering
23 // (Node) environment
24 describe('useSyncExternalStore (userspace shim, server rendering)', () => {
25 beforeEach(() => {
26 jest.resetModules();
27
28 // Remove useSyncExternalStore from the React imports so that we use the
29 // shim instead. Also removing startTransition, since we use that to detect
30 // outdated 18 alphas that don't yet include useSyncExternalStore.
31 //
32 // Longer term, we'll probably test this branch using an actual build of
33 // React 17.
34 jest.mock('react', () => {
35 const {
36 startTransition: _,
37 useSyncExternalStore: __,
38 ...otherExports
39 } = jest.requireActual('react');
40 return otherExports;
41 });
42
43 jest.mock('use-sync-external-store/shim', () =>
44 jest.requireActual('use-sync-external-store/shim/index.native'),
45 );
46
47 React = require('react');
48 ReactNoop = require('react-noop-renderer');
49 Scheduler = require('scheduler');
50 act = require('internal-test-utils').act;
51
52 const InternalTestUtils = require('internal-test-utils');
53 assertLog = InternalTestUtils.assertLog;
54
55 if (gate(flags => flags.source)) {
56 // The `shim/with-selector` module composes the main
57 // `use-sync-external-store` entrypoint. In the compiled artifacts, this
58 // is resolved to the `shim` implementation by our build config, but when
59 // running the tests against the source files, we need to tell Jest how to
60 // resolve it. Because this is a source module, this mock has no affect on
61 // the build tests.
62 jest.mock('use-sync-external-store/src/useSyncExternalStore', () =>
63 jest.requireActual('use-sync-external-store/shim'),
64 );
65 jest.mock('use-sync-external-store/src/isServerEnvironment', () =>
66 jest.requireActual(
67 'use-sync-external-store/src/forks/isServerEnvironment.native',
68 ),
69 );
70 }
71 useSyncExternalStore =
72 require('use-sync-external-store/shim').useSyncExternalStore;
73 useSyncExternalStoreWithSelector =
74 require('use-sync-external-store/shim/with-selector').useSyncExternalStoreWithSelector;
75 });
76
77 function Text({text}) {
78 Scheduler.log(text);
79 return text;
80 }
81
82 function createExternalStore(initialState) {
83 const listeners = new Set();
84 let currentState = initialState;
85 return {
86 set(text) {
87 currentState = text;
88 ReactNoop.batchedUpdates(() => {
89 listeners.forEach(listener => listener());
90 });
91 },
92 subscribe(listener) {
93 listeners.add(listener);
94 return () => listeners.delete(listener);
95 },
96 getState() {
97 return currentState;
98 },
99 getSubscriberCount() {
100 return listeners.size;
101 },
102 };
103 }
104
105 it('native version', async () => {
106 const store = createExternalStore('client');
107
108 function App() {
109 const text = useSyncExternalStore(
110 store.subscribe,
111 store.getState,
112 () => 'server',
113 );
114 return <Text text={text} />;
115 }
116
117 const root = ReactNoop.createRoot();
118 await act(() => {
119 root.render(<App />);
120 });
121 assertLog(['client']);
122 expect(root).toMatchRenderedOutput('client');
123 });
124
125 it('Using isEqual to bailout', async () => {
126 const store = createExternalStore({a: 0, b: 0});
127
128 function A() {
129 const {a} = useSyncExternalStoreWithSelector(
130 store.subscribe,
131 store.getState,
132 null,
133 state => ({a: state.a}),
134 (state1, state2) => state1.a === state2.a,
135 );
136 return <Text text={'A' + a} />;
137 }
138 function B() {
139 const {b} = useSyncExternalStoreWithSelector(
140 store.subscribe,
141 store.getState,
142 null,
143 state => {
144 return {b: state.b};
145 },
146 (state1, state2) => state1.b === state2.b,
147 );
148 return <Text text={'B' + b} />;
149 }
150
151 function App() {
152 return (
153 <>
154 <A />
155 <B />
156 </>
157 );
158 }
159
160 const root = ReactNoop.createRoot();
161 await act(() => root.render(<App />));
162
163 assertLog(['A0', 'B0']);
164 expect(root).toMatchRenderedOutput('A0B0');
165
166 // Update b but not a
167 await act(() => {
168 store.set({a: 0, b: 1});
169 });
170 // Only b re-renders
171 assertLog(['B1']);
172 expect(root).toMatchRenderedOutput('A0B1');
173
174 // Update a but not b
175 await act(() => {
176 store.set({a: 1, b: 1});
177 });
178 // Only a re-renders
179 assertLog(['A1']);
180 expect(root).toMatchRenderedOutput('A1B1');
181 });
182 });