floppy-disk 3.0.1 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -22
- package/esm/react/create-query.d.mts +27 -1
- package/esm/react/create-store.d.mts +6 -1
- package/esm/react/create-stores.d.mts +8 -1
- package/esm/react/use-store.d.mts +8 -2
- package/esm/react.mjs +44 -11
- package/esm/vanilla/store.d.mts +6 -0
- package/esm/vanilla.mjs +2 -0
- package/package.json +1 -1
- package/react/create-query.d.ts +27 -1
- package/react/create-store.d.ts +6 -1
- package/react/create-stores.d.ts +8 -1
- package/react/use-store.d.ts +8 -2
- package/react.js +44 -11
- package/vanilla/store.d.ts +6 -0
- package/vanilla.js +2 -0
package/README.md
CHANGED
|
@@ -365,34 +365,39 @@ Revalidating dozens of previously viewed pages rarely provides value to the user
|
|
|
365
365
|
|
|
366
366
|
## SSR Guidance
|
|
367
367
|
|
|
368
|
-
|
|
368
|
+
Examples for using stores and queries in SSR with isolated data (no shared state between users).
|
|
369
369
|
|
|
370
|
-
|
|
370
|
+
### Initialize Store State from Server
|
|
371
371
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
This is the same philosophy as TanStack Query. 💡
|
|
375
|
-
|
|
376
|
-
In many cases, developers mix SSR/ISR with client-side state because they want:
|
|
377
|
-
|
|
378
|
-
1. Data to be rendered into HTML on the server
|
|
379
|
-
2. The ability to **revalidate it on the client**
|
|
380
|
-
|
|
381
|
-
A common (but inefficient) approach is:
|
|
372
|
+
```tsx
|
|
373
|
+
const useCountStore = createStore({ count: 0 });
|
|
382
374
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
375
|
+
function Page({ initialCount }) {
|
|
376
|
+
const { count } = useCountStore({
|
|
377
|
+
initialState: { count: initialCount }, // e.g. 3
|
|
378
|
+
});
|
|
386
379
|
|
|
387
|
-
|
|
380
|
+
return <>count is {count}</>; // Output: count is 3
|
|
381
|
+
}
|
|
382
|
+
```
|
|
388
383
|
|
|
389
|
-
|
|
384
|
+
### Initialize Query Data from Server
|
|
390
385
|
|
|
391
|
-
|
|
386
|
+
```tsx
|
|
387
|
+
async function MyServerComponent() {
|
|
388
|
+
const data = await getData(); // e.g. { count: 3 }
|
|
389
|
+
return <MyClientComponent initialData={data} />;
|
|
390
|
+
}
|
|
392
391
|
|
|
393
|
-
|
|
392
|
+
const myQuery = createQuery(getData);
|
|
393
|
+
const useMyQuery = myQuery();
|
|
394
394
|
|
|
395
|
-
|
|
395
|
+
function MyClientComponent({ initialData }) {
|
|
396
|
+
const { data } = useMyQuery({
|
|
397
|
+
initialData: initialData,
|
|
398
|
+
// initialDataIsStale: true <-- Optional, default to false (no immediate revalidation)
|
|
399
|
+
});
|
|
396
400
|
|
|
397
|
-
|
|
398
|
-
|
|
401
|
+
return <>count is {data.count}</>; // Output: count is 3
|
|
402
|
+
}
|
|
403
|
+
```
|
|
@@ -182,7 +182,33 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
|
|
|
182
182
|
* // While loading userId=2, still show userId=1 data
|
|
183
183
|
* useQuery({ id: userId }, { keepPreviousData: true });
|
|
184
184
|
*/ keepPreviousData?: boolean;
|
|
185
|
-
}
|
|
185
|
+
} & ({
|
|
186
|
+
/**
|
|
187
|
+
* Initial data used on first render (and will also update the store state right after that)
|
|
188
|
+
*
|
|
189
|
+
* If provided, `initialData` will be applied **once per query-store instance**
|
|
190
|
+
*/
|
|
191
|
+
initialData: TData;
|
|
192
|
+
/**
|
|
193
|
+
* Whether the provided `initialData` should be treated as stale.
|
|
194
|
+
*
|
|
195
|
+
* @remarks
|
|
196
|
+
* - If `true`, a revalidation (refetch) will be triggered immediately.
|
|
197
|
+
* - If `false` (default), `initialData` is treated as fresh and will not be revalidated.
|
|
198
|
+
*
|
|
199
|
+
* @default false
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* useQuery({
|
|
203
|
+
* initialData: dataFromSSR,
|
|
204
|
+
* initialDataIsStale: true, // force refetch
|
|
205
|
+
* });
|
|
206
|
+
*/
|
|
207
|
+
initialDataIsStale?: boolean;
|
|
208
|
+
} | {
|
|
209
|
+
initialData?: never;
|
|
210
|
+
initialDataIsStale?: never;
|
|
211
|
+
})) => QueryState<TData, TError>) & {
|
|
186
212
|
metadata: {
|
|
187
213
|
isInvalidated?: boolean;
|
|
188
214
|
promise?: Promise<QueryState<TData, TError>> | undefined;
|
|
@@ -25,4 +25,9 @@ import { type InitStoreOptions } from 'floppy-disk/vanilla';
|
|
|
25
25
|
*
|
|
26
26
|
* useMyStore.setState({ foo: 2 }); // only components using foo will re-render
|
|
27
27
|
*/
|
|
28
|
-
export declare const createStore: <TState extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => ((
|
|
28
|
+
export declare const createStore: <TState extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => ((options?: {
|
|
29
|
+
/**
|
|
30
|
+
* Initial state used on first render (and will also update the store state right after that)
|
|
31
|
+
*/
|
|
32
|
+
initialState?: Partial<TState>;
|
|
33
|
+
}) => TState) & import("../vanilla.d.mts").StoreApi<TState>;
|
|
@@ -30,7 +30,14 @@ import { type InitStoreOptions } from 'floppy-disk/vanilla';
|
|
|
30
30
|
* return <div>{state.name}</div>;
|
|
31
31
|
* }
|
|
32
32
|
*/
|
|
33
|
-
export declare const createStores: <TState extends Record<string, any>, TKey extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => (key?: TKey) => ((
|
|
33
|
+
export declare const createStores: <TState extends Record<string, any>, TKey extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => (key?: TKey) => ((options?: {
|
|
34
|
+
/**
|
|
35
|
+
* Initial state used on first render (and will also update the store state right after that)
|
|
36
|
+
*
|
|
37
|
+
* If provided, `initialState` will be applied **once per store instance**
|
|
38
|
+
*/
|
|
39
|
+
initialState?: Partial<TState>;
|
|
40
|
+
}) => TState) & {
|
|
34
41
|
delete: () => boolean;
|
|
35
42
|
setState: (value: import("../vanilla.d.mts").SetState<TState>) => void;
|
|
36
43
|
getState: () => TState;
|
|
@@ -4,10 +4,14 @@ export declare const getValueByPath: (obj: any, path: Path) => any;
|
|
|
4
4
|
export declare const isPrefixPath: (candidatePrefix: Path, targetPath: Path) => boolean;
|
|
5
5
|
export declare const compressPaths: (paths: Path[]) => Path[];
|
|
6
6
|
export declare const useStoreStateProxy: <TState extends Record<string, any>>(storeState: TState) => readonly [TState, import("react").RefObject<Path[]>];
|
|
7
|
+
export declare const NO_INITIAL_VALUE: {};
|
|
8
|
+
export declare const useStoreStateWithInitializer: <TState extends Record<string, any>>(store: StoreApi<TState>, initialState?: Partial<TState>) => readonly [TState, import("react").RefObject<WeakMap<StoreApi<TState>, number>>];
|
|
7
9
|
/**
|
|
8
10
|
* React hook for subscribing to a store using automatic dependency tracking.
|
|
9
11
|
*
|
|
10
12
|
* @param store - The store instance to subscribe to
|
|
13
|
+
* @param options - Optional configuration
|
|
14
|
+
* @param options.initialState - Initial state used on first render (and will also update the store state right after that)
|
|
11
15
|
*
|
|
12
16
|
* @returns A proxied version of the store state
|
|
13
17
|
*
|
|
@@ -17,12 +21,14 @@ export declare const useStoreStateProxy: <TState extends Record<string, any>>(st
|
|
|
17
21
|
* - State must be treated as **immutable**:
|
|
18
22
|
* - Updates must replace objects rather than mutate them
|
|
19
23
|
* - Otherwise, changes may not be detected
|
|
20
|
-
* -
|
|
24
|
+
* - If provided, `initialState` will be applied **once per store instance**
|
|
21
25
|
*
|
|
22
26
|
* @example
|
|
23
27
|
* const state = useStoreState(store);
|
|
24
28
|
* return <div>{state.user.name}</div>;
|
|
25
29
|
* // Component will only re-render if `user.name` changes
|
|
26
30
|
*/
|
|
27
|
-
export declare const useStoreState: <TState extends Record<string, any>>(
|
|
31
|
+
export declare const useStoreState: <TState extends Record<string, any>>(store: StoreApi<TState>, options?: {
|
|
32
|
+
initialState?: Partial<TState>;
|
|
33
|
+
}) => TState;
|
|
28
34
|
export {};
|
package/esm/react.mjs
CHANGED
|
@@ -49,11 +49,24 @@ const useStoreStateProxy = (storeState) => {
|
|
|
49
49
|
}, [storeState]);
|
|
50
50
|
return [trackedState, usedPathsRef];
|
|
51
51
|
};
|
|
52
|
-
const
|
|
53
|
-
|
|
52
|
+
const NO_INITIAL_VALUE = {};
|
|
53
|
+
const useStoreStateWithInitializer = (store, initialState = NO_INITIAL_VALUE) => {
|
|
54
|
+
const initiatedAt = useRef(new WeakMap([[store, 0]]));
|
|
55
|
+
useIsomorphicLayoutEffect(() => {
|
|
56
|
+
if (initialState === NO_INITIAL_VALUE || initiatedAt.current.get(store)) return;
|
|
57
|
+
store.setState(initialState);
|
|
58
|
+
initiatedAt.current.set(store, Date.now());
|
|
59
|
+
}, [store, initialState]);
|
|
60
|
+
const storeState = store.getState();
|
|
61
|
+
const finalState = initialState === NO_INITIAL_VALUE || initiatedAt.current.get(store) ? storeState : { ...storeState, ...initialState };
|
|
62
|
+
return [finalState, initiatedAt];
|
|
63
|
+
};
|
|
64
|
+
const useStoreState = (store, options = {}) => {
|
|
65
|
+
const [state] = useStoreStateWithInitializer(store, options.initialState);
|
|
66
|
+
const [trackedState, usedPathsRef] = useStoreStateProxy(state);
|
|
54
67
|
const [, reRender] = useState({});
|
|
55
68
|
useIsomorphicLayoutEffect(() => {
|
|
56
|
-
return subscribe((nextState, prevState, changedKeys) => {
|
|
69
|
+
return store.subscribe((nextState, prevState, changedKeys) => {
|
|
57
70
|
const paths = compressPaths(usedPathsRef.current);
|
|
58
71
|
for (const path of paths) {
|
|
59
72
|
const rootKey = path[0];
|
|
@@ -63,13 +76,13 @@ const useStoreState = (storeState, subscribe) => {
|
|
|
63
76
|
if (!Object.is(prevVal, nextVal)) return reRender({});
|
|
64
77
|
}
|
|
65
78
|
});
|
|
66
|
-
}, [
|
|
79
|
+
}, [store]);
|
|
67
80
|
return trackedState;
|
|
68
81
|
};
|
|
69
82
|
|
|
70
83
|
const createStore = (initialState, options) => {
|
|
71
84
|
const store = initStore(initialState, options);
|
|
72
|
-
const useStore = () => useStoreState(store
|
|
85
|
+
const useStore = (options2) => useStoreState(store, options2);
|
|
73
86
|
return Object.assign(useStore, store);
|
|
74
87
|
};
|
|
75
88
|
|
|
@@ -84,7 +97,7 @@ const createStores = (initialState, options) => {
|
|
|
84
97
|
store = initStore(initialState, options);
|
|
85
98
|
stores.set(keyHash, store);
|
|
86
99
|
}
|
|
87
|
-
const useStore = () => useStoreState(store
|
|
100
|
+
const useStore = (options2) => useStoreState(store, options2);
|
|
88
101
|
return Object.assign(useStore, {
|
|
89
102
|
...store,
|
|
90
103
|
delete: () => {
|
|
@@ -367,8 +380,20 @@ const createQuery = (queryFn, options = {}) => {
|
|
|
367
380
|
internals.set(store, configureInternals(store, variable, variableHash));
|
|
368
381
|
}
|
|
369
382
|
const useStore = (options2 = {}) => {
|
|
370
|
-
const {
|
|
371
|
-
|
|
383
|
+
const {
|
|
384
|
+
initialData = NO_INITIAL_VALUE,
|
|
385
|
+
initialDataIsStale = false,
|
|
386
|
+
revalidateOnMount = true,
|
|
387
|
+
keepPreviousData
|
|
388
|
+
} = options2;
|
|
389
|
+
const [storeState, initialDataInitiatedAt] = useStoreStateWithInitializer(
|
|
390
|
+
store,
|
|
391
|
+
initialData === NO_INITIAL_VALUE ? void 0 : {
|
|
392
|
+
state: "SUCCESS",
|
|
393
|
+
isSuccess: true,
|
|
394
|
+
data: initialData
|
|
395
|
+
}
|
|
396
|
+
);
|
|
372
397
|
const prevState = useRef({});
|
|
373
398
|
let storeStateToBeUsed = storeState;
|
|
374
399
|
if (storeState.state !== "INITIAL") {
|
|
@@ -404,8 +429,16 @@ const createQuery = (queryFn, options = {}) => {
|
|
|
404
429
|
});
|
|
405
430
|
}, [store]);
|
|
406
431
|
useIsomorphicLayoutEffect(() => {
|
|
407
|
-
if (revalidateOnMount !== false)
|
|
408
|
-
|
|
432
|
+
if (revalidateOnMount !== false) {
|
|
433
|
+
if (!initialDataIsStale) {
|
|
434
|
+
const dataInitiatedAt = initialDataInitiatedAt.current.get(store);
|
|
435
|
+
if (dataInitiatedAt && dataInitiatedAt + staleTime > Date.now()) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
revalidate(store, variable, false);
|
|
440
|
+
}
|
|
441
|
+
}, [store, revalidateOnMount, initialDataIsStale]);
|
|
409
442
|
if (keepPreviousData) {
|
|
410
443
|
!!trackedState.error;
|
|
411
444
|
}
|
|
@@ -483,7 +516,7 @@ const createMutation = (mutationFn, options = {}) => {
|
|
|
483
516
|
let ongoingPromise;
|
|
484
517
|
const resolveFns = /* @__PURE__ */ new Set([]);
|
|
485
518
|
const store = initStore(initialState, options);
|
|
486
|
-
const useStore = () => useStoreState(store
|
|
519
|
+
const useStore = () => useStoreState(store);
|
|
487
520
|
const execute = (variable) => {
|
|
488
521
|
let currentResolveFn;
|
|
489
522
|
const stateBeforeExecute = store.getState();
|
package/esm/vanilla/store.d.mts
CHANGED
|
@@ -51,6 +51,12 @@ export type InitStoreOptions<TState extends Record<string, any>> = {
|
|
|
51
51
|
onSubscribe?: (state: TState, store: StoreApi<TState>) => void;
|
|
52
52
|
onUnsubscribe?: (state: TState, store: StoreApi<TState>) => void;
|
|
53
53
|
onLastUnsubscribe?: (state: TState, store: StoreApi<TState>) => void;
|
|
54
|
+
/**
|
|
55
|
+
* Called whenever the state changes, without counting as a subscriber.
|
|
56
|
+
* Acts like a "spy" on state updates.
|
|
57
|
+
* Useful for devtools, logging, or debugging state changes.
|
|
58
|
+
*/
|
|
59
|
+
onStateChange?: (state: TState, prevState: TState, changedKeys: Array<keyof TState>) => void;
|
|
54
60
|
/**
|
|
55
61
|
* By default, calling `setState` on the server is disallowed to prevent shared state across requests.
|
|
56
62
|
* Set this to `true` only if you explicitly intend to mutate state during server execution.
|
package/esm/vanilla.mjs
CHANGED
|
@@ -35,6 +35,7 @@ const initStore = (initialState, options = {}) => {
|
|
|
35
35
|
onSubscribe = noop,
|
|
36
36
|
onUnsubscribe = noop,
|
|
37
37
|
onLastUnsubscribe = noop,
|
|
38
|
+
onStateChange = noop,
|
|
38
39
|
allowSetStateServerSide = false
|
|
39
40
|
} = options;
|
|
40
41
|
const subscribers = /* @__PURE__ */ new Set();
|
|
@@ -68,6 +69,7 @@ const initStore = (initialState, options = {}) => {
|
|
|
68
69
|
}
|
|
69
70
|
if (changedKeys.length === 0) return;
|
|
70
71
|
state = { ...prevState, ...newValue };
|
|
72
|
+
onStateChange(state, prevState, changedKeys);
|
|
71
73
|
[...subscribers].forEach((subscriber) => subscriber(state, prevState, changedKeys));
|
|
72
74
|
};
|
|
73
75
|
const storeApi = {
|
package/package.json
CHANGED
package/react/create-query.d.ts
CHANGED
|
@@ -182,7 +182,33 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
|
|
|
182
182
|
* // While loading userId=2, still show userId=1 data
|
|
183
183
|
* useQuery({ id: userId }, { keepPreviousData: true });
|
|
184
184
|
*/ keepPreviousData?: boolean;
|
|
185
|
-
}
|
|
185
|
+
} & ({
|
|
186
|
+
/**
|
|
187
|
+
* Initial data used on first render (and will also update the store state right after that)
|
|
188
|
+
*
|
|
189
|
+
* If provided, `initialData` will be applied **once per query-store instance**
|
|
190
|
+
*/
|
|
191
|
+
initialData: TData;
|
|
192
|
+
/**
|
|
193
|
+
* Whether the provided `initialData` should be treated as stale.
|
|
194
|
+
*
|
|
195
|
+
* @remarks
|
|
196
|
+
* - If `true`, a revalidation (refetch) will be triggered immediately.
|
|
197
|
+
* - If `false` (default), `initialData` is treated as fresh and will not be revalidated.
|
|
198
|
+
*
|
|
199
|
+
* @default false
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* useQuery({
|
|
203
|
+
* initialData: dataFromSSR,
|
|
204
|
+
* initialDataIsStale: true, // force refetch
|
|
205
|
+
* });
|
|
206
|
+
*/
|
|
207
|
+
initialDataIsStale?: boolean;
|
|
208
|
+
} | {
|
|
209
|
+
initialData?: never;
|
|
210
|
+
initialDataIsStale?: never;
|
|
211
|
+
})) => QueryState<TData, TError>) & {
|
|
186
212
|
metadata: {
|
|
187
213
|
isInvalidated?: boolean;
|
|
188
214
|
promise?: Promise<QueryState<TData, TError>> | undefined;
|
package/react/create-store.d.ts
CHANGED
|
@@ -25,4 +25,9 @@ import { type InitStoreOptions } from 'floppy-disk/vanilla';
|
|
|
25
25
|
*
|
|
26
26
|
* useMyStore.setState({ foo: 2 }); // only components using foo will re-render
|
|
27
27
|
*/
|
|
28
|
-
export declare const createStore: <TState extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => ((
|
|
28
|
+
export declare const createStore: <TState extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => ((options?: {
|
|
29
|
+
/**
|
|
30
|
+
* Initial state used on first render (and will also update the store state right after that)
|
|
31
|
+
*/
|
|
32
|
+
initialState?: Partial<TState>;
|
|
33
|
+
}) => TState) & import("../vanilla.ts").StoreApi<TState>;
|
package/react/create-stores.d.ts
CHANGED
|
@@ -30,7 +30,14 @@ import { type InitStoreOptions } from 'floppy-disk/vanilla';
|
|
|
30
30
|
* return <div>{state.name}</div>;
|
|
31
31
|
* }
|
|
32
32
|
*/
|
|
33
|
-
export declare const createStores: <TState extends Record<string, any>, TKey extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => (key?: TKey) => ((
|
|
33
|
+
export declare const createStores: <TState extends Record<string, any>, TKey extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => (key?: TKey) => ((options?: {
|
|
34
|
+
/**
|
|
35
|
+
* Initial state used on first render (and will also update the store state right after that)
|
|
36
|
+
*
|
|
37
|
+
* If provided, `initialState` will be applied **once per store instance**
|
|
38
|
+
*/
|
|
39
|
+
initialState?: Partial<TState>;
|
|
40
|
+
}) => TState) & {
|
|
34
41
|
delete: () => boolean;
|
|
35
42
|
setState: (value: import("../vanilla.ts").SetState<TState>) => void;
|
|
36
43
|
getState: () => TState;
|
package/react/use-store.d.ts
CHANGED
|
@@ -4,10 +4,14 @@ export declare const getValueByPath: (obj: any, path: Path) => any;
|
|
|
4
4
|
export declare const isPrefixPath: (candidatePrefix: Path, targetPath: Path) => boolean;
|
|
5
5
|
export declare const compressPaths: (paths: Path[]) => Path[];
|
|
6
6
|
export declare const useStoreStateProxy: <TState extends Record<string, any>>(storeState: TState) => readonly [TState, import("react").RefObject<Path[]>];
|
|
7
|
+
export declare const NO_INITIAL_VALUE: {};
|
|
8
|
+
export declare const useStoreStateWithInitializer: <TState extends Record<string, any>>(store: StoreApi<TState>, initialState?: Partial<TState>) => readonly [TState, import("react").RefObject<WeakMap<StoreApi<TState>, number>>];
|
|
7
9
|
/**
|
|
8
10
|
* React hook for subscribing to a store using automatic dependency tracking.
|
|
9
11
|
*
|
|
10
12
|
* @param store - The store instance to subscribe to
|
|
13
|
+
* @param options - Optional configuration
|
|
14
|
+
* @param options.initialState - Initial state used on first render (and will also update the store state right after that)
|
|
11
15
|
*
|
|
12
16
|
* @returns A proxied version of the store state
|
|
13
17
|
*
|
|
@@ -17,12 +21,14 @@ export declare const useStoreStateProxy: <TState extends Record<string, any>>(st
|
|
|
17
21
|
* - State must be treated as **immutable**:
|
|
18
22
|
* - Updates must replace objects rather than mutate them
|
|
19
23
|
* - Otherwise, changes may not be detected
|
|
20
|
-
* -
|
|
24
|
+
* - If provided, `initialState` will be applied **once per store instance**
|
|
21
25
|
*
|
|
22
26
|
* @example
|
|
23
27
|
* const state = useStoreState(store);
|
|
24
28
|
* return <div>{state.user.name}</div>;
|
|
25
29
|
* // Component will only re-render if `user.name` changes
|
|
26
30
|
*/
|
|
27
|
-
export declare const useStoreState: <TState extends Record<string, any>>(
|
|
31
|
+
export declare const useStoreState: <TState extends Record<string, any>>(store: StoreApi<TState>, options?: {
|
|
32
|
+
initialState?: Partial<TState>;
|
|
33
|
+
}) => TState;
|
|
28
34
|
export {};
|
package/react.js
CHANGED
|
@@ -51,11 +51,24 @@ const useStoreStateProxy = (storeState) => {
|
|
|
51
51
|
}, [storeState]);
|
|
52
52
|
return [trackedState, usedPathsRef];
|
|
53
53
|
};
|
|
54
|
-
const
|
|
55
|
-
|
|
54
|
+
const NO_INITIAL_VALUE = {};
|
|
55
|
+
const useStoreStateWithInitializer = (store, initialState = NO_INITIAL_VALUE) => {
|
|
56
|
+
const initiatedAt = react.useRef(new WeakMap([[store, 0]]));
|
|
57
|
+
useIsomorphicLayoutEffect(() => {
|
|
58
|
+
if (initialState === NO_INITIAL_VALUE || initiatedAt.current.get(store)) return;
|
|
59
|
+
store.setState(initialState);
|
|
60
|
+
initiatedAt.current.set(store, Date.now());
|
|
61
|
+
}, [store, initialState]);
|
|
62
|
+
const storeState = store.getState();
|
|
63
|
+
const finalState = initialState === NO_INITIAL_VALUE || initiatedAt.current.get(store) ? storeState : { ...storeState, ...initialState };
|
|
64
|
+
return [finalState, initiatedAt];
|
|
65
|
+
};
|
|
66
|
+
const useStoreState = (store, options = {}) => {
|
|
67
|
+
const [state] = useStoreStateWithInitializer(store, options.initialState);
|
|
68
|
+
const [trackedState, usedPathsRef] = useStoreStateProxy(state);
|
|
56
69
|
const [, reRender] = react.useState({});
|
|
57
70
|
useIsomorphicLayoutEffect(() => {
|
|
58
|
-
return subscribe((nextState, prevState, changedKeys) => {
|
|
71
|
+
return store.subscribe((nextState, prevState, changedKeys) => {
|
|
59
72
|
const paths = compressPaths(usedPathsRef.current);
|
|
60
73
|
for (const path of paths) {
|
|
61
74
|
const rootKey = path[0];
|
|
@@ -65,13 +78,13 @@ const useStoreState = (storeState, subscribe) => {
|
|
|
65
78
|
if (!Object.is(prevVal, nextVal)) return reRender({});
|
|
66
79
|
}
|
|
67
80
|
});
|
|
68
|
-
}, [
|
|
81
|
+
}, [store]);
|
|
69
82
|
return trackedState;
|
|
70
83
|
};
|
|
71
84
|
|
|
72
85
|
const createStore = (initialState, options) => {
|
|
73
86
|
const store = vanilla.initStore(initialState, options);
|
|
74
|
-
const useStore = () => useStoreState(store
|
|
87
|
+
const useStore = (options2) => useStoreState(store, options2);
|
|
75
88
|
return Object.assign(useStore, store);
|
|
76
89
|
};
|
|
77
90
|
|
|
@@ -86,7 +99,7 @@ const createStores = (initialState, options) => {
|
|
|
86
99
|
store = vanilla.initStore(initialState, options);
|
|
87
100
|
stores.set(keyHash, store);
|
|
88
101
|
}
|
|
89
|
-
const useStore = () => useStoreState(store
|
|
102
|
+
const useStore = (options2) => useStoreState(store, options2);
|
|
90
103
|
return Object.assign(useStore, {
|
|
91
104
|
...store,
|
|
92
105
|
delete: () => {
|
|
@@ -369,8 +382,20 @@ const createQuery = (queryFn, options = {}) => {
|
|
|
369
382
|
internals.set(store, configureInternals(store, variable, variableHash));
|
|
370
383
|
}
|
|
371
384
|
const useStore = (options2 = {}) => {
|
|
372
|
-
const {
|
|
373
|
-
|
|
385
|
+
const {
|
|
386
|
+
initialData = NO_INITIAL_VALUE,
|
|
387
|
+
initialDataIsStale = false,
|
|
388
|
+
revalidateOnMount = true,
|
|
389
|
+
keepPreviousData
|
|
390
|
+
} = options2;
|
|
391
|
+
const [storeState, initialDataInitiatedAt] = useStoreStateWithInitializer(
|
|
392
|
+
store,
|
|
393
|
+
initialData === NO_INITIAL_VALUE ? void 0 : {
|
|
394
|
+
state: "SUCCESS",
|
|
395
|
+
isSuccess: true,
|
|
396
|
+
data: initialData
|
|
397
|
+
}
|
|
398
|
+
);
|
|
374
399
|
const prevState = react.useRef({});
|
|
375
400
|
let storeStateToBeUsed = storeState;
|
|
376
401
|
if (storeState.state !== "INITIAL") {
|
|
@@ -406,8 +431,16 @@ const createQuery = (queryFn, options = {}) => {
|
|
|
406
431
|
});
|
|
407
432
|
}, [store]);
|
|
408
433
|
useIsomorphicLayoutEffect(() => {
|
|
409
|
-
if (revalidateOnMount !== false)
|
|
410
|
-
|
|
434
|
+
if (revalidateOnMount !== false) {
|
|
435
|
+
if (!initialDataIsStale) {
|
|
436
|
+
const dataInitiatedAt = initialDataInitiatedAt.current.get(store);
|
|
437
|
+
if (dataInitiatedAt && dataInitiatedAt + staleTime > Date.now()) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
revalidate(store, variable, false);
|
|
442
|
+
}
|
|
443
|
+
}, [store, revalidateOnMount, initialDataIsStale]);
|
|
411
444
|
if (keepPreviousData) {
|
|
412
445
|
!!trackedState.error;
|
|
413
446
|
}
|
|
@@ -485,7 +518,7 @@ const createMutation = (mutationFn, options = {}) => {
|
|
|
485
518
|
let ongoingPromise;
|
|
486
519
|
const resolveFns = /* @__PURE__ */ new Set([]);
|
|
487
520
|
const store = vanilla.initStore(initialState, options);
|
|
488
|
-
const useStore = () => useStoreState(store
|
|
521
|
+
const useStore = () => useStoreState(store);
|
|
489
522
|
const execute = (variable) => {
|
|
490
523
|
let currentResolveFn;
|
|
491
524
|
const stateBeforeExecute = store.getState();
|
package/vanilla/store.d.ts
CHANGED
|
@@ -51,6 +51,12 @@ export type InitStoreOptions<TState extends Record<string, any>> = {
|
|
|
51
51
|
onSubscribe?: (state: TState, store: StoreApi<TState>) => void;
|
|
52
52
|
onUnsubscribe?: (state: TState, store: StoreApi<TState>) => void;
|
|
53
53
|
onLastUnsubscribe?: (state: TState, store: StoreApi<TState>) => void;
|
|
54
|
+
/**
|
|
55
|
+
* Called whenever the state changes, without counting as a subscriber.
|
|
56
|
+
* Acts like a "spy" on state updates.
|
|
57
|
+
* Useful for devtools, logging, or debugging state changes.
|
|
58
|
+
*/
|
|
59
|
+
onStateChange?: (state: TState, prevState: TState, changedKeys: Array<keyof TState>) => void;
|
|
54
60
|
/**
|
|
55
61
|
* By default, calling `setState` on the server is disallowed to prevent shared state across requests.
|
|
56
62
|
* Set this to `true` only if you explicitly intend to mutate state during server execution.
|
package/vanilla.js
CHANGED
|
@@ -37,6 +37,7 @@ const initStore = (initialState, options = {}) => {
|
|
|
37
37
|
onSubscribe = noop,
|
|
38
38
|
onUnsubscribe = noop,
|
|
39
39
|
onLastUnsubscribe = noop,
|
|
40
|
+
onStateChange = noop,
|
|
40
41
|
allowSetStateServerSide = false
|
|
41
42
|
} = options;
|
|
42
43
|
const subscribers = /* @__PURE__ */ new Set();
|
|
@@ -70,6 +71,7 @@ const initStore = (initialState, options = {}) => {
|
|
|
70
71
|
}
|
|
71
72
|
if (changedKeys.length === 0) return;
|
|
72
73
|
state = { ...prevState, ...newValue };
|
|
74
|
+
onStateChange(state, prevState, changedKeys);
|
|
73
75
|
[...subscribers].forEach((subscriber) => subscriber(state, prevState, changedKeys));
|
|
74
76
|
};
|
|
75
77
|
const storeApi = {
|