Skip to content

Commit 92dd7c0

Browse files
committed
replace with useSyncExternalStore
1 parent d90ddae commit 92dd7c0

File tree

2 files changed

+52
-21
lines changed

2 files changed

+52
-21
lines changed

packages/react/src/index.ts

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { Effect, ReactDispatcher } from "./internal";
1919

2020
export { signal, computed, batch, effect, Signal, type ReadonlySignal };
2121

22+
const Empty = Object.freeze([]);
23+
2224
/**
2325
* Install a middleware into React.createElement to replace any Signals in props with their value.
2426
* @todo this likely needs to be duplicated for jsx()...
@@ -55,7 +57,6 @@ function createPropUpdater(props: any, prop: string, signal: Signal) {
5557
*/
5658

5759
let finishUpdate: (() => void) | undefined;
58-
const updaterForComponent = new WeakMap<() => void, Effect>();
5960

6061
function setCurrentUpdater(updater?: Effect) {
6162
// end tracking for the current update:
@@ -64,13 +65,39 @@ function setCurrentUpdater(updater?: Effect) {
6465
finishUpdate = updater && updater._start();
6566
}
6667

67-
function createUpdater(update: () => void) {
68+
/**
69+
* A redux-like store whose store value is a positive 32bit integer (a 'version') to be used with useSyncExternalStore API.
70+
* React (current owner) subscribes to this store and gets a snapshot of the current 'version'.
71+
* Whenever the 'version' changes, we tell React it's time to update the component (call 'onStoreChange').
72+
*
73+
* How we achieve this is by creating a binding with an 'effect', when the `effect._callback' is called,
74+
* we update our store version and tell React to re-render the component ([1] We don't really care when/how React does it).
75+
*
76+
* [1]
77+
* @see https://reactjs.org/docs/hooks-reference.html#usesyncexternalstore
78+
* @see https://github.com/reactwg/react-18/discussions/86
79+
*/
80+
function createEffectStore() {
6881
let updater!: Effect;
82+
let version = 0;
83+
6984
effect(function (this: Effect) {
7085
updater = this;
7186
});
72-
updater._callback = update;
73-
return updater;
87+
88+
return {
89+
updater,
90+
subscribe(onStoreChange: () => void) {
91+
updater._callback = function () {
92+
version = (version + 1) & ~(1 << 31);
93+
onStoreChange();
94+
};
95+
return function _unsubscribe() {};
96+
},
97+
getSnapshot() {
98+
return version;
99+
},
100+
};
74101
}
75102

76103
/**
@@ -97,30 +124,31 @@ Object.defineProperties(Signal.prototype, {
97124

98125
// Track the current dispatcher (roughly equiv to current component impl)
99126
let lock = false;
100-
const UPDATE = () => ({});
101127
let currentDispatcher: ReactDispatcher;
128+
102129
Object.defineProperty(internals.ReactCurrentDispatcher, "current", {
103130
get() {
104131
return currentDispatcher;
105132
},
106-
set(api) {
133+
set(api: ReactDispatcher) {
107134
currentDispatcher = api;
108135
if (lock) return;
109136
if (api && !isInvalidHookAccessor(api)) {
110-
// prevent re-injecting useReducer when the Dispatcher
111-
// context changes to run the reducer callback:
137+
// prevent re-injecting useMemo & useSyncExternalStore when the Dispatcher
138+
// context changes.
112139
lock = true;
113-
const rerender = api.useReducer(UPDATE, {})[1];
140+
141+
const store = api.useMemo(createEffectStore, Empty);
142+
143+
api.useSyncExternalStore(
144+
store.subscribe,
145+
store.getSnapshot,
146+
store.getSnapshot
147+
);
148+
114149
lock = false;
115150

116-
let updater = updaterForComponent.get(rerender);
117-
if (!updater) {
118-
updater = createUpdater(rerender);
119-
updaterForComponent.set(rerender, updater);
120-
} else {
121-
updater._callback = rerender;
122-
}
123-
setCurrentUpdater(updater);
151+
setCurrentUpdater(store.updater);
124152
} else {
125153
setCurrentUpdater();
126154
}
@@ -141,13 +169,13 @@ function isInvalidHookAccessor(api: ReactDispatcher) {
141169
}
142170

143171
export function useSignal<T>(value: T) {
144-
return useMemo(() => signal<T>(value), []);
172+
return useMemo(() => signal<T>(value), Empty);
145173
}
146174

147175
export function useComputed<T>(compute: () => T) {
148176
const $compute = useRef(compute);
149177
$compute.current = compute;
150-
return useMemo(() => computed<T>(() => $compute.current()), []);
178+
return useMemo(() => computed<T>(() => $compute.current()), Empty);
151179
}
152180

153181
export function useSignalEffect(cb: () => void | (() => void)) {
@@ -158,5 +186,5 @@ export function useSignalEffect(cb: () => void | (() => void)) {
158186
return effect(() => {
159187
return callback.current();
160188
});
161-
}, []);
189+
}, Empty);
162190
}

packages/react/src/internal.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Signal } from "@preact/signals-core";
2+
import type { useCallback, useMemo, useSyncExternalStore } from "react"
23

34
export interface Effect {
45
_sources: object | undefined;
@@ -8,7 +9,9 @@ export interface Effect {
89
}
910

1011
export interface ReactDispatcher {
11-
useCallback(): unknown;
12+
useCallback: typeof useCallback;
13+
useMemo: typeof useMemo;
14+
useSyncExternalStore: typeof useSyncExternalStore;
1215
}
1316

1417
export type Updater = Signal<unknown>;

0 commit comments

Comments
 (0)