@samitouri / QOS-React-2 / commits / 1e3e30dae2

Fix useSyncExternalStore dropped update when state is dispatched in render phase (#25578)

Fix https://github.com/facebook/react/issues/25565

Aurélien Chivot-Buhler committed Nov 8, 2022 at 10:25 UTC 1e3e30dae2bcfbeb0abc686f2a37aec208eedb39
3 files changed +33 -2
packages/react-reconciler/src/ReactFiberHooks.new.js
+1 -1
@@ -1615,7 +1615,7 @@ function updateSyncExternalStore<T>(
1615 }
1616 }
1617 }
1618 - const prevSnapshot = hook.memoizedState;
1618 + const prevSnapshot = (currentHook || hook).memoizedState;
1619 const snapshotChanged = !is(prevSnapshot, nextSnapshot);
1620 if (snapshotChanged) {
1621 hook.memoizedState = nextSnapshot;
packages/react-reconciler/src/ReactFiberHooks.old.js
+1 -1
@@ -1615,7 +1615,7 @@ function updateSyncExternalStore<T>(
1615 }
1616 }
1617 }
1618 - const prevSnapshot = hook.memoizedState;
1618 + const prevSnapshot = (currentHook || hook).memoizedState;
1619 const snapshotChanged = !is(prevSnapshot, nextSnapshot);
1620 if (snapshotChanged) {
1621 hook.memoizedState = nextSnapshot;
packages/react-reconciler/src/__tests__/useSyncExternalStore-test.js
+31
@@ -18,6 +18,7 @@ let useLayoutEffect;
18 let forwardRef;
19 let useImperativeHandle;
20 let useRef;
21 +let useState;
22 let startTransition;
23
24 // This tests the native useSyncExternalStore implementation, not the shim.
@@ -36,6 +37,7 @@ describe('useSyncExternalStore', () => {
37 useImperativeHandle = React.useImperativeHandle;
38 forwardRef = React.forwardRef;
39 useRef = React.useRef;
40 + useState = React.useState;
41 useSyncExternalStore = React.useSyncExternalStore;
42 startTransition = React.startTransition;
43
@@ -173,4 +175,33 @@ describe('useSyncExternalStore', () => {
175 });
176 },
177 );
178 +
179 + test('next value is correctly cached when state is dispatched in render phase', async () => {
180 + const store = createExternalStore('value:initial');
181 +
182 + function App() {
183 + const value = useSyncExternalStore(store.subscribe, store.getState);
184 + const [sameValue, setSameValue] = useState(value);
185 + if (value !== sameValue) setSameValue(value);
186 + return <Text text={value} />;
187 + }
188 +
189 + const root = ReactNoop.createRoot();
190 + act(() => {
191 + // Start a render that reads from the store and yields value
192 + root.render(<App />);
193 + });
194 + expect(Scheduler).toHaveYielded(['value:initial']);
195 +
196 + await act(() => {
197 + store.set('value:changed');
198 + });
199 + expect(Scheduler).toHaveYielded(['value:changed']);
200 +
201 + // If cached value was updated, we expect a re-render
202 + await act(() => {
203 + store.set('value:initial');
204 + });
205 + expect(Scheduler).toHaveYielded(['value:initial']);
206 + });
207 });