-
-
Notifications
You must be signed in to change notification settings - Fork 366
/
store.tsx
186 lines (164 loc) · 5.75 KB
/
store.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
import { hasOwnProperty, identity } from "@ariakit/core/utils/misc";
import { batch, init, subscribe, sync } from "@ariakit/core/utils/store";
import type {
Store as CoreStore,
State,
StoreState,
} from "@ariakit/core/utils/store";
import type {
AnyFunction,
AnyObject,
PickByValue,
SetState,
} from "@ariakit/core/utils/types";
import * as React from "react";
// import { useSyncExternalStore } from "use-sync-external-store/shim/index.js";
// This doesn't work in ESM, because use-sync-external-store only exposes CJS.
// The following is a workaround until ESM is supported.
import useSyncExternalStoreExports from "use-sync-external-store/shim/index.js";
const { useSyncExternalStore } = useSyncExternalStoreExports;
import { useEvent, useLiveRef, useSafeLayoutEffect } from "./hooks.ts";
export interface UseState<S> {
/**
* Re-renders the component when state changes and returns the current state.
* @example
* const state = store.useState();
*/
(): S;
/**
* Re-renders the component when the state changes and returns the current
* state given the passed key. Changes on other keys will not trigger a
* re-render.
* @param key The state key.
* @example
* const foo = store.useState("foo");
*/
<K extends keyof S>(key: K): S[K];
/**
* Re-renders the component when the state changes given the return value of
* the selector function. The selector should return a stable value that will
* be compared to the previous value. Returns the value returned by the
* selector function.
* @param selector The selector function.
* @example
* const foo = store.useState((state) => state.foo);
*/
<V>(selector: (state: S) => V): V;
}
type StateStore<T = CoreStore> = T | null | undefined;
type StateKey<T = CoreStore> = keyof StoreState<T>;
const noopSubscribe = () => () => {};
export function useStoreState<T extends CoreStore>(store: T): StoreState<T>;
export function useStoreState<T extends CoreStore>(
store: T | null | undefined,
): StoreState<T> | undefined;
export function useStoreState<T extends CoreStore, K extends StateKey<T>>(
store: T,
key: K,
): StoreState<T>[K];
export function useStoreState<T extends CoreStore, K extends StateKey<T>>(
store: T | null | undefined,
key: K,
): StoreState<T>[K] | undefined;
export function useStoreState<T extends CoreStore, V>(
store: T,
selector: (state: StoreState<T>) => V,
): V;
export function useStoreState<T extends CoreStore, V>(
store: T | null | undefined,
selector: (state?: StoreState<T>) => V,
): V;
export function useStoreState(
store: StateStore,
keyOrSelector: StateKey | ((state?: AnyObject) => any) = identity,
) {
const storeSubscribe = React.useCallback(
(callback: () => void) => {
if (!store) return noopSubscribe();
return subscribe(store, null, callback);
},
[store],
);
const getSnapshot = () => {
const key = typeof keyOrSelector === "string" ? keyOrSelector : null;
const selector = typeof keyOrSelector === "function" ? keyOrSelector : null;
const state = store?.getState();
if (selector) return selector(state);
if (!state) return;
if (!key) return;
if (!hasOwnProperty(state, key)) return;
return state[key];
};
return useSyncExternalStore(storeSubscribe, getSnapshot, getSnapshot);
}
/**
* Synchronizes the store with the props, including parent store props.
* @param store The store to synchronize.
* @param props The props to synchronize with.
* @param key The key of the value prop.
* @param setKey The key of the setValue prop.
*/
export function useStoreProps<
S extends State,
P extends Partial<S>,
K extends keyof S,
SK extends keyof PickByValue<P, SetState<P[K]>>,
>(store: CoreStore<S>, props: P, key: K, setKey?: SK) {
const value = hasOwnProperty(props, key) ? props[key] : undefined;
const setValue = setKey ? props[setKey] : undefined;
const propsRef = useLiveRef({ value, setValue });
// Calls setValue when the state value changes.
useSafeLayoutEffect(() => {
return sync(store, [key], (state, prev) => {
const { value, setValue } = propsRef.current;
if (!setValue) return;
if (state[key] === prev[key]) return;
if (state[key] === value) return;
setValue(state[key]);
});
}, [store, key]);
// If the value prop is provided, we'll always reset the store state to it.
useSafeLayoutEffect(() => {
if (value === undefined) return;
// @ts-expect-error This is probably a bug in TypeScript. If we duplicate
// the line above, it works.
store.setState(key, value);
return batch(store, [key], () => {
if (value === undefined) return;
store.setState(key, value);
});
});
}
/**
* Creates a React store from a core store object and returns a tuple with the
* store and a function to update the store.
* @param createStore A function that receives the props and returns a core
* store object.
* @param props The props to pass to the createStore function.
*/
export function useStore<T extends CoreStore, P>(
createStore: (props: P) => T,
props: P,
) {
const [store, setStore] = React.useState(() => createStore(props));
useSafeLayoutEffect(() => init(store), [store]);
const useState: UseState<StoreState<T>> = React.useCallback<AnyFunction>(
(keyOrSelector) => useStoreState(store, keyOrSelector),
[store],
);
const memoizedStore = React.useMemo(
() => ({ ...store, useState }),
[store, useState],
);
const updateStore = useEvent(() => {
setStore((store) => createStore({ ...props, ...store.getState() }));
});
return [memoizedStore, updateStore] as const;
}
export type Store<T extends CoreStore = CoreStore> = T & {
/**
* Re-renders the component when the state changes and returns the current
* state.
*/
useState: UseState<StoreState<T>>;
};