Skip to content

Commit 1c9f631

Browse files
committed
[wip] Update current value with a ref
Originally we updated it in the subscription handler so that we could be sure it always represented the latest snapshot. However, we run into problems because the `getSnapshot` could change the next time in the same batch as the re-render, in which case the value is no longer correct. So this moves it back to the commit phase. We avoid the stale `getSnapshot` problem with a tearing check in the commit phase whenever `getSnapshot` changes. Since `getSnapshot` is designed to be fast, I combined both into a single `useLayoutEffect`, along with the one we do when `subscribe` changes, to save some memory.
1 parent f866470 commit 1c9f631

1 file changed

Lines changed: 9 additions & 30 deletions

File tree

packages/use-sync-external-store/src/useSyncExternalStore.js

Lines changed: 9 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -78,48 +78,33 @@ function useSyncExternalStore_shim<T>(
7878
let inst;
7979
if (refs.current === null) {
8080
inst = {
81-
// This represents the last value that we read from the external store,
82-
// not the last value that we rendered. We can use it to eagerly bail out,
83-
// regardless of the current rendered state.
81+
// This represents the currently rendered value and getSnapshot function.
82+
// We update them with a ref whenever they change.
8483
value,
85-
86-
// This is the currently rendered getSnapshot functions. We track them
87-
// with a commit effect. However, it's possible we might receive a store
88-
// update in between render and when the commit effect runs. Refer to the
89-
// next branch to see how we handle that.
9084
getSnapshot,
9185
};
9286
refs.current = inst;
9387
} else {
9488
inst = refs.current;
95-
96-
// Every time we read from the store, we must update the latest value.
97-
inst.value = value;
9889
}
9990

10091
// Track the latest getSnapshot function with a ref. This needs to be updated
10192
// in the layout phase so that we can access it during the tearing check that
10293
// happens on subscribe.
10394
// TODO: Circumvent SSR warning
10495
useLayoutEffect(() => {
96+
inst.value = value;
10597
inst.getSnapshot = getSnapshot;
106-
if (checkIfSnapshotChanged(inst)) {
107-
// Force a re-render.
108-
setVersion(bumpVersion);
109-
}
110-
}, [getSnapshot]);
11198

112-
// We re-subscribe in a passive effect whenever a new subscribe function is
113-
// passed. Before we allow the browser to paint, though, we should check to
114-
// confirm that the value we read during render is consistent, in case there
115-
// were additional mutations since then.
116-
// TODO: Circumvent SSR warning
117-
useLayoutEffect(() => {
99+
// Whenever getSnapshot or subscribe changes, we need to check in the
100+
// commit phase if there was an interleaved mutation. In concurrent mode
101+
// this can happen all the time, but even in synchronous mode, an earlier
102+
// effect may have mutated the store.
118103
if (checkIfSnapshotChanged(inst)) {
119104
// Force a re-render.
120105
setVersion(bumpVersion);
121106
}
122-
}, [subscribe]);
107+
}, [subscribe, value, getSnapshot]);
123108

124109
useEffect(() => {
125110
// Check for changes right before subscribing. Subsequent changes will be
@@ -154,13 +139,7 @@ function checkIfSnapshotChanged(inst) {
154139
const prevValue = inst.value;
155140
try {
156141
const nextValue = latestGetSnapshot();
157-
if (is(prevValue, nextValue)) {
158-
return false;
159-
} else {
160-
// Force a re-render.
161-
inst.value = nextValue;
162-
return true;
163-
}
142+
return !is(prevValue, nextValue);
164143
} catch (error) {
165144
return true;
166145
}

0 commit comments

Comments
 (0)