floppy-disk 3.0.1 → 3.1.0-beta.1

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 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
- FloppyDisk is designed primarily for **client-side [sync/async] state**.
368
+ Examples for using stores and queries in SSR with isolated data (no shared state between users).
369
369
 
370
- If your data is already fetched on the server (e.g. via SSR/ISR, Server Components, or Server Actions), then:
370
+ ### Initialize Store State from Server
371
371
 
372
- > **You most likely don't need this library.**
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
- - fetch on the server
384
- - hydrate it into a client-side cache
385
- - then revalidate using a query library
375
+ function Page({ initialCount }) {
376
+ const { count } = useCountStore({
377
+ initialState: { count: initialCount }, // e.g. 3
378
+ });
386
379
 
387
- While this works, it introduces additional complexity.
380
+ return <>count is {count}</>; // Output: count is 3
381
+ }
382
+ ```
388
383
 
389
- Instead, we encourage a simpler approach:
384
+ ### Initialize Query Data from Server
390
385
 
391
- > If your data is fetched on the server, revalidate it using **your framework's built-in mechanism** (e.g. Next.js route revalidation).
386
+ ```tsx
387
+ async function MyServerComponent() {
388
+ const data = await getData(); // e.g. { count: 3 }
389
+ return <MyClientComponent initialData={data} />;
390
+ }
392
391
 
393
- Because of this philosophy, FloppyDisk **does not support** hydrating server-fetched data into the client store.
392
+ const myQuery = createQuery(getData);
393
+ const useMyQuery = myQuery();
394
394
 
395
- This keeps the mental model clean:
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
- - server data handled by the framework
398
- - client async state → handled by FloppyDisk
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
- }) => QueryState<TData, TError>) & {
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>) => (() => TState) & import("../vanilla.d.mts").StoreApi<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) => (() => TState) & {
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
- * - No selector or memoization is needed.
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>>(storeState: TState, subscribe: StoreApi<TState>["subscribe"]) => TState;
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 useStoreState = (storeState, subscribe) => {
53
- const [trackedState, usedPathsRef] = useStoreStateProxy(storeState);
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
- }, [subscribe]);
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.getState(), store.subscribe);
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.getState(), store.subscribe);
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 { revalidateOnMount = true, keepPreviousData } = options2;
371
- const storeState = store.getState();
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) revalidate(store, variable, false);
408
- }, [store, revalidateOnMount]);
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.getState(), store.subscribe);
519
+ const useStore = () => useStoreState(store);
487
520
  const execute = (variable) => {
488
521
  let currentResolveFn;
489
522
  const stateBeforeExecute = store.getState();
@@ -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
@@ -2,7 +2,10 @@
2
2
  "name": "floppy-disk",
3
3
  "description": "Lightweight, simple, and powerful state management library",
4
4
  "private": false,
5
- "version": "3.0.1",
5
+ "version": "3.1.0-beta.1",
6
+ "publishConfig": {
7
+ "tag": "beta"
8
+ },
6
9
  "keywords": [
7
10
  "utilities",
8
11
  "store",
@@ -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
- }) => QueryState<TData, TError>) & {
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>) => (() => TState) & import("../vanilla.ts").StoreApi<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>;
@@ -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) => (() => TState) & {
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;
@@ -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
- * - No selector or memoization is needed.
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>>(storeState: TState, subscribe: StoreApi<TState>["subscribe"]) => TState;
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 useStoreState = (storeState, subscribe) => {
55
- const [trackedState, usedPathsRef] = useStoreStateProxy(storeState);
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
- }, [subscribe]);
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.getState(), store.subscribe);
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.getState(), store.subscribe);
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 { revalidateOnMount = true, keepPreviousData } = options2;
373
- const storeState = store.getState();
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) revalidate(store, variable, false);
410
- }, [store, revalidateOnMount]);
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.getState(), store.subscribe);
521
+ const useStore = () => useStoreState(store);
489
522
  const execute = (variable) => {
490
523
  let currentResolveFn;
491
524
  const stateBeforeExecute = store.getState();
@@ -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 = {