floppy-disk 3.0.0-beta.1 → 3.0.0-beta.2

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.
@@ -14,7 +14,7 @@ import { type InitStoreOptions, type SetState } from 'floppy-disk/vanilla';
14
14
  * - No retry mechanism
15
15
  * - No caching across executions
16
16
  */
17
- export type MutationState<TData, TVariable> = {
17
+ export type MutationState<TData, TVariable, TError> = {
18
18
  isPending: boolean;
19
19
  } & ({
20
20
  state: 'INITIAL';
@@ -41,7 +41,7 @@ export type MutationState<TData, TVariable> = {
41
41
  variable: TVariable;
42
42
  data: undefined;
43
43
  dataUpdatedAt: undefined;
44
- error: any;
44
+ error: TError;
45
45
  errorUpdatedAt: number;
46
46
  });
47
47
  /**
@@ -50,19 +50,19 @@ export type MutationState<TData, TVariable> = {
50
50
  * @remarks
51
51
  * Lifecycle callbacks are triggered for each execution.
52
52
  */
53
- export type MutationOptions<TData, TVariable> = InitStoreOptions<MutationState<TData, TVariable>> & {
53
+ export type MutationOptions<TData, TVariable, TError = Error> = InitStoreOptions<MutationState<TData, TVariable, TError>> & {
54
54
  /**
55
55
  * Called when the mutation succeeds.
56
56
  */
57
- onSuccess?: (data: TData, variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable>) => void;
57
+ onSuccess?: (data: TData, variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable, TError>) => void;
58
58
  /**
59
59
  * Called when the mutation fails.
60
60
  */
61
- onError?: (error: any, variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable>) => void;
61
+ onError?: (error: TError, variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable, TError>) => void;
62
62
  /**
63
63
  * Called after the mutation settles (either success or error).
64
64
  */
65
- onSettled?: (variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable>) => void;
65
+ onSettled?: (variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable, TError>) => void;
66
66
  };
67
67
  /**
68
68
  * Creates a mutation store for handling async operations that modify data.
@@ -89,10 +89,10 @@ 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>) => (<TStateSlice = MutationState<TData, TVariable>>(selector?: (state: MutationState<TData, TVariable>) => TStateSlice) => TStateSlice) & {
93
- subscribe: (subscriber: import("../vanilla.d.mts").Subscriber<MutationState<TData, TVariable>>) => () => void;
94
- getSubscribers: () => Set<import("../vanilla.d.mts").Subscriber<MutationState<TData, TVariable>>>;
95
- getState: () => MutationState<TData, TVariable>;
92
+ export declare const createMutation: <TData, TVariable = undefined, TError = Error>(mutationFn: (variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable, TError>) => Promise<TData>, options?: MutationOptions<TData, TVariable, TError>) => (() => MutationState<TData, TVariable, TError>) & {
93
+ subscribe: (subscriber: import("../vanilla.d.mts").Subscriber<MutationState<TData, TVariable, TError>>) => () => void;
94
+ getSubscribers: () => Set<import("../vanilla.d.mts").Subscriber<MutationState<TData, TVariable, TError>>>;
95
+ getState: () => MutationState<TData, TVariable, TError>;
96
96
  /**
97
97
  * Manually updates the mutation state.
98
98
  *
@@ -100,7 +100,7 @@ export declare const createMutation: <TData, TVariable = undefined>(mutationFn:
100
100
  * - Intended for advanced use cases.
101
101
  * - Prefer using provided mutation actions (`execute`, `reset`) instead.
102
102
  */
103
- setState: (value: SetState<MutationState<TData, TVariable>>) => void;
103
+ setState: (value: SetState<MutationState<TData, TVariable, TError>>) => void;
104
104
  /**
105
105
  * Executes the mutation.
106
106
  *
@@ -118,11 +118,11 @@ export declare const createMutation: <TData, TVariable = undefined>(mutationFn:
118
118
  execute: TVariable extends undefined ? () => Promise<{
119
119
  variable: undefined;
120
120
  data?: TData;
121
- error?: any;
121
+ error?: TError;
122
122
  }> : (variable: TVariable) => Promise<{
123
123
  variable: TVariable;
124
124
  data?: TData;
125
- error?: any;
125
+ error?: TError;
126
126
  }>;
127
127
  /**
128
128
  * Resets the mutation state back to its initial state.
@@ -19,7 +19,7 @@ import { type InitStoreOptions, type SetState } from 'floppy-disk/vanilla';
19
19
  * @remarks
20
20
  * - Data and error are mutually exclusive except in `SUCCESS_BUT_REVALIDATION_ERROR`.
21
21
  */
22
- export type QueryState<TData> = {
22
+ export type QueryState<TData, TError> = {
23
23
  isPending: boolean;
24
24
  isRevalidating: boolean;
25
25
  isRetrying: boolean;
@@ -46,7 +46,7 @@ export type QueryState<TData> = {
46
46
  isError: true;
47
47
  data: undefined;
48
48
  dataUpdatedAt: undefined;
49
- error: any;
49
+ error: TError;
50
50
  errorUpdatedAt: number;
51
51
  } | {
52
52
  state: 'SUCCESS_BUT_REVALIDATION_ERROR';
@@ -54,7 +54,7 @@ export type QueryState<TData> = {
54
54
  isError: false;
55
55
  data: TData;
56
56
  dataUpdatedAt: number;
57
- error: any;
57
+ error: TError;
58
58
  errorUpdatedAt: number;
59
59
  });
60
60
  /**
@@ -63,13 +63,13 @@ export type QueryState<TData> = {
63
63
  * @remarks
64
64
  * Controls caching, retry behavior, lifecycle, and side effects of an async operation.
65
65
  */
66
- export type QueryOptions<TData, TVariable extends Record<string, any>> = InitStoreOptions<QueryState<TData>> & {
66
+ export type QueryOptions<TData, TVariable extends Record<string, any>, TError = Error> = InitStoreOptions<QueryState<TData, TError>> & {
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 minutes)
72
+ * @default 2500 ms (2.5 seconds)
73
73
  */
74
74
  staleTime?: number;
75
75
  /**
@@ -95,15 +95,15 @@ export type QueryOptions<TData, TVariable extends Record<string, any>> = InitSto
95
95
  /**
96
96
  * Called when the query succeeds.
97
97
  */
98
- onSuccess?: (data: TData, variable: TVariable, stateBeforeExecute: QueryState<TData>) => void;
98
+ onSuccess?: (data: TData, variable: TVariable, stateBeforeExecute: QueryState<TData, TError>) => void;
99
99
  /**
100
100
  * Called when the query fails and will not retry.
101
101
  */
102
- onError?: (error: any, variable: TVariable, stateBeforeExecute: QueryState<TData>) => void;
102
+ onError?: (error: TError, variable: TVariable, stateBeforeExecute: QueryState<TData, TError>) => void;
103
103
  /**
104
104
  * Called after the query settles (success or final failure).
105
105
  */
106
- onSettled?: (variable: TVariable, stateBeforeExecute: QueryState<TData>) => void;
106
+ onSettled?: (variable: TVariable, stateBeforeExecute: QueryState<TData, TError>) => void;
107
107
  /**
108
108
  * Determines whether a failed query should retry.
109
109
  *
@@ -120,7 +120,7 @@ export type QueryOptions<TData, TVariable extends Record<string, any>> = InitSto
120
120
  * return [false];
121
121
  * }
122
122
  */
123
- shouldRetry?: (error: any, currentState: QueryState<TData>) => [true, number] | [false];
123
+ shouldRetry?: (error: TError, currentState: QueryState<TData, TError>) => [true, number] | [false];
124
124
  };
125
125
  /**
126
126
  * Creates a query factory for managing cached async operations.
@@ -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 cached by a deterministic key derived from `variable`.
134
+ * - Queries are **keyed by variable** (via deterministic hashing).
135
135
  * - Each unique variable maps to its own store instance.
136
- * - Queries support:
137
- * - Caching with `staleTime`
138
- * - Explicit invalidation independent of freshness
139
- * - Automatic garbage collection (`gcTime`)
140
- * - Retry logic via `shouldRetry`
141
- * - Background revalidation (focus / reconnect)
142
- * - Execution is deduplicated: multiple calls share the same in-flight promise.
143
- * - Ongoing executions can be optionally overwritten.
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,39 +157,36 @@ 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) => {
157
- <TStateSlice = QueryState<TData>>(options?: {
158
- /**
159
- * Whether the query should execute automatically on mount.
160
- *
161
- * @default true
162
- */
163
- enabled?: boolean;
164
- /**
165
- * Whether to keep previous successful data while a new variable is loading.
166
- *
167
- * @remarks
168
- * - Only applies when the query is in the `INITIAL` state (no data & no error).
169
- * - Intended for variable changes:
170
- * when switching from one variable to another, the previous data is temporarily shown
171
- * while the new execution is in progress.
172
- * - Once the new execution resolves (success or error), the previous data is no longer used.
173
- * - Prevents UI flicker (e.g. empty/loading state) during transitions.
174
- *
175
- * @example
176
- * // Switching from userId=1 userId=2
177
- * // While loading userId=2, still show userId=1 data
178
- * useQuery({ id: userId }, { keepPreviousData: true });
179
- */ keepPreviousData?: boolean;
180
- }, selector?: (state: QueryState<TData>) => TStateSlice): TStateSlice;
181
- <TStateSlice = QueryState<TData>>(selector?: (state: QueryState<TData>) => TStateSlice): TStateSlice;
182
- } & {
160
+ export declare const createQuery: <TData, TVariable extends Record<string, any> = never, TError = Error>(queryFn: (variable: TVariable, currentState: QueryState<TData, TError>) => Promise<TData>, options?: QueryOptions<TData, TVariable, TError>) => ((variable?: TVariable) => ((options?: {
161
+ /**
162
+ * Whether the query should execute automatically on mount.
163
+ *
164
+ * @default true
165
+ */
166
+ enabled?: boolean;
167
+ /**
168
+ * Whether to keep previous successful data while a new variable is loading.
169
+ *
170
+ * @remarks
171
+ * - Only applies when the query is in the `INITIAL` state (no data & no error).
172
+ * - Intended for variable changes:
173
+ * when switching from one variable to another, the previous data is temporarily shown
174
+ * while the new execution is in progress.
175
+ * - Once the new execution resolves (success or error), the previous data is no longer used.
176
+ * - Prevents UI flicker (e.g. empty/loading state) during transitions.
177
+ *
178
+ * @example
179
+ * // Switching from userId=1 → userId=2
180
+ * // While loading userId=2, still show userId=1 data
181
+ * useQuery({ id: userId }, { keepPreviousData: true });
182
+ */ keepPreviousData?: boolean;
183
+ }) => QueryState<TData, TError>) & {
183
184
  metadata: {
184
185
  isInvalidated?: boolean;
185
- promise?: Promise<QueryState<TData>> | undefined;
186
- promiseResolver?: ((value: QueryState<TData> | PromiseLike<QueryState<TData>>) => void) | undefined;
186
+ promise?: Promise<QueryState<TData, TError>> | undefined;
187
+ promiseResolver?: ((value: QueryState<TData, TError> | PromiseLike<QueryState<TData, TError>>) => void) | undefined;
187
188
  retryTimeoutId?: number;
188
- retryResolver?: ((value: QueryState<TData> | PromiseLike<QueryState<TData>>) => void) | undefined;
189
+ retryResolver?: ((value: QueryState<TData, TError> | PromiseLike<QueryState<TData, TError>>) => void) | undefined;
189
190
  garbageCollectionTimeoutId?: number;
190
191
  rollbackData?: TData | undefined;
191
192
  };
@@ -211,7 +212,7 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
211
212
  * @returns A promise resolving to the latest query state
212
213
  *
213
214
  * @remarks
214
- * - 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.
215
216
  * - Set `overwriteOngoingExecution: false` to reuse an ongoing execution (deduplication).
216
217
  * - Handles:
217
218
  * - Pending state
@@ -221,7 +222,7 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
221
222
  */
222
223
  execute: (options?: {
223
224
  overwriteOngoingExecution?: boolean;
224
- }) => Promise<QueryState<TData>>;
225
+ }) => Promise<QueryState<TData, TError>>;
225
226
  /**
226
227
  * Re-executes the query if needed based on freshness or invalidation.
227
228
  *
@@ -237,7 +238,7 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
237
238
  */
238
239
  revalidate: (options?: {
239
240
  overwriteOngoingExecution?: boolean;
240
- }) => Promise<QueryState<TData>>;
241
+ }) => Promise<QueryState<TData, TError>>;
241
242
  /**
242
243
  * Marks the query as invalidated and optionally triggers re-execution.
243
244
  *
@@ -289,7 +290,7 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
289
290
  * const { rollback, revalidate } = query.optimisticUpdate(newData);
290
291
  */
291
292
  optimisticUpdate: (data: TData) => {
292
- revalidate: () => Promise<QueryState<TData>>;
293
+ revalidate: () => Promise<QueryState<TData, TError>>;
293
294
  rollback: () => TData;
294
295
  };
295
296
  /**
@@ -301,10 +302,10 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
301
302
  * - Should be used if an optimistic update fails.
302
303
  */
303
304
  rollbackOptimisticUpdate: () => TData;
304
- subscribe: (subscriber: import("../vanilla.d.mts").Subscriber<QueryState<TData>>) => () => void;
305
- getSubscribers: () => Set<import("../vanilla.d.mts").Subscriber<QueryState<TData>>>;
306
- getState: () => QueryState<TData>;
307
- setState: (value: SetState<QueryState<TData>>) => void;
305
+ subscribe: (subscriber: import("../vanilla.d.mts").Subscriber<QueryState<TData, TError>>) => () => void;
306
+ getSubscribers: () => Set<import("../vanilla.d.mts").Subscriber<QueryState<TData, TError>>>;
307
+ getState: () => QueryState<TData, TError>;
308
+ setState: (value: SetState<QueryState<TData, TError>>) => void;
308
309
  }) & {
309
310
  /**
310
311
  * Executes all query instances.
@@ -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 useCounter = createStore({ count: 0 });
19
+ * const useMyStore = createStore({ foo: 1, bar: 2 });
18
20
  *
19
21
  * function Component() {
20
- * const count = useCounter((s) => s.count);
22
+ * const state = useMyStore();
23
+ * return <div>{state.foo}</div>;
21
24
  * }
22
25
  *
23
- * useCounter.setState({ count: 1 });
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>) => (<TStateSlice = TState>(selector?: (state: TState) => TStateSlice) => TStateSlice) & import("../vanilla.d.mts").StoreApi<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 getUserStore = createStores({ name: '' });
25
+ * const userStore = createStores<{ name: string }, { id: number }>({ name: '' });
22
26
  *
23
- * const userStore = getUserStore({ id: 1 });
24
- * const name = userStore((s) => s.name);
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) => (<TStateSlice = TState>(selector?: (state: TState) => TStateSlice) => TStateSlice) & {
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
- export declare const useStoreUpdateNotifier: <TState extends Record<string, any>, TStateSlice = TState>(store: StoreApi<TState>, selector: (state: TState) => TStateSlice) => void;
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 with optional state selection.
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 The selected state slice (or full state if no selector is provided)
12
+ * @returns A proxied version of the store state
10
13
  *
11
14
  * @remarks
12
- * - The selector does **not** need to be memoized.
13
- * - The hook internally keeps the latest selector reference to avoid re-subscription.
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 count = useStoreState(store, (s) => s.count);
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>, TStateSlice = TState>(store: StoreApi<TState>, selector?: (state: TState) => TStateSlice) => TStateSlice;
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 * from './react/use-store.mjs';
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, identity, shallow, initStore, getHash, noop } from 'floppy-disk/vanilla';
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 useStoreUpdateNotifier = (store, selector) => {
7
- const [, reRender] = useState({});
8
- const selectorRef = useRef(selector);
9
- selectorRef.current = selector;
10
- useIsomorphicLayoutEffect(
11
- () => store.subscribe((state, prevState) => {
12
- if (selectorRef.current === identity) return reRender({});
13
- const prevSlice = selectorRef.current(prevState);
14
- const nextSlice = selectorRef.current(state);
15
- if (!shallow(prevSlice, nextSlice)) reRender({});
16
- }),
17
- [store]
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 useStoreState = (store, selector = identity) => {
21
- useStoreUpdateNotifier(store, selector);
22
- return selector(store.getState());
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 = (selector) => useStoreState(store, selector);
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 = (selector) => {
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,30 +366,51 @@ const createQuery = (queryFn, options = {}) => {
323
366
  stores.set(variableHash, store);
324
367
  internals.set(store, configureInternals(store, variable, variableHash));
325
368
  }
326
- function useStore(optionsOrSelector = {}, maybeSelector) {
327
- let selector;
328
- let options2;
329
- if (typeof optionsOrSelector === "function") {
330
- options2 = {};
331
- selector = optionsOrSelector;
332
- } else {
333
- options2 = optionsOrSelector;
334
- selector = maybeSelector || identity;
335
- }
336
- useStoreUpdateNotifier(store, selector);
337
- useIsomorphicLayoutEffect(() => {
338
- if (options2.enabled !== false) revalidate(store, variable, false);
339
- }, [store, options2.enabled]);
369
+ const useStore = (options2 = {}) => {
370
+ const { enabled = true, keepPreviousData } = options2;
340
371
  const storeState = store.getState();
341
- let storeStateToBeUsed = storeState;
342
372
  const prevState = useRef({});
343
- if (storeState.isSuccess) {
344
- prevState.current = { data: storeState.data, dataUpdatedAt: storeState.dataUpdatedAt };
345
- } else if (storeState.state === "INITIAL" && options2.keepPreviousData) {
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) {
346
380
  storeStateToBeUsed = { ...storeState, ...prevState.current };
347
381
  }
348
- return selector(storeStateToBeUsed);
349
- }
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;
413
+ };
350
414
  return Object.assign(useStore, {
351
415
  subscribe: store.subscribe,
352
416
  getSubscribers: store.getSubscribers,
@@ -417,7 +481,7 @@ const createMutation = (mutationFn, options = {}) => {
417
481
  const { onSuccess = noop, onError, onSettled = noop } = options;
418
482
  const initialState = INITIAL_STATE;
419
483
  const store = initStore(initialState, options);
420
- const useStore = (selector) => useStoreState(store, selector);
484
+ const useStore = () => useStoreState(store.getState(), store.subscribe);
421
485
  const execute = (variable) => {
422
486
  const stateBeforeExecute = store.getState();
423
487
  if (stateBeforeExecute.isPending) {
@@ -509,4 +573,4 @@ const createMutation = (mutationFn, options = {}) => {
509
573
  });
510
574
  };
511
575
 
512
- export { createMutation, createQuery, createStore, createStores, useIsomorphicLayoutEffect, useStoreState, useStoreUpdateNotifier };
576
+ export { createMutation, createQuery, createStore, createStores, useIsomorphicLayoutEffect, useStoreState };
@@ -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.