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
});