Skip to content

Commit 1e3e30d

Browse files
authored
Fix useSyncExternalStore dropped update when state is dispatched in render phase (#25578)
Fix #25565
1 parent 18dff79 commit 1e3e30d

File tree

3 files changed

+33
-2
lines changed

3 files changed

+33
-2
lines changed

packages/react-reconciler/src/ReactFiberHooks.new.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1615,7 +1615,7 @@ function updateSyncExternalStore<T>(
16151615
}
16161616
}
16171617
}
1618-
const prevSnapshot = hook.memoizedState;
1618+
const prevSnapshot = (currentHook || hook).memoizedState;
16191619
const snapshotChanged = !is(prevSnapshot, nextSnapshot);
16201620
if (snapshotChanged) {
16211621
hook.memoizedState = nextSnapshot;

packages/react-reconciler/src/ReactFiberHooks.old.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1615,7 +1615,7 @@ function updateSyncExternalStore<T>(
16151615
}
16161616
}
16171617
}
1618-
const prevSnapshot = hook.memoizedState;
1618+
const prevSnapshot = (currentHook || hook).memoizedState;
16191619
const snapshotChanged = !is(prevSnapshot, nextSnapshot);
16201620
if (snapshotChanged) {
16211621
hook.memoizedState = nextSnapshot;

packages/react-reconciler/src/__tests__/useSyncExternalStore-test.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ let useLayoutEffect;
1818
let forwardRef;
1919
let useImperativeHandle;
2020
let useRef;
21+
let useState;
2122
let startTransition;
2223

2324
// This tests the native useSyncExternalStore implementation, not the shim.
@@ -36,6 +37,7 @@ describe('useSyncExternalStore', () => {
3637
useImperativeHandle = React.useImperativeHandle;
3738
forwardRef = React.forwardRef;
3839
useRef = React.useRef;
40+
useState = React.useState;
3941
useSyncExternalStore = React.useSyncExternalStore;
4042
startTransition = React.startTransition;
4143

@@ -173,4 +175,33 @@ describe('useSyncExternalStore', () => {
173175
});
174176
},
175177
);
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+
});
176207
});

0 commit comments

Comments
 (0)