Skip to content

Commit a15126f

Browse files
authored
fix: use useSyncExternalStore in the useStateStore hook (#3084)
1 parent ce8c64b commit a15126f

File tree

1 file changed

+42
-13
lines changed

1 file changed

+42
-13
lines changed

package/src/hooks/useStateStore.ts

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
import { useEffect, useState } from 'react';
1+
import { useCallback, useMemo } from 'react';
22

33
import type { StateStore } from 'stream-chat';
4+
import { useSyncExternalStore } from 'use-sync-external-store/shim';
5+
6+
const noop = () => {};
47

58
export function useStateStore<
69
T extends Record<string, unknown>,
@@ -14,22 +17,48 @@ export function useStateStore<
1417
T extends Record<string, unknown>,
1518
O extends Readonly<Record<string, unknown> | Readonly<unknown[]>>,
1619
>(store: StateStore<T> | undefined, selector: (v: T) => O) {
17-
const [state, setState] = useState<O | undefined>(() => {
18-
if (!store) {
19-
return undefined;
20-
}
21-
return selector(store.getLatestValue());
22-
});
20+
const wrappedSubscription = useCallback(
21+
(onStoreChange: () => void) => {
22+
const unsubscribe = store?.subscribeWithSelector(selector, onStoreChange);
23+
return unsubscribe ?? noop;
24+
},
25+
[store, selector],
26+
);
27+
28+
const wrappedSnapshot = useMemo(() => {
29+
let cachedTuple: [T, O];
30+
31+
return () => {
32+
const currentValue = store?.getLatestValue();
33+
34+
if (!currentValue) return undefined;
2335

24-
useEffect(() => {
25-
if (!store) {
26-
return;
27-
}
36+
// store value hasn't changed, no need to compare individual values
37+
if (cachedTuple && cachedTuple[0] === currentValue) {
38+
return cachedTuple[1];
39+
}
2840

29-
const unsubscribe = store.subscribeWithSelector(selector, setState);
41+
const newlySelected = selector(currentValue);
3042

31-
return unsubscribe;
43+
// store value changed but selected values wouldn't have to, double-check selected
44+
if (cachedTuple) {
45+
let selectededAreEqualToCached = true;
46+
47+
for (const key in cachedTuple[1]) {
48+
if (cachedTuple[1][key] === newlySelected[key]) continue;
49+
selectededAreEqualToCached = false;
50+
break;
51+
}
52+
53+
if (selectededAreEqualToCached) return cachedTuple[1];
54+
}
55+
56+
cachedTuple = [currentValue, newlySelected];
57+
return cachedTuple[1];
58+
};
3259
}, [store, selector]);
3360

61+
const state = useSyncExternalStore(wrappedSubscription, wrappedSnapshot);
62+
3463
return state;
3564
}

0 commit comments

Comments
 (0)