Skip to content

Commit 9e9df92

Browse files
committed
[wip] Combine useRef and useState hooks
Since the useState hook is only used to force a render, and we don't use it to track any state, we can stash our mutable instance object in there instead of using a separate useRef hook. Doesn't affect any behavior, just saves a bit of hook memory.
1 parent 6a9738d commit 9e9df92

File tree

2 files changed

+17
-62
lines changed

2 files changed

+17
-62
lines changed

packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShared-test.js

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,6 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
9696
};
9797
}
9898

99-
// 18 has an experimental flag to warn about reading refs. Will circumvent
100-
// when built-in API is implemented.
101-
// @gate !enableUseRefAccessWarning || !__DEV__
10299
test('basic usage', () => {
103100
const store = createExternalStore('Initial');
104101

@@ -141,9 +138,6 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
141138
expect(root).toMatchRenderedOutput('Initial');
142139
});
143140

144-
// 18 has an experimental flag to warn about reading refs. Will circumvent
145-
// when built-in API is implemented.
146-
// @gate !enableUseRefAccessWarning || !__DEV__
147141
test('switch to a different store', () => {
148142
const storeA = createExternalStore(0);
149143
const storeB = createExternalStore(0);
@@ -193,9 +187,6 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
193187
expect(root).toMatchRenderedOutput('1');
194188
});
195189

196-
// 18 has an experimental flag to warn about reading refs. Will circumvent
197-
// when built-in API is implemented.
198-
// @gate !enableUseRefAccessWarning || !__DEV__
199190
test('selecting a specific value inside getSnapshot', () => {
200191
const store = createExternalStore({a: 0, b: 0});
201192

@@ -239,9 +230,6 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
239230
expect(root).toMatchRenderedOutput('A1B1');
240231
});
241232

242-
// 18 has an experimental flag to warn about reading refs. Will circumvent
243-
// when built-in API is implemented.
244-
// @gate !enableUseRefAccessWarning || !__DEV__
245233
test(
246234
"compares to current state before bailing out, even when there's a " +
247235
'mutation in between the sync and passive effects',
@@ -284,9 +272,6 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
284272
},
285273
);
286274

287-
// 18 has an experimental flag to warn about reading refs. Will circumvent
288-
// when built-in API is implemented.
289-
// @gate !enableUseRefAccessWarning || !__DEV__
290275
test('mutating the store in between render and commit when getSnapshot has changed', () => {
291276
const store = createExternalStore({a: 1, b: 1});
292277

@@ -345,9 +330,6 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
345330
expect(root).toMatchRenderedOutput('B2');
346331
});
347332

348-
// 18 has an experimental flag to warn about reading refs. Will circumvent
349-
// when built-in API is implemented.
350-
// @gate !enableUseRefAccessWarning || !__DEV__
351333
test('mutating the store in between render and commit when getSnapshot has _not_ changed', () => {
352334
// Same as previous test, but `getSnapshot` does not change
353335
const store = createExternalStore({a: 1, b: 1});
@@ -405,9 +387,6 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
405387
expect(root).toMatchRenderedOutput('A1');
406388
});
407389

408-
// 18 has an experimental flag to warn about reading refs. Will circumvent
409-
// when built-in API is implemented.
410-
// @gate !enableUseRefAccessWarning || !__DEV__
411390
test("does not bail out if the previous update hasn't finished yet", () => {
412391
const store = createExternalStore(0);
413392

@@ -443,9 +422,6 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
443422
expect(root).toMatchRenderedOutput('00');
444423
});
445424

446-
// 18 has an experimental flag to warn about reading refs. Will circumvent
447-
// when built-in API is implemented.
448-
// @gate !enableUseRefAccessWarning || !__DEV__
449425
test('uses the latest getSnapshot, even if it changed in the same batch as a store update', () => {
450426
const store = createExternalStore({a: 0, b: 0});
451427

@@ -473,9 +449,6 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
473449
expect(root).toMatchRenderedOutput('2');
474450
});
475451

476-
// 18 has an experimental flag to warn about reading refs. Will circumvent
477-
// when built-in API is implemented.
478-
// @gate !enableUseRefAccessWarning || !__DEV__
479452
test('handles errors thrown by getSnapshot or isEqual', () => {
480453
class ErrorBoundary extends React.Component {
481454
state = {error: null};
@@ -552,9 +525,6 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
552525
});
553526

554527
describe('extra features implemented in user-space', () => {
555-
// 18 has an experimental flag to warn about reading refs. Will circumvent
556-
// when built-in API is implemented.
557-
// @gate !enableUseRefAccessWarning || !__DEV__
558528
test('memoized selectors are only called once per update', () => {
559529
const store = createExternalStore({a: 0, b: 0});
560530

@@ -593,9 +563,6 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
593563
expect(root).toMatchRenderedOutput('A1');
594564
});
595565

596-
// 18 has an experimental flag to warn about reading refs. Will circumvent
597-
// when built-in API is implemented.
598-
// @gate !enableUseRefAccessWarning || !__DEV__
599566
test('Using isEqual to bailout', () => {
600567
const store = createExternalStore({a: 0, b: 0});
601568

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

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import is from 'shared/objectIs';
1414
// dispatch for CommonJS interop named imports.
1515
const {
1616
useState,
17-
useRef,
1817
useEffect,
1918
useLayoutEffect,
2019
useDebugValue,
@@ -59,34 +58,27 @@ function useSyncExternalStore_shim<T>(
5958
}
6059
}
6160

62-
// Because updates are synchronous, we don't queue them. Instead we force a
63-
// re-render whenever the subscribed state changes, and read the current state
64-
// directly in render.
65-
//
66-
// We force a re-render by bumping a version number.
67-
const [, setVersion] = useState(0);
68-
6961
// Read the current snapshot from the store on every render. Again, this
7062
// breaks the rules of React, and only works here because of specific
7163
// implementation details, most importantly that updates are
7264
// always synchronous.
7365
const value = getSnapshot();
7466

67+
// Because updates are synchronous, we don't queue them. Instead we force a
68+
// re-render whenever the subscribed state changes by updating an some
69+
// arbitrary useState hook. Then, during render, we call getSnapshot to read
70+
// the current value.
71+
//
72+
// Because we don't actually use the state returned by the useState hook, we
73+
// can save a bit of memory by storing other stuff in that slot.
74+
//
7575
// To implement the early bailout, we need to track some things on a mutable
76-
// ref object. Initialize this object on the first render.
77-
const refs = useRef(null);
78-
let inst;
79-
if (refs.current === null) {
80-
inst = {
81-
// This represents the currently rendered value and getSnapshot function.
82-
// We update them with a ref whenever they change.
83-
value,
84-
getSnapshot,
85-
};
86-
refs.current = inst;
87-
} else {
88-
inst = refs.current;
89-
}
76+
// object. Usually, we would put that with a useRef hook, but we can stash
77+
// that in our useState hook instead.
78+
//
79+
// To force a re-render, we call forceUpdate({inst}). That works because the
80+
// new object always fails an equality check.
81+
const [{inst}, forceUpdate] = useState({inst: {value, getSnapshot}});
9082

9183
// Track the latest getSnapshot function with a ref. This needs to be updated
9284
// in the layout phase so that we can access it during the tearing check that
@@ -102,7 +94,7 @@ function useSyncExternalStore_shim<T>(
10294
// effect may have mutated the store.
10395
if (checkIfSnapshotChanged(inst)) {
10496
// Force a re-render.
105-
setVersion(bumpVersion);
97+
forceUpdate({inst});
10698
}
10799
}, [subscribe, value, getSnapshot]);
108100

@@ -111,7 +103,7 @@ function useSyncExternalStore_shim<T>(
111103
// detected in the subscription handler.
112104
if (checkIfSnapshotChanged(inst)) {
113105
// Force a re-render.
114-
setVersion(bumpVersion);
106+
forceUpdate({inst});
115107
}
116108
const handleStoreChange = () => {
117109
// TODO: Because there is no cross-renderer API for batching updates, it's
@@ -123,7 +115,7 @@ function useSyncExternalStore_shim<T>(
123115
// read from the store.
124116
if (checkIfSnapshotChanged(inst)) {
125117
// Force a re-render.
126-
setVersion(bumpVersion);
118+
forceUpdate({inst});
127119
}
128120
};
129121
// Subscribe to the store and return a clean-up function.
@@ -144,7 +136,3 @@ function checkIfSnapshotChanged(inst) {
144136
return true;
145137
}
146138
}
147-
148-
function bumpVersion(v) {
149-
return v + 1;
150-
}

0 commit comments

Comments
 (0)