floppy-disk 3.0.0-alpha.3 → 3.0.0-alpha.5
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/esm/react/create-mutation.d.mts +1 -1
- package/esm/react/create-query.d.mts +18 -14
- package/esm/react/create-store.d.mts +7 -4
- package/esm/react/create-stores.d.mts +11 -4
- package/esm/react/use-store.d.mts +18 -8
- package/esm/react.d.mts +1 -1
- package/esm/react.mjs +107 -34
- package/esm/vanilla/basic.d.mts +0 -8
- package/esm/vanilla/store.d.mts +17 -8
- package/esm/vanilla.d.mts +0 -1
- package/esm/vanilla.mjs +6 -40
- package/package.json +1 -1
- package/react/create-mutation.d.ts +1 -1
- package/react/create-query.d.ts +18 -14
- package/react/create-store.d.ts +7 -4
- package/react/create-stores.d.ts +11 -4
- package/react/use-store.d.ts +18 -8
- package/react.d.ts +1 -1
- package/react.js +104 -32
- package/vanilla/basic.d.ts +0 -8
- package/vanilla/store.d.ts +17 -8
- package/vanilla.d.ts +0 -1
- package/vanilla.js +5 -41
- package/esm/vanilla/shallow.d.mts +0 -6
- package/vanilla/shallow.d.ts +0 -6
|
@@ -89,7 +89,7 @@ export type MutationOptions<TData, TVariable> = InitStoreOptions<MutationState<T
|
|
|
89
89
|
* const { isPending } = useCreateUser();
|
|
90
90
|
* const result = await useCreateUser.execute({ name: 'John' });
|
|
91
91
|
*/
|
|
92
|
-
export declare const createMutation: <TData, TVariable = undefined>(mutationFn: (variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable>) => Promise<TData>, options?: MutationOptions<TData, TVariable>) => (
|
|
92
|
+
export declare const createMutation: <TData, TVariable = undefined>(mutationFn: (variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable>) => Promise<TData>, options?: MutationOptions<TData, TVariable>) => (() => MutationState<TData, TVariable>) & {
|
|
93
93
|
subscribe: (subscriber: import("../vanilla.d.mts").Subscriber<MutationState<TData, TVariable>>) => () => void;
|
|
94
94
|
getSubscribers: () => Set<import("../vanilla.d.mts").Subscriber<MutationState<TData, TVariable>>>;
|
|
95
95
|
getState: () => MutationState<TData, TVariable>;
|
|
@@ -67,9 +67,9 @@ export type QueryOptions<TData, TVariable extends Record<string, any>> = InitSto
|
|
|
67
67
|
/**
|
|
68
68
|
* Time (in milliseconds) that data is considered fresh.
|
|
69
69
|
*
|
|
70
|
-
* While fresh, revalidation will be skipped.
|
|
70
|
+
* While fresh, revalidation will be skipped unless explicitly invalidated.
|
|
71
71
|
*
|
|
72
|
-
* @default 2500 ms (2.5
|
|
72
|
+
* @default 2500 ms (2.5 seconds)
|
|
73
73
|
*/
|
|
74
74
|
staleTime?: number;
|
|
75
75
|
/**
|
|
@@ -131,16 +131,20 @@ export type QueryOptions<TData, TVariable extends Record<string, any>> = InitSto
|
|
|
131
131
|
* @returns A function to retrieve or create a query instance by variable
|
|
132
132
|
*
|
|
133
133
|
* @remarks
|
|
134
|
-
* - Queries are
|
|
134
|
+
* - Queries are **keyed by variable** (via deterministic hashing).
|
|
135
135
|
* - Each unique variable maps to its own store instance.
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* -
|
|
143
|
-
*
|
|
136
|
+
*
|
|
137
|
+
* Core features:
|
|
138
|
+
* - Caching via `staleTime`
|
|
139
|
+
* - Explicit invalidation (independent of freshness)
|
|
140
|
+
* - Retry logic via `shouldRetry`
|
|
141
|
+
* - Background revalidation (focus / reconnect)
|
|
142
|
+
* - Garbage collection via `gcTime`
|
|
143
|
+
*
|
|
144
|
+
* Execution behavior:
|
|
145
|
+
* - By default, executions **overwrite ongoing executions**
|
|
146
|
+
* - Set `overwriteOngoingExecution: false` to enable deduplication
|
|
147
|
+
* - Internal revalidation (focus/reconnect) uses deduplication by default
|
|
144
148
|
*
|
|
145
149
|
* @example
|
|
146
150
|
* const userQuery = createQuery<UserDetail, { id: string }>(async ({ id }) => {
|
|
@@ -153,7 +157,7 @@ export type QueryOptions<TData, TVariable extends Record<string, any>> = InitSto
|
|
|
153
157
|
* // ...
|
|
154
158
|
* }
|
|
155
159
|
*/
|
|
156
|
-
export declare const createQuery: <TData, TVariable extends Record<string, any> = never>(queryFn: (variable: TVariable, currentState: QueryState<TData>) => Promise<TData>, options?: QueryOptions<TData, TVariable>) => ((variable?: TVariable) => (
|
|
160
|
+
export declare const createQuery: <TData, TVariable extends Record<string, any> = never>(queryFn: (variable: TVariable, currentState: QueryState<TData>) => Promise<TData>, options?: QueryOptions<TData, TVariable>) => ((variable?: TVariable) => ((options?: {
|
|
157
161
|
/**
|
|
158
162
|
* Whether the query should execute automatically on mount.
|
|
159
163
|
*
|
|
@@ -176,7 +180,7 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
|
|
|
176
180
|
* // While loading userId=2, still show userId=1 data
|
|
177
181
|
* useQuery({ id: userId }, { keepPreviousData: true });
|
|
178
182
|
*/ keepPreviousData?: boolean;
|
|
179
|
-
}
|
|
183
|
+
}) => QueryState<TData>) & {
|
|
180
184
|
metadata: {
|
|
181
185
|
isInvalidated?: boolean;
|
|
182
186
|
promise?: Promise<QueryState<TData>> | undefined;
|
|
@@ -208,7 +212,7 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
|
|
|
208
212
|
* @returns A promise resolving to the latest query state
|
|
209
213
|
*
|
|
210
214
|
* @remarks
|
|
211
|
-
* - By default, each call starts a new execution even if one is already in progress.
|
|
215
|
+
* - By default, each call **starts a new execution** even if one is already in progress.
|
|
212
216
|
* - Set `overwriteOngoingExecution: false` to reuse an ongoing execution (deduplication).
|
|
213
217
|
* - Handles:
|
|
214
218
|
* - Pending state
|
|
@@ -12,14 +12,17 @@ import { type InitStoreOptions } from 'floppy-disk/vanilla';
|
|
|
12
12
|
* @remarks
|
|
13
13
|
* - Combines the vanilla store with React integration.
|
|
14
14
|
* - The returned function can be used directly as a hook.
|
|
15
|
+
* - The hook uses Proxy-based tracking to automatically detect which state fields are used.
|
|
16
|
+
* - Components will only re-render when the accessed values change.
|
|
15
17
|
*
|
|
16
18
|
* @example
|
|
17
|
-
* const
|
|
19
|
+
* const useMyStore = createStore({ foo: 1, bar: 2 });
|
|
18
20
|
*
|
|
19
21
|
* function Component() {
|
|
20
|
-
* const
|
|
22
|
+
* const state = useMyStore();
|
|
23
|
+
* return <div>{state.foo}</div>;
|
|
21
24
|
* }
|
|
22
25
|
*
|
|
23
|
-
*
|
|
26
|
+
* useMyStore.setState({ foo: 2 }); // only components using foo will re-render
|
|
24
27
|
*/
|
|
25
|
-
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>) => (() => TState) & import("../vanilla.d.mts").StoreApi<TState>;
|
|
@@ -12,18 +12,25 @@ import { type InitStoreOptions } from 'floppy-disk/vanilla';
|
|
|
12
12
|
* - Keys are deterministically hashed, ensuring stable identity.
|
|
13
13
|
* - Stores are lazily created and cached.
|
|
14
14
|
* - Each store has its own state, subscribers, and lifecycle.
|
|
15
|
+
* - Each returned store includes:
|
|
16
|
+
* - React hook (with Proxy-based tracking)
|
|
17
|
+
* - Store API methods
|
|
18
|
+
* - `delete()` for manual cleanup
|
|
15
19
|
* - Useful for scenarios like:
|
|
16
20
|
* - Query caches
|
|
17
21
|
* - Entity-based state
|
|
18
22
|
* - Dynamic instances
|
|
19
23
|
*
|
|
20
24
|
* @example
|
|
21
|
-
* const
|
|
25
|
+
* const userStore = createStores<{ name: string }, { id: number }>({ name: '' });
|
|
22
26
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
27
|
+
* function Component() {
|
|
28
|
+
* const useUserStore = userStore({ id: 1 });
|
|
29
|
+
* const state = useUserStore();
|
|
30
|
+
* return <div>{state.name}</div>;
|
|
31
|
+
* }
|
|
25
32
|
*/
|
|
26
|
-
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) => (() => TState) & {
|
|
27
34
|
delete: () => boolean;
|
|
28
35
|
setState: (value: import("../vanilla.d.mts").SetState<TState>) => void;
|
|
29
36
|
getState: () => TState;
|
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
import { type StoreApi } from 'floppy-disk/vanilla';
|
|
2
|
-
|
|
2
|
+
type Path = Array<string | number | symbol>;
|
|
3
|
+
export declare const getValueByPath: (obj: any, path: Path) => any;
|
|
4
|
+
export declare const isPrefixPath: (candidatePrefix: Path, targetPath: Path) => boolean;
|
|
5
|
+
export declare const compressPaths: (paths: Path[]) => Path[];
|
|
6
|
+
export declare const useStoreStateProxy: <TState extends Record<string, any>>(storeState: TState) => readonly [TState, import("react").RefObject<Path[]>];
|
|
3
7
|
/**
|
|
4
|
-
* React hook for subscribing to a store
|
|
8
|
+
* React hook for subscribing to a store using automatic dependency tracking.
|
|
5
9
|
*
|
|
6
10
|
* @param store - The store instance to subscribe to
|
|
7
|
-
* @param selector - Optional selector to derive a slice of state
|
|
8
11
|
*
|
|
9
|
-
* @returns
|
|
12
|
+
* @returns A proxied version of the store state
|
|
10
13
|
*
|
|
11
14
|
* @remarks
|
|
12
|
-
* -
|
|
13
|
-
* - The
|
|
15
|
+
* - This hook uses a **Proxy-based tracking mechanism** to detect which parts of the state are accessed during render.
|
|
16
|
+
* - The component will only re-render when the **accessed values actually change**.
|
|
17
|
+
* - State must be treated as **immutable**:
|
|
18
|
+
* - Updates must replace objects rather than mutate them
|
|
19
|
+
* - Otherwise, changes may not be detected
|
|
20
|
+
* - No selector or memoization is needed.
|
|
14
21
|
*
|
|
15
22
|
* @example
|
|
16
|
-
* const
|
|
23
|
+
* const state = useStoreState(store);
|
|
24
|
+
* return <div>{state.user.name}</div>;
|
|
25
|
+
* // Component will only re-render if `user.name` changes
|
|
17
26
|
*/
|
|
18
|
-
export declare const useStoreState: <TState extends Record<string, any
|
|
27
|
+
export declare const useStoreState: <TState extends Record<string, any>>(storeState: TState, subscribe: StoreApi<TState>["subscribe"]) => TState;
|
|
28
|
+
export {};
|
package/esm/react.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export * from './react/use-isomorphic-layout-effect.mjs';
|
|
2
|
-
export
|
|
2
|
+
export { useStoreState } from './react/use-store.mjs';
|
|
3
3
|
export * from './react/create-store.mjs';
|
|
4
4
|
export * from './react/create-stores.mjs';
|
|
5
5
|
export * from './react/create-query.mjs';
|
package/esm/react.mjs
CHANGED
|
@@ -1,30 +1,75 @@
|
|
|
1
|
-
import { useLayoutEffect, useEffect, useState, useRef } from 'react';
|
|
2
|
-
import { isClient,
|
|
1
|
+
import { useLayoutEffect, useEffect, useState, useRef, useMemo } from 'react';
|
|
2
|
+
import { isClient, initStore, getHash, noop } from 'floppy-disk/vanilla';
|
|
3
3
|
|
|
4
4
|
const useIsomorphicLayoutEffect = isClient ? useLayoutEffect : useEffect;
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
6
|
+
const getValueByPath = (obj, path) => path.reduce((acc, key) => acc == null ? void 0 : acc[key], obj);
|
|
7
|
+
const isPrefixPath = (candidatePrefix, targetPath) => {
|
|
8
|
+
if (candidatePrefix.length >= targetPath.length) return false;
|
|
9
|
+
for (let i = 0; i < candidatePrefix.length; i++) {
|
|
10
|
+
if (candidatePrefix[i] !== targetPath[i]) return false;
|
|
11
|
+
}
|
|
12
|
+
return true;
|
|
13
|
+
};
|
|
14
|
+
const compressPaths = (paths) => {
|
|
15
|
+
const result = [];
|
|
16
|
+
let prev = null;
|
|
17
|
+
for (let i = paths.length - 1; i >= 0; i--) {
|
|
18
|
+
const current = paths[i];
|
|
19
|
+
if (!prev || !isPrefixPath(current, prev)) result.push(current);
|
|
20
|
+
prev = current;
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
19
23
|
};
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
const useStoreStateProxy = (storeState) => {
|
|
25
|
+
const usedPathsRef = useRef([]);
|
|
26
|
+
usedPathsRef.current = [];
|
|
27
|
+
const trackedState = useMemo(() => {
|
|
28
|
+
const track = (path) => usedPathsRef.current.push(path);
|
|
29
|
+
const proxyCache = /* @__PURE__ */ new WeakMap();
|
|
30
|
+
const createDeepProxy = (target, path = []) => {
|
|
31
|
+
if (typeof target !== "object" || target === null) {
|
|
32
|
+
return target;
|
|
33
|
+
}
|
|
34
|
+
if (proxyCache.has(target)) {
|
|
35
|
+
return proxyCache.get(target);
|
|
36
|
+
}
|
|
37
|
+
const proxy = new Proxy(target, {
|
|
38
|
+
get(obj, key) {
|
|
39
|
+
const newPath = [...path, key];
|
|
40
|
+
track(newPath);
|
|
41
|
+
const value = obj[key];
|
|
42
|
+
return createDeepProxy(value, newPath);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
proxyCache.set(target, proxy);
|
|
46
|
+
return proxy;
|
|
47
|
+
};
|
|
48
|
+
return createDeepProxy(storeState);
|
|
49
|
+
}, [storeState]);
|
|
50
|
+
return [trackedState, usedPathsRef];
|
|
51
|
+
};
|
|
52
|
+
const useStoreState = (storeState, subscribe) => {
|
|
53
|
+
const [trackedState, usedPathsRef] = useStoreStateProxy(storeState);
|
|
54
|
+
const [, reRender] = useState({});
|
|
55
|
+
useIsomorphicLayoutEffect(() => {
|
|
56
|
+
return subscribe((nextState, prevState, changedKeys) => {
|
|
57
|
+
const paths = compressPaths(usedPathsRef.current);
|
|
58
|
+
for (const path of paths) {
|
|
59
|
+
const rootKey = path[0];
|
|
60
|
+
if (!changedKeys.includes(rootKey)) continue;
|
|
61
|
+
const prevVal = getValueByPath(prevState, path);
|
|
62
|
+
const nextVal = getValueByPath(nextState, path);
|
|
63
|
+
if (!Object.is(prevVal, nextVal)) return reRender({});
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}, [subscribe]);
|
|
67
|
+
return trackedState;
|
|
23
68
|
};
|
|
24
69
|
|
|
25
70
|
const createStore = (initialState, options) => {
|
|
26
71
|
const store = initStore(initialState, options);
|
|
27
|
-
const useStore = (
|
|
72
|
+
const useStore = () => useStoreState(store.getState(), store.subscribe);
|
|
28
73
|
return Object.assign(useStore, store);
|
|
29
74
|
};
|
|
30
75
|
|
|
@@ -39,9 +84,7 @@ const createStores = (initialState, options) => {
|
|
|
39
84
|
store = initStore(initialState, options);
|
|
40
85
|
stores.set(keyHash, store);
|
|
41
86
|
}
|
|
42
|
-
const useStore = (
|
|
43
|
-
return useStoreState(store, selector);
|
|
44
|
-
};
|
|
87
|
+
const useStore = () => useStoreState(store.getState(), store.subscribe);
|
|
45
88
|
return Object.assign(useStore, {
|
|
46
89
|
...store,
|
|
47
90
|
delete: () => {
|
|
@@ -323,20 +366,50 @@ const createQuery = (queryFn, options = {}) => {
|
|
|
323
366
|
stores.set(variableHash, store);
|
|
324
367
|
internals.set(store, configureInternals(store, variable, variableHash));
|
|
325
368
|
}
|
|
326
|
-
const useStore = (options2 = {}
|
|
327
|
-
|
|
328
|
-
useIsomorphicLayoutEffect(() => {
|
|
329
|
-
if (options2.enabled !== false) revalidate(store, variable, false);
|
|
330
|
-
}, [store, options2.enabled]);
|
|
369
|
+
const useStore = (options2 = {}) => {
|
|
370
|
+
const { enabled = true, keepPreviousData } = options2;
|
|
331
371
|
const storeState = store.getState();
|
|
332
|
-
let storeStateToBeUsed = storeState;
|
|
333
372
|
const prevState = useRef({});
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
373
|
+
let storeStateToBeUsed = storeState;
|
|
374
|
+
if (storeState.state !== "INITIAL") {
|
|
375
|
+
prevState.current = {
|
|
376
|
+
data: storeState.data,
|
|
377
|
+
dataUpdatedAt: storeState.dataUpdatedAt
|
|
378
|
+
};
|
|
379
|
+
} else if (keepPreviousData) {
|
|
337
380
|
storeStateToBeUsed = { ...storeState, ...prevState.current };
|
|
338
381
|
}
|
|
339
|
-
|
|
382
|
+
const [trackedState, usedPathsRef] = useStoreStateProxy(
|
|
383
|
+
enabled && storeState.state === "INITIAL" ? (
|
|
384
|
+
// Optimize rendering on initial state
|
|
385
|
+
// Do { isPending: true } → result
|
|
386
|
+
// instead of { isPending: false } → { isPending: true } → result
|
|
387
|
+
{ ...storeStateToBeUsed, isPending: true }
|
|
388
|
+
) : storeStateToBeUsed
|
|
389
|
+
);
|
|
390
|
+
const [, reRender] = useState({});
|
|
391
|
+
useIsomorphicLayoutEffect(() => {
|
|
392
|
+
return store.subscribe((nextState, prevState2, changedKeys) => {
|
|
393
|
+
if (prevState2.state === "INITIAL" && !prevState2.isPending && nextState.isPending) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const paths = compressPaths(usedPathsRef.current);
|
|
397
|
+
for (const path of paths) {
|
|
398
|
+
const rootKey = path[0];
|
|
399
|
+
if (!changedKeys.includes(rootKey)) continue;
|
|
400
|
+
const prevVal = getValueByPath(prevState2, path);
|
|
401
|
+
const nextVal = getValueByPath(nextState, path);
|
|
402
|
+
if (!Object.is(prevVal, nextVal)) return reRender({});
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
}, [store]);
|
|
406
|
+
useIsomorphicLayoutEffect(() => {
|
|
407
|
+
if (enabled !== false) revalidate(store, variable, false);
|
|
408
|
+
}, [store, enabled]);
|
|
409
|
+
if (keepPreviousData) {
|
|
410
|
+
!!trackedState.error;
|
|
411
|
+
}
|
|
412
|
+
return trackedState;
|
|
340
413
|
};
|
|
341
414
|
return Object.assign(useStore, {
|
|
342
415
|
subscribe: store.subscribe,
|
|
@@ -408,7 +481,7 @@ const createMutation = (mutationFn, options = {}) => {
|
|
|
408
481
|
const { onSuccess = noop, onError, onSettled = noop } = options;
|
|
409
482
|
const initialState = INITIAL_STATE;
|
|
410
483
|
const store = initStore(initialState, options);
|
|
411
|
-
const useStore = (
|
|
484
|
+
const useStore = () => useStoreState(store.getState(), store.subscribe);
|
|
412
485
|
const execute = (variable) => {
|
|
413
486
|
const stateBeforeExecute = store.getState();
|
|
414
487
|
if (stateBeforeExecute.isPending) {
|
|
@@ -500,4 +573,4 @@ const createMutation = (mutationFn, options = {}) => {
|
|
|
500
573
|
});
|
|
501
574
|
};
|
|
502
575
|
|
|
503
|
-
export { createMutation, createQuery, createStore, createStores, useIsomorphicLayoutEffect, useStoreState
|
|
576
|
+
export { createMutation, createQuery, createStore, createStores, useIsomorphicLayoutEffect, useStoreState };
|
package/esm/vanilla/basic.d.mts
CHANGED
|
@@ -6,14 +6,6 @@ export declare const isClient: boolean;
|
|
|
6
6
|
* Empty function.
|
|
7
7
|
*/
|
|
8
8
|
export declare const noop: () => void;
|
|
9
|
-
/**
|
|
10
|
-
* Identity function.
|
|
11
|
-
*
|
|
12
|
-
* It accepts 1 argument, and simply return it.
|
|
13
|
-
*
|
|
14
|
-
* `const identity = value => value`
|
|
15
|
-
*/
|
|
16
|
-
export declare const identity: <T>(value: T) => T;
|
|
17
9
|
/**
|
|
18
10
|
* If the value is a function, it will invoke the function.\
|
|
19
11
|
* If the value is not a function, it will just return it.
|
package/esm/vanilla/store.d.mts
CHANGED
|
@@ -11,18 +11,21 @@ export type SetState<TState> = Partial<TState> | ((state: TState) => Partial<TSt
|
|
|
11
11
|
*
|
|
12
12
|
* @param state - The latest state
|
|
13
13
|
* @param prevState - The previous state before the update
|
|
14
|
+
* @param changedKeys - The top-level keys that changed (shallow diff)
|
|
14
15
|
*
|
|
15
16
|
* @remarks
|
|
16
|
-
* - Subscribers are only called when
|
|
17
|
+
* - Subscribers are only called when at least one field changes.
|
|
17
18
|
* - Change detection is performed per key using `Object.is`.
|
|
19
|
+
* - `changedKeys` only includes top-level keys; nested changes must be inferred by the consumer.
|
|
18
20
|
*/
|
|
19
|
-
export type Subscriber<TState> = (state: TState, prevState: TState) => void;
|
|
21
|
+
export type Subscriber<TState> = (state: TState, prevState: TState, changedKeys: Array<keyof TState>) => void;
|
|
20
22
|
/**
|
|
21
23
|
* Core store API for managing state.
|
|
22
24
|
*
|
|
23
25
|
* @remarks
|
|
24
26
|
* - The store performs **shallow change detection per key** before notifying subscribers.
|
|
25
27
|
* - Subscribers are only notified when at least one field changes.
|
|
28
|
+
* - State is treated as **immutable**. Mutating nested values directly will not trigger updates.
|
|
26
29
|
* - Designed to be framework-agnostic (React bindings are built separately).
|
|
27
30
|
* - By default, `setState` is **disabled on the server** to prevent accidental shared state between requests.
|
|
28
31
|
*/
|
|
@@ -48,13 +51,17 @@ export type InitStoreOptions<TState extends Record<string, any>> = {
|
|
|
48
51
|
onSubscribe?: (state: TState, store: StoreApi<TState>) => void;
|
|
49
52
|
onUnsubscribe?: (state: TState, store: StoreApi<TState>) => void;
|
|
50
53
|
onLastUnsubscribe?: (state: TState, store: StoreApi<TState>) => void;
|
|
54
|
+
/**
|
|
55
|
+
* By default, calling `setState` on the server is disallowed to prevent shared state across requests.
|
|
56
|
+
* Set this to `true` only if you explicitly intend to mutate state during server execution.
|
|
57
|
+
*/
|
|
51
58
|
allowSetStateServerSide?: boolean;
|
|
52
59
|
};
|
|
53
60
|
/**
|
|
54
61
|
* Creates a vanilla store with pub-sub capabilities.
|
|
55
62
|
*
|
|
56
|
-
* The store state
|
|
57
|
-
* Updates are applied as
|
|
63
|
+
* The store state must be an **object**.\
|
|
64
|
+
* Updates are applied as shallow merges, so non-object states are not supported.
|
|
58
65
|
*
|
|
59
66
|
* @param initialState - The initial state of the store
|
|
60
67
|
* @param options - Optional lifecycle hooks
|
|
@@ -64,11 +71,13 @@ export type InitStoreOptions<TState extends Record<string, any>> = {
|
|
|
64
71
|
* @remarks
|
|
65
72
|
* - State updates are **shallowly compared per key** before notifying subscribers.
|
|
66
73
|
* - Subscribers are only notified when at least one updated field changes (using `Object.is` comparison).
|
|
67
|
-
* - Subscribers receive
|
|
74
|
+
* - Subscribers receive the new state, previous state, and changed top-level keys.
|
|
75
|
+
* - State is expected to be treated as **immutable**.
|
|
76
|
+
* - Mutating nested values directly will not trigger updates.
|
|
68
77
|
* - Lifecycle hooks allow side-effect management tied to subscription count.
|
|
69
|
-
* - By default, `setState` is **
|
|
70
|
-
* - This
|
|
71
|
-
* -
|
|
78
|
+
* - By default, `setState` is **not allowed on the server** to prevent accidental shared state between requests.
|
|
79
|
+
* - This helps avoid leaking data between users in server environments.
|
|
80
|
+
* - If you intentionally want to allow this behavior, set `allowSetStateServerSide: true`.
|
|
72
81
|
*
|
|
73
82
|
* @example
|
|
74
83
|
* const store = initStore({ count: 0 });
|
package/esm/vanilla.d.mts
CHANGED
package/esm/vanilla.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
const isClient = typeof window !== "undefined" && !("Deno" in window);
|
|
2
2
|
const noop = () => {
|
|
3
3
|
};
|
|
4
|
-
const identity = (value) => value;
|
|
5
4
|
const getValue = (valueOrComputeValueFn, ...params) => {
|
|
6
5
|
if (typeof valueOrComputeValueFn === "function") {
|
|
7
6
|
return valueOrComputeValueFn(...params);
|
|
@@ -9,41 +8,6 @@ const getValue = (valueOrComputeValueFn, ...params) => {
|
|
|
9
8
|
return valueOrComputeValueFn;
|
|
10
9
|
};
|
|
11
10
|
|
|
12
|
-
const shallow = (a, b) => {
|
|
13
|
-
if (Object.is(a, b)) {
|
|
14
|
-
return true;
|
|
15
|
-
}
|
|
16
|
-
if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
if (a instanceof Map && b instanceof Map) {
|
|
20
|
-
if (a.size !== b.size) return false;
|
|
21
|
-
for (const [key, value] of a) {
|
|
22
|
-
if (!Object.is(value, b.get(key))) {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return true;
|
|
27
|
-
}
|
|
28
|
-
if (a instanceof Set && b instanceof Set) {
|
|
29
|
-
if (a.size !== b.size) return false;
|
|
30
|
-
for (const value of a) {
|
|
31
|
-
if (!b.has(value)) return false;
|
|
32
|
-
}
|
|
33
|
-
return true;
|
|
34
|
-
}
|
|
35
|
-
const keysA = Object.keys(a);
|
|
36
|
-
if (keysA.length !== Object.keys(b).length) {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
for (let i = 0; i < keysA.length; i++) {
|
|
40
|
-
if (!Object.prototype.hasOwnProperty.call(b, keysA[i]) || !Object.is(a[keysA[i]], b[keysA[i]])) {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return true;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
11
|
const hasObjectPrototype = (value) => {
|
|
48
12
|
return Object.prototype.toString.call(value) === "[object Object]";
|
|
49
13
|
};
|
|
@@ -96,13 +60,15 @@ const initStore = (initialState, options = {}) => {
|
|
|
96
60
|
}
|
|
97
61
|
const prevState = state;
|
|
98
62
|
const newValue = getValue(value, state);
|
|
63
|
+
const changedKeys = [];
|
|
99
64
|
for (const key in newValue) {
|
|
100
65
|
if (!Object.is(prevState[key], newValue[key])) {
|
|
101
|
-
|
|
102
|
-
[...subscribers].forEach((subscriber) => subscriber(state, prevState));
|
|
103
|
-
return;
|
|
66
|
+
changedKeys.push(key);
|
|
104
67
|
}
|
|
105
68
|
}
|
|
69
|
+
if (changedKeys.length === 0) return;
|
|
70
|
+
state = { ...prevState, ...newValue };
|
|
71
|
+
[...subscribers].forEach((subscriber) => subscriber(state, prevState, changedKeys));
|
|
106
72
|
};
|
|
107
73
|
const storeApi = {
|
|
108
74
|
getState,
|
|
@@ -113,4 +79,4 @@ const initStore = (initialState, options = {}) => {
|
|
|
113
79
|
return storeApi;
|
|
114
80
|
};
|
|
115
81
|
|
|
116
|
-
export { getHash, getValue,
|
|
82
|
+
export { getHash, getValue, initStore, isClient, isPlainObject, noop };
|
package/package.json
CHANGED
|
@@ -89,7 +89,7 @@ export type MutationOptions<TData, TVariable> = InitStoreOptions<MutationState<T
|
|
|
89
89
|
* const { isPending } = useCreateUser();
|
|
90
90
|
* const result = await useCreateUser.execute({ name: 'John' });
|
|
91
91
|
*/
|
|
92
|
-
export declare const createMutation: <TData, TVariable = undefined>(mutationFn: (variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable>) => Promise<TData>, options?: MutationOptions<TData, TVariable>) => (
|
|
92
|
+
export declare const createMutation: <TData, TVariable = undefined>(mutationFn: (variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable>) => Promise<TData>, options?: MutationOptions<TData, TVariable>) => (() => MutationState<TData, TVariable>) & {
|
|
93
93
|
subscribe: (subscriber: import("../vanilla.ts").Subscriber<MutationState<TData, TVariable>>) => () => void;
|
|
94
94
|
getSubscribers: () => Set<import("../vanilla.ts").Subscriber<MutationState<TData, TVariable>>>;
|
|
95
95
|
getState: () => MutationState<TData, TVariable>;
|
package/react/create-query.d.ts
CHANGED
|
@@ -67,9 +67,9 @@ export type QueryOptions<TData, TVariable extends Record<string, any>> = InitSto
|
|
|
67
67
|
/**
|
|
68
68
|
* Time (in milliseconds) that data is considered fresh.
|
|
69
69
|
*
|
|
70
|
-
* While fresh, revalidation will be skipped.
|
|
70
|
+
* While fresh, revalidation will be skipped unless explicitly invalidated.
|
|
71
71
|
*
|
|
72
|
-
* @default 2500 ms (2.5
|
|
72
|
+
* @default 2500 ms (2.5 seconds)
|
|
73
73
|
*/
|
|
74
74
|
staleTime?: number;
|
|
75
75
|
/**
|
|
@@ -131,16 +131,20 @@ export type QueryOptions<TData, TVariable extends Record<string, any>> = InitSto
|
|
|
131
131
|
* @returns A function to retrieve or create a query instance by variable
|
|
132
132
|
*
|
|
133
133
|
* @remarks
|
|
134
|
-
* - Queries are
|
|
134
|
+
* - Queries are **keyed by variable** (via deterministic hashing).
|
|
135
135
|
* - Each unique variable maps to its own store instance.
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* -
|
|
143
|
-
*
|
|
136
|
+
*
|
|
137
|
+
* Core features:
|
|
138
|
+
* - Caching via `staleTime`
|
|
139
|
+
* - Explicit invalidation (independent of freshness)
|
|
140
|
+
* - Retry logic via `shouldRetry`
|
|
141
|
+
* - Background revalidation (focus / reconnect)
|
|
142
|
+
* - Garbage collection via `gcTime`
|
|
143
|
+
*
|
|
144
|
+
* Execution behavior:
|
|
145
|
+
* - By default, executions **overwrite ongoing executions**
|
|
146
|
+
* - Set `overwriteOngoingExecution: false` to enable deduplication
|
|
147
|
+
* - Internal revalidation (focus/reconnect) uses deduplication by default
|
|
144
148
|
*
|
|
145
149
|
* @example
|
|
146
150
|
* const userQuery = createQuery<UserDetail, { id: string }>(async ({ id }) => {
|
|
@@ -153,7 +157,7 @@ export type QueryOptions<TData, TVariable extends Record<string, any>> = InitSto
|
|
|
153
157
|
* // ...
|
|
154
158
|
* }
|
|
155
159
|
*/
|
|
156
|
-
export declare const createQuery: <TData, TVariable extends Record<string, any> = never>(queryFn: (variable: TVariable, currentState: QueryState<TData>) => Promise<TData>, options?: QueryOptions<TData, TVariable>) => ((variable?: TVariable) => (
|
|
160
|
+
export declare const createQuery: <TData, TVariable extends Record<string, any> = never>(queryFn: (variable: TVariable, currentState: QueryState<TData>) => Promise<TData>, options?: QueryOptions<TData, TVariable>) => ((variable?: TVariable) => ((options?: {
|
|
157
161
|
/**
|
|
158
162
|
* Whether the query should execute automatically on mount.
|
|
159
163
|
*
|
|
@@ -176,7 +180,7 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
|
|
|
176
180
|
* // While loading userId=2, still show userId=1 data
|
|
177
181
|
* useQuery({ id: userId }, { keepPreviousData: true });
|
|
178
182
|
*/ keepPreviousData?: boolean;
|
|
179
|
-
}
|
|
183
|
+
}) => QueryState<TData>) & {
|
|
180
184
|
metadata: {
|
|
181
185
|
isInvalidated?: boolean;
|
|
182
186
|
promise?: Promise<QueryState<TData>> | undefined;
|
|
@@ -208,7 +212,7 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
|
|
|
208
212
|
* @returns A promise resolving to the latest query state
|
|
209
213
|
*
|
|
210
214
|
* @remarks
|
|
211
|
-
* - By default, each call starts a new execution even if one is already in progress.
|
|
215
|
+
* - By default, each call **starts a new execution** even if one is already in progress.
|
|
212
216
|
* - Set `overwriteOngoingExecution: false` to reuse an ongoing execution (deduplication).
|
|
213
217
|
* - Handles:
|
|
214
218
|
* - Pending state
|
package/react/create-store.d.ts
CHANGED
|
@@ -12,14 +12,17 @@ import { type InitStoreOptions } from 'floppy-disk/vanilla';
|
|
|
12
12
|
* @remarks
|
|
13
13
|
* - Combines the vanilla store with React integration.
|
|
14
14
|
* - The returned function can be used directly as a hook.
|
|
15
|
+
* - The hook uses Proxy-based tracking to automatically detect which state fields are used.
|
|
16
|
+
* - Components will only re-render when the accessed values change.
|
|
15
17
|
*
|
|
16
18
|
* @example
|
|
17
|
-
* const
|
|
19
|
+
* const useMyStore = createStore({ foo: 1, bar: 2 });
|
|
18
20
|
*
|
|
19
21
|
* function Component() {
|
|
20
|
-
* const
|
|
22
|
+
* const state = useMyStore();
|
|
23
|
+
* return <div>{state.foo}</div>;
|
|
21
24
|
* }
|
|
22
25
|
*
|
|
23
|
-
*
|
|
26
|
+
* useMyStore.setState({ foo: 2 }); // only components using foo will re-render
|
|
24
27
|
*/
|
|
25
|
-
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>) => (() => TState) & import("../vanilla.ts").StoreApi<TState>;
|
package/react/create-stores.d.ts
CHANGED
|
@@ -12,18 +12,25 @@ import { type InitStoreOptions } from 'floppy-disk/vanilla';
|
|
|
12
12
|
* - Keys are deterministically hashed, ensuring stable identity.
|
|
13
13
|
* - Stores are lazily created and cached.
|
|
14
14
|
* - Each store has its own state, subscribers, and lifecycle.
|
|
15
|
+
* - Each returned store includes:
|
|
16
|
+
* - React hook (with Proxy-based tracking)
|
|
17
|
+
* - Store API methods
|
|
18
|
+
* - `delete()` for manual cleanup
|
|
15
19
|
* - Useful for scenarios like:
|
|
16
20
|
* - Query caches
|
|
17
21
|
* - Entity-based state
|
|
18
22
|
* - Dynamic instances
|
|
19
23
|
*
|
|
20
24
|
* @example
|
|
21
|
-
* const
|
|
25
|
+
* const userStore = createStores<{ name: string }, { id: number }>({ name: '' });
|
|
22
26
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
27
|
+
* function Component() {
|
|
28
|
+
* const useUserStore = userStore({ id: 1 });
|
|
29
|
+
* const state = useUserStore();
|
|
30
|
+
* return <div>{state.name}</div>;
|
|
31
|
+
* }
|
|
25
32
|
*/
|
|
26
|
-
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) => (() => TState) & {
|
|
27
34
|
delete: () => boolean;
|
|
28
35
|
setState: (value: import("../vanilla.ts").SetState<TState>) => void;
|
|
29
36
|
getState: () => TState;
|
package/react/use-store.d.ts
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
import { type StoreApi } from 'floppy-disk/vanilla';
|
|
2
|
-
|
|
2
|
+
type Path = Array<string | number | symbol>;
|
|
3
|
+
export declare const getValueByPath: (obj: any, path: Path) => any;
|
|
4
|
+
export declare const isPrefixPath: (candidatePrefix: Path, targetPath: Path) => boolean;
|
|
5
|
+
export declare const compressPaths: (paths: Path[]) => Path[];
|
|
6
|
+
export declare const useStoreStateProxy: <TState extends Record<string, any>>(storeState: TState) => readonly [TState, import("react").RefObject<Path[]>];
|
|
3
7
|
/**
|
|
4
|
-
* React hook for subscribing to a store
|
|
8
|
+
* React hook for subscribing to a store using automatic dependency tracking.
|
|
5
9
|
*
|
|
6
10
|
* @param store - The store instance to subscribe to
|
|
7
|
-
* @param selector - Optional selector to derive a slice of state
|
|
8
11
|
*
|
|
9
|
-
* @returns
|
|
12
|
+
* @returns A proxied version of the store state
|
|
10
13
|
*
|
|
11
14
|
* @remarks
|
|
12
|
-
* -
|
|
13
|
-
* - The
|
|
15
|
+
* - This hook uses a **Proxy-based tracking mechanism** to detect which parts of the state are accessed during render.
|
|
16
|
+
* - The component will only re-render when the **accessed values actually change**.
|
|
17
|
+
* - State must be treated as **immutable**:
|
|
18
|
+
* - Updates must replace objects rather than mutate them
|
|
19
|
+
* - Otherwise, changes may not be detected
|
|
20
|
+
* - No selector or memoization is needed.
|
|
14
21
|
*
|
|
15
22
|
* @example
|
|
16
|
-
* const
|
|
23
|
+
* const state = useStoreState(store);
|
|
24
|
+
* return <div>{state.user.name}</div>;
|
|
25
|
+
* // Component will only re-render if `user.name` changes
|
|
17
26
|
*/
|
|
18
|
-
export declare const useStoreState: <TState extends Record<string, any
|
|
27
|
+
export declare const useStoreState: <TState extends Record<string, any>>(storeState: TState, subscribe: StoreApi<TState>["subscribe"]) => TState;
|
|
28
|
+
export {};
|
package/react.d.ts
CHANGED
package/react.js
CHANGED
|
@@ -5,28 +5,73 @@ var vanilla = require('floppy-disk/vanilla');
|
|
|
5
5
|
|
|
6
6
|
const useIsomorphicLayoutEffect = vanilla.isClient ? react.useLayoutEffect : react.useEffect;
|
|
7
7
|
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
8
|
+
const getValueByPath = (obj, path) => path.reduce((acc, key) => acc == null ? void 0 : acc[key], obj);
|
|
9
|
+
const isPrefixPath = (candidatePrefix, targetPath) => {
|
|
10
|
+
if (candidatePrefix.length >= targetPath.length) return false;
|
|
11
|
+
for (let i = 0; i < candidatePrefix.length; i++) {
|
|
12
|
+
if (candidatePrefix[i] !== targetPath[i]) return false;
|
|
13
|
+
}
|
|
14
|
+
return true;
|
|
15
|
+
};
|
|
16
|
+
const compressPaths = (paths) => {
|
|
17
|
+
const result = [];
|
|
18
|
+
let prev = null;
|
|
19
|
+
for (let i = paths.length - 1; i >= 0; i--) {
|
|
20
|
+
const current = paths[i];
|
|
21
|
+
if (!prev || !isPrefixPath(current, prev)) result.push(current);
|
|
22
|
+
prev = current;
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
21
25
|
};
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
const useStoreStateProxy = (storeState) => {
|
|
27
|
+
const usedPathsRef = react.useRef([]);
|
|
28
|
+
usedPathsRef.current = [];
|
|
29
|
+
const trackedState = react.useMemo(() => {
|
|
30
|
+
const track = (path) => usedPathsRef.current.push(path);
|
|
31
|
+
const proxyCache = /* @__PURE__ */ new WeakMap();
|
|
32
|
+
const createDeepProxy = (target, path = []) => {
|
|
33
|
+
if (typeof target !== "object" || target === null) {
|
|
34
|
+
return target;
|
|
35
|
+
}
|
|
36
|
+
if (proxyCache.has(target)) {
|
|
37
|
+
return proxyCache.get(target);
|
|
38
|
+
}
|
|
39
|
+
const proxy = new Proxy(target, {
|
|
40
|
+
get(obj, key) {
|
|
41
|
+
const newPath = [...path, key];
|
|
42
|
+
track(newPath);
|
|
43
|
+
const value = obj[key];
|
|
44
|
+
return createDeepProxy(value, newPath);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
proxyCache.set(target, proxy);
|
|
48
|
+
return proxy;
|
|
49
|
+
};
|
|
50
|
+
return createDeepProxy(storeState);
|
|
51
|
+
}, [storeState]);
|
|
52
|
+
return [trackedState, usedPathsRef];
|
|
53
|
+
};
|
|
54
|
+
const useStoreState = (storeState, subscribe) => {
|
|
55
|
+
const [trackedState, usedPathsRef] = useStoreStateProxy(storeState);
|
|
56
|
+
const [, reRender] = react.useState({});
|
|
57
|
+
useIsomorphicLayoutEffect(() => {
|
|
58
|
+
return subscribe((nextState, prevState, changedKeys) => {
|
|
59
|
+
const paths = compressPaths(usedPathsRef.current);
|
|
60
|
+
for (const path of paths) {
|
|
61
|
+
const rootKey = path[0];
|
|
62
|
+
if (!changedKeys.includes(rootKey)) continue;
|
|
63
|
+
const prevVal = getValueByPath(prevState, path);
|
|
64
|
+
const nextVal = getValueByPath(nextState, path);
|
|
65
|
+
if (!Object.is(prevVal, nextVal)) return reRender({});
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}, [subscribe]);
|
|
69
|
+
return trackedState;
|
|
25
70
|
};
|
|
26
71
|
|
|
27
72
|
const createStore = (initialState, options) => {
|
|
28
73
|
const store = vanilla.initStore(initialState, options);
|
|
29
|
-
const useStore = (
|
|
74
|
+
const useStore = () => useStoreState(store.getState(), store.subscribe);
|
|
30
75
|
return Object.assign(useStore, store);
|
|
31
76
|
};
|
|
32
77
|
|
|
@@ -41,9 +86,7 @@ const createStores = (initialState, options) => {
|
|
|
41
86
|
store = vanilla.initStore(initialState, options);
|
|
42
87
|
stores.set(keyHash, store);
|
|
43
88
|
}
|
|
44
|
-
const useStore = (
|
|
45
|
-
return useStoreState(store, selector);
|
|
46
|
-
};
|
|
89
|
+
const useStore = () => useStoreState(store.getState(), store.subscribe);
|
|
47
90
|
return Object.assign(useStore, {
|
|
48
91
|
...store,
|
|
49
92
|
delete: () => {
|
|
@@ -325,20 +368,50 @@ const createQuery = (queryFn, options = {}) => {
|
|
|
325
368
|
stores.set(variableHash, store);
|
|
326
369
|
internals.set(store, configureInternals(store, variable, variableHash));
|
|
327
370
|
}
|
|
328
|
-
const useStore = (options2 = {}
|
|
329
|
-
|
|
330
|
-
useIsomorphicLayoutEffect(() => {
|
|
331
|
-
if (options2.enabled !== false) revalidate(store, variable, false);
|
|
332
|
-
}, [store, options2.enabled]);
|
|
371
|
+
const useStore = (options2 = {}) => {
|
|
372
|
+
const { enabled = true, keepPreviousData } = options2;
|
|
333
373
|
const storeState = store.getState();
|
|
334
|
-
let storeStateToBeUsed = storeState;
|
|
335
374
|
const prevState = react.useRef({});
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
375
|
+
let storeStateToBeUsed = storeState;
|
|
376
|
+
if (storeState.state !== "INITIAL") {
|
|
377
|
+
prevState.current = {
|
|
378
|
+
data: storeState.data,
|
|
379
|
+
dataUpdatedAt: storeState.dataUpdatedAt
|
|
380
|
+
};
|
|
381
|
+
} else if (keepPreviousData) {
|
|
339
382
|
storeStateToBeUsed = { ...storeState, ...prevState.current };
|
|
340
383
|
}
|
|
341
|
-
|
|
384
|
+
const [trackedState, usedPathsRef] = useStoreStateProxy(
|
|
385
|
+
enabled && storeState.state === "INITIAL" ? (
|
|
386
|
+
// Optimize rendering on initial state
|
|
387
|
+
// Do { isPending: true } → result
|
|
388
|
+
// instead of { isPending: false } → { isPending: true } → result
|
|
389
|
+
{ ...storeStateToBeUsed, isPending: true }
|
|
390
|
+
) : storeStateToBeUsed
|
|
391
|
+
);
|
|
392
|
+
const [, reRender] = react.useState({});
|
|
393
|
+
useIsomorphicLayoutEffect(() => {
|
|
394
|
+
return store.subscribe((nextState, prevState2, changedKeys) => {
|
|
395
|
+
if (prevState2.state === "INITIAL" && !prevState2.isPending && nextState.isPending) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const paths = compressPaths(usedPathsRef.current);
|
|
399
|
+
for (const path of paths) {
|
|
400
|
+
const rootKey = path[0];
|
|
401
|
+
if (!changedKeys.includes(rootKey)) continue;
|
|
402
|
+
const prevVal = getValueByPath(prevState2, path);
|
|
403
|
+
const nextVal = getValueByPath(nextState, path);
|
|
404
|
+
if (!Object.is(prevVal, nextVal)) return reRender({});
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
}, [store]);
|
|
408
|
+
useIsomorphicLayoutEffect(() => {
|
|
409
|
+
if (enabled !== false) revalidate(store, variable, false);
|
|
410
|
+
}, [store, enabled]);
|
|
411
|
+
if (keepPreviousData) {
|
|
412
|
+
!!trackedState.error;
|
|
413
|
+
}
|
|
414
|
+
return trackedState;
|
|
342
415
|
};
|
|
343
416
|
return Object.assign(useStore, {
|
|
344
417
|
subscribe: store.subscribe,
|
|
@@ -410,7 +483,7 @@ const createMutation = (mutationFn, options = {}) => {
|
|
|
410
483
|
const { onSuccess = vanilla.noop, onError, onSettled = vanilla.noop } = options;
|
|
411
484
|
const initialState = INITIAL_STATE;
|
|
412
485
|
const store = vanilla.initStore(initialState, options);
|
|
413
|
-
const useStore = (
|
|
486
|
+
const useStore = () => useStoreState(store.getState(), store.subscribe);
|
|
414
487
|
const execute = (variable) => {
|
|
415
488
|
const stateBeforeExecute = store.getState();
|
|
416
489
|
if (stateBeforeExecute.isPending) {
|
|
@@ -508,4 +581,3 @@ exports.createStore = createStore;
|
|
|
508
581
|
exports.createStores = createStores;
|
|
509
582
|
exports.useIsomorphicLayoutEffect = useIsomorphicLayoutEffect;
|
|
510
583
|
exports.useStoreState = useStoreState;
|
|
511
|
-
exports.useStoreUpdateNotifier = useStoreUpdateNotifier;
|
package/vanilla/basic.d.ts
CHANGED
|
@@ -6,14 +6,6 @@ export declare const isClient: boolean;
|
|
|
6
6
|
* Empty function.
|
|
7
7
|
*/
|
|
8
8
|
export declare const noop: () => void;
|
|
9
|
-
/**
|
|
10
|
-
* Identity function.
|
|
11
|
-
*
|
|
12
|
-
* It accepts 1 argument, and simply return it.
|
|
13
|
-
*
|
|
14
|
-
* `const identity = value => value`
|
|
15
|
-
*/
|
|
16
|
-
export declare const identity: <T>(value: T) => T;
|
|
17
9
|
/**
|
|
18
10
|
* If the value is a function, it will invoke the function.\
|
|
19
11
|
* If the value is not a function, it will just return it.
|
package/vanilla/store.d.ts
CHANGED
|
@@ -11,18 +11,21 @@ export type SetState<TState> = Partial<TState> | ((state: TState) => Partial<TSt
|
|
|
11
11
|
*
|
|
12
12
|
* @param state - The latest state
|
|
13
13
|
* @param prevState - The previous state before the update
|
|
14
|
+
* @param changedKeys - The top-level keys that changed (shallow diff)
|
|
14
15
|
*
|
|
15
16
|
* @remarks
|
|
16
|
-
* - Subscribers are only called when
|
|
17
|
+
* - Subscribers are only called when at least one field changes.
|
|
17
18
|
* - Change detection is performed per key using `Object.is`.
|
|
19
|
+
* - `changedKeys` only includes top-level keys; nested changes must be inferred by the consumer.
|
|
18
20
|
*/
|
|
19
|
-
export type Subscriber<TState> = (state: TState, prevState: TState) => void;
|
|
21
|
+
export type Subscriber<TState> = (state: TState, prevState: TState, changedKeys: Array<keyof TState>) => void;
|
|
20
22
|
/**
|
|
21
23
|
* Core store API for managing state.
|
|
22
24
|
*
|
|
23
25
|
* @remarks
|
|
24
26
|
* - The store performs **shallow change detection per key** before notifying subscribers.
|
|
25
27
|
* - Subscribers are only notified when at least one field changes.
|
|
28
|
+
* - State is treated as **immutable**. Mutating nested values directly will not trigger updates.
|
|
26
29
|
* - Designed to be framework-agnostic (React bindings are built separately).
|
|
27
30
|
* - By default, `setState` is **disabled on the server** to prevent accidental shared state between requests.
|
|
28
31
|
*/
|
|
@@ -48,13 +51,17 @@ export type InitStoreOptions<TState extends Record<string, any>> = {
|
|
|
48
51
|
onSubscribe?: (state: TState, store: StoreApi<TState>) => void;
|
|
49
52
|
onUnsubscribe?: (state: TState, store: StoreApi<TState>) => void;
|
|
50
53
|
onLastUnsubscribe?: (state: TState, store: StoreApi<TState>) => void;
|
|
54
|
+
/**
|
|
55
|
+
* By default, calling `setState` on the server is disallowed to prevent shared state across requests.
|
|
56
|
+
* Set this to `true` only if you explicitly intend to mutate state during server execution.
|
|
57
|
+
*/
|
|
51
58
|
allowSetStateServerSide?: boolean;
|
|
52
59
|
};
|
|
53
60
|
/**
|
|
54
61
|
* Creates a vanilla store with pub-sub capabilities.
|
|
55
62
|
*
|
|
56
|
-
* The store state
|
|
57
|
-
* Updates are applied as
|
|
63
|
+
* The store state must be an **object**.\
|
|
64
|
+
* Updates are applied as shallow merges, so non-object states are not supported.
|
|
58
65
|
*
|
|
59
66
|
* @param initialState - The initial state of the store
|
|
60
67
|
* @param options - Optional lifecycle hooks
|
|
@@ -64,11 +71,13 @@ export type InitStoreOptions<TState extends Record<string, any>> = {
|
|
|
64
71
|
* @remarks
|
|
65
72
|
* - State updates are **shallowly compared per key** before notifying subscribers.
|
|
66
73
|
* - Subscribers are only notified when at least one updated field changes (using `Object.is` comparison).
|
|
67
|
-
* - Subscribers receive
|
|
74
|
+
* - Subscribers receive the new state, previous state, and changed top-level keys.
|
|
75
|
+
* - State is expected to be treated as **immutable**.
|
|
76
|
+
* - Mutating nested values directly will not trigger updates.
|
|
68
77
|
* - Lifecycle hooks allow side-effect management tied to subscription count.
|
|
69
|
-
* - By default, `setState` is **
|
|
70
|
-
* - This
|
|
71
|
-
* -
|
|
78
|
+
* - By default, `setState` is **not allowed on the server** to prevent accidental shared state between requests.
|
|
79
|
+
* - This helps avoid leaking data between users in server environments.
|
|
80
|
+
* - If you intentionally want to allow this behavior, set `allowSetStateServerSide: true`.
|
|
72
81
|
*
|
|
73
82
|
* @example
|
|
74
83
|
* const store = initStore({ count: 0 });
|
package/vanilla.d.ts
CHANGED
package/vanilla.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
const isClient = typeof window !== "undefined" && !("Deno" in window);
|
|
4
4
|
const noop = () => {
|
|
5
5
|
};
|
|
6
|
-
const identity = (value) => value;
|
|
7
6
|
const getValue = (valueOrComputeValueFn, ...params) => {
|
|
8
7
|
if (typeof valueOrComputeValueFn === "function") {
|
|
9
8
|
return valueOrComputeValueFn(...params);
|
|
@@ -11,41 +10,6 @@ const getValue = (valueOrComputeValueFn, ...params) => {
|
|
|
11
10
|
return valueOrComputeValueFn;
|
|
12
11
|
};
|
|
13
12
|
|
|
14
|
-
const shallow = (a, b) => {
|
|
15
|
-
if (Object.is(a, b)) {
|
|
16
|
-
return true;
|
|
17
|
-
}
|
|
18
|
-
if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) {
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
if (a instanceof Map && b instanceof Map) {
|
|
22
|
-
if (a.size !== b.size) return false;
|
|
23
|
-
for (const [key, value] of a) {
|
|
24
|
-
if (!Object.is(value, b.get(key))) {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
30
|
-
if (a instanceof Set && b instanceof Set) {
|
|
31
|
-
if (a.size !== b.size) return false;
|
|
32
|
-
for (const value of a) {
|
|
33
|
-
if (!b.has(value)) return false;
|
|
34
|
-
}
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
const keysA = Object.keys(a);
|
|
38
|
-
if (keysA.length !== Object.keys(b).length) {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
for (let i = 0; i < keysA.length; i++) {
|
|
42
|
-
if (!Object.prototype.hasOwnProperty.call(b, keysA[i]) || !Object.is(a[keysA[i]], b[keysA[i]])) {
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return true;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
13
|
const hasObjectPrototype = (value) => {
|
|
50
14
|
return Object.prototype.toString.call(value) === "[object Object]";
|
|
51
15
|
};
|
|
@@ -98,13 +62,15 @@ const initStore = (initialState, options = {}) => {
|
|
|
98
62
|
}
|
|
99
63
|
const prevState = state;
|
|
100
64
|
const newValue = getValue(value, state);
|
|
65
|
+
const changedKeys = [];
|
|
101
66
|
for (const key in newValue) {
|
|
102
67
|
if (!Object.is(prevState[key], newValue[key])) {
|
|
103
|
-
|
|
104
|
-
[...subscribers].forEach((subscriber) => subscriber(state, prevState));
|
|
105
|
-
return;
|
|
68
|
+
changedKeys.push(key);
|
|
106
69
|
}
|
|
107
70
|
}
|
|
71
|
+
if (changedKeys.length === 0) return;
|
|
72
|
+
state = { ...prevState, ...newValue };
|
|
73
|
+
[...subscribers].forEach((subscriber) => subscriber(state, prevState, changedKeys));
|
|
108
74
|
};
|
|
109
75
|
const storeApi = {
|
|
110
76
|
getState,
|
|
@@ -117,9 +83,7 @@ const initStore = (initialState, options = {}) => {
|
|
|
117
83
|
|
|
118
84
|
exports.getHash = getHash;
|
|
119
85
|
exports.getValue = getValue;
|
|
120
|
-
exports.identity = identity;
|
|
121
86
|
exports.initStore = initStore;
|
|
122
87
|
exports.isClient = isClient;
|
|
123
88
|
exports.isPlainObject = isPlainObject;
|
|
124
89
|
exports.noop = noop;
|
|
125
|
-
exports.shallow = shallow;
|