Skip to content

Commit bc57bac

Browse files
author
Brian Vaughn
committed
useMutableSource: bugfix for new getSnapshot with mutation
1 parent 885ed46 commit bc57bac

File tree

2 files changed

+71
-0
lines changed

2 files changed

+71
-0
lines changed

Diff for: packages/react-reconciler/src/ReactFiberHooks.js

+13
Original file line numberDiff line numberDiff line change
@@ -991,6 +991,19 @@ function useMutableSource<Source, Snapshot>(
991991
// Sync the values needed by our subscribe function after each commit.
992992
dispatcher.useEffect(() => {
993993
refs.getSnapshot = getSnapshot;
994+
995+
// Because getSnapshot is shared with subscriptions via a ref,
996+
// we don't resubscribe when getSnapshot changes.
997+
// This means that we also don't check for any missed mutations
998+
// between the render and the passive commit though.
999+
// So we need to check here, just like when we newly subscribe.
1000+
const maybeNewVersion = getVersion(source._source);
1001+
if (!is(version, maybeNewVersion)) {
1002+
const maybeNewSnapshot = getSnapshot(source._source);
1003+
if (!is(snapshot, maybeNewSnapshot)) {
1004+
setSnapshot(maybeNewSnapshot);
1005+
}
1006+
}
9941007
}, [getSnapshot]);
9951008

9961009
// If we got a new source or subscribe function,

Diff for: packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js

+58
Original file line numberDiff line numberDiff line change
@@ -1265,6 +1265,64 @@ describe('useMutableSource', () => {
12651265
expect(Scheduler).toHaveYielded(['x: bar, y: bar']);
12661266
});
12671267

1268+
it('getSnapshot changes and then source is mutated in between paint and passive effect phase', async () => {
1269+
const source = createSource({
1270+
a: 'foo',
1271+
b: 'bar',
1272+
});
1273+
const mutableSource = createMutableSource(source);
1274+
1275+
function mutateB(newB) {
1276+
source.value = {
1277+
...source.value,
1278+
b: newB,
1279+
};
1280+
}
1281+
1282+
const getSnapshotA = () => source.value.a;
1283+
const getSnapshotB = () => source.value.b;
1284+
1285+
function App({getSnapshot}) {
1286+
const value = useMutableSource(
1287+
mutableSource,
1288+
getSnapshot,
1289+
defaultSubscribe,
1290+
);
1291+
1292+
Scheduler.unstable_yieldValue('Render: ' + value);
1293+
React.useEffect(() => {
1294+
Scheduler.unstable_yieldValue('Commit: ' + value);
1295+
}, [value]);
1296+
1297+
return value;
1298+
}
1299+
1300+
const root = ReactNoop.createRoot();
1301+
await act(async () => {
1302+
root.render(<App getSnapshot={getSnapshotA} />);
1303+
});
1304+
expect(Scheduler).toHaveYielded(['Render: foo', 'Commit: foo']);
1305+
1306+
await act(async () => {
1307+
// Switch getSnapshot to read from B instead
1308+
root.render(<App getSnapshot={getSnapshotB} />);
1309+
// Render and finish the tree, but yield right after paint, before
1310+
// the passive effects have fired.
1311+
expect(Scheduler).toFlushUntilNextPaint(['Render: bar']);
1312+
// Then mutate B.
1313+
mutateB('baz');
1314+
});
1315+
expect(Scheduler).toHaveYielded([
1316+
// Fires the effect from the previous render
1317+
'Commit: bar',
1318+
// During that effect, it should detect that the snapshot has changed
1319+
// and re-render.
1320+
'Render: baz',
1321+
'Commit: baz',
1322+
]);
1323+
expect(root).toMatchRenderedOutput('baz');
1324+
});
1325+
12681326
if (__DEV__) {
12691327
describe('dev warnings', () => {
12701328
it('should warn if the subscribe function does not return an unsubscribe function', () => {

0 commit comments

Comments
 (0)