floppy-disk 3.0.0 → 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 +88 -55
- package/esm/react/create-query.d.mts +27 -1
- package/esm/react/create-store.d.mts +6 -1
- package/esm/react/create-stores.d.mts +8 -1
- package/esm/react/use-store.d.mts +8 -2
- package/esm/react.mjs +44 -11
- package/esm/vanilla/store.d.mts +6 -0
- package/esm/vanilla.mjs +2 -0
- package/package.json +4 -1
- package/react/create-query.d.ts +27 -1
- package/react/create-store.d.ts +6 -1
- package/react/create-stores.d.ts +8 -1
- package/react/use-store.d.ts +8 -2
- package/react.js +44 -11
- package/vanilla/store.d.ts +6 -0
- package/vanilla.js +2 -0
package/README.md
CHANGED
|
@@ -5,8 +5,6 @@ A lightweight, simple, and powerful state management library.
|
|
|
5
5
|
This library was highly-inspired by [Zustand](https://www.npmjs.com/package/zustand) and [TanStack-Query](https://tanstack.com/query), they're awesome state manager.
|
|
6
6
|
FloppyDisk provides a very similar developer experience (DX), while introducing additional features and a smaller bundle size.
|
|
7
7
|
|
|
8
|
-
Comparison: https://github.com/afiiif/floppy-disk/tree/beta/comparison
|
|
9
|
-
|
|
10
8
|
Demo: https://afiiif.github.io/floppy-disk/
|
|
11
9
|
|
|
12
10
|
**Installation:**
|
|
@@ -23,7 +21,7 @@ Here's how to create and use a store:
|
|
|
23
21
|
import { createStore } from 'floppy-disk/react';
|
|
24
22
|
|
|
25
23
|
const useDigimon = createStore({
|
|
26
|
-
age:
|
|
24
|
+
age: 7,
|
|
27
25
|
level: 'Rookie',
|
|
28
26
|
});
|
|
29
27
|
```
|
|
@@ -41,10 +39,12 @@ function MyDigimon() {
|
|
|
41
39
|
function Control() {
|
|
42
40
|
return (
|
|
43
41
|
<>
|
|
44
|
-
<button
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
<button
|
|
43
|
+
onClick={() => {
|
|
44
|
+
// You can setState directly
|
|
45
|
+
useDigimon.setState((prev) => ({ age: prev.age + 1 }));
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
48
|
Increase digimon's age
|
|
49
49
|
</button>
|
|
50
50
|
|
|
@@ -66,6 +66,43 @@ const evolve = () => {
|
|
|
66
66
|
};
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
### Store Subscription
|
|
70
|
+
|
|
71
|
+
At its core, FloppyDisk is a **pub-sub store**.
|
|
72
|
+
|
|
73
|
+
You can subscribe manually:
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
const unsubscribe = useMyStore.subscribe((state, prev) => {
|
|
77
|
+
console.log('New state:', state);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Later
|
|
81
|
+
unsubscribe();
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
FloppyDisk provides lifecycle hooks tied to subscription count.
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
const useTowerDefense = createStore(
|
|
88
|
+
{ archers: 3, mages: 1, barracks: 2, artillery: 1 },
|
|
89
|
+
{
|
|
90
|
+
onFirstSubscribe: () => {
|
|
91
|
+
console.log('First subscriber! We’re officially popular 🎉');
|
|
92
|
+
},
|
|
93
|
+
onSubscribe: () => {
|
|
94
|
+
console.log('New subscriber joined. Welcome aboard 🫡');
|
|
95
|
+
},
|
|
96
|
+
onUnsubscribe: () => {
|
|
97
|
+
console.log('Subscriber left... was it something I said? 😭');
|
|
98
|
+
},
|
|
99
|
+
onLastUnsubscribe: () => {
|
|
100
|
+
console.log('Everyone left. Guess I’ll just exist quietly now...');
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
```
|
|
105
|
+
|
|
69
106
|
### Differences from Zustand
|
|
70
107
|
|
|
71
108
|
If you're coming from Zustand, this should feel very familiar.\
|
|
@@ -80,28 +117,28 @@ Key differences:
|
|
|
80
117
|
Zustand examples:
|
|
81
118
|
|
|
82
119
|
```tsx
|
|
83
|
-
const
|
|
120
|
+
const useDate = create(new Date(2021, 01, 11));
|
|
84
121
|
|
|
85
|
-
const
|
|
122
|
+
const useCounter = create((set) => ({
|
|
86
123
|
value: 1,
|
|
87
|
-
|
|
124
|
+
increment: () => set((prev) => ({ value: prev.value + 1 })),
|
|
88
125
|
}));
|
|
89
126
|
```
|
|
90
127
|
|
|
91
128
|
FloppyDisk equivalents:
|
|
92
129
|
|
|
93
130
|
```tsx
|
|
94
|
-
const
|
|
131
|
+
const useDate = createStore({ value: new Date(2021, 01, 11) });
|
|
95
132
|
|
|
133
|
+
const useCounter = createStore({ value: 1 });
|
|
134
|
+
const increment = () => useCounter.setState((prev) => ({ value: prev.value + 1 }));
|
|
96
135
|
// Unlike Zustand, defining actions inside the store is **discouraged** in FloppyDisk.
|
|
97
136
|
// This improves tree-shakeability and keeps your store minimal.
|
|
98
|
-
const useExample2 = createStore({ value: 1 });
|
|
99
|
-
const inc = () => useExample2.setState(prev => ({ value: prev.value + 1 }));
|
|
100
137
|
|
|
101
|
-
// However, it's still possible if you understand how closures work:
|
|
102
|
-
const
|
|
138
|
+
// However, it's still possible to mix actions with the state if you understand how closures work:
|
|
139
|
+
const useCounterAlt = createStore({
|
|
103
140
|
value: 1,
|
|
104
|
-
|
|
141
|
+
increment: () => useCounterAlt.setState((prev) => ({ value: prev.value + 1 })),
|
|
105
142
|
});
|
|
106
143
|
```
|
|
107
144
|
|
|
@@ -272,14 +309,11 @@ type GetPostsResponse = {
|
|
|
272
309
|
meta: { nextCursor: string };
|
|
273
310
|
};
|
|
274
311
|
|
|
275
|
-
const postsQuery = createQuery<GetPostsResponse, GetPostParams>(
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
revalidateOnReconnect: false,
|
|
281
|
-
},
|
|
282
|
-
);
|
|
312
|
+
const postsQuery = createQuery<GetPostsResponse, GetPostParams>(getPosts, {
|
|
313
|
+
staleTime: Infinity,
|
|
314
|
+
revalidateOnFocus: false,
|
|
315
|
+
revalidateOnReconnect: false,
|
|
316
|
+
});
|
|
283
317
|
|
|
284
318
|
function Main() {
|
|
285
319
|
return <Page cursor={undefined} />;
|
|
@@ -294,12 +328,10 @@ function Page({ cursor }: { cursor?: string }) {
|
|
|
294
328
|
|
|
295
329
|
return (
|
|
296
330
|
<>
|
|
297
|
-
{data.posts.map(post => (
|
|
331
|
+
{data.posts.map((post) => (
|
|
298
332
|
<PostCard key={post.id} post={post} />
|
|
299
333
|
))}
|
|
300
|
-
{data.meta.nextCursor &&
|
|
301
|
-
<LoadMore nextCursor={data.meta.nextCursor} />
|
|
302
|
-
)}
|
|
334
|
+
{data.meta.nextCursor && <LoadMore nextCursor={data.meta.nextCursor} />}
|
|
303
335
|
</>
|
|
304
336
|
);
|
|
305
337
|
}
|
|
@@ -314,11 +346,7 @@ function LoadMore({ nextCursor }: { nextCursor?: string }) {
|
|
|
314
346
|
return <Page cursor={nextCursor} />;
|
|
315
347
|
}
|
|
316
348
|
|
|
317
|
-
return (
|
|
318
|
-
<ReachingBottomObserver
|
|
319
|
-
onReachBottom={() => setIsNextPageRequested(true)}
|
|
320
|
-
/>
|
|
321
|
-
);
|
|
349
|
+
return <BottomObserver onReachBottom={() => setIsNextPageRequested(true)} />;
|
|
322
350
|
}
|
|
323
351
|
```
|
|
324
352
|
|
|
@@ -337,34 +365,39 @@ Revalidating dozens of previously viewed pages rarely provides value to the user
|
|
|
337
365
|
|
|
338
366
|
## SSR Guidance
|
|
339
367
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
If your data is already fetched on the server (e.g. via SSR/ISR, Server Components, or Server Actions), then:
|
|
368
|
+
Examples for using stores and queries in SSR with isolated data (no shared state between users).
|
|
343
369
|
|
|
344
|
-
|
|
370
|
+
### Initialize Store State from Server
|
|
345
371
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
In many cases, developers mix SSR/ISR with client-side state because they want:
|
|
349
|
-
|
|
350
|
-
1. Data to be rendered into HTML on the server
|
|
351
|
-
2. The ability to **revalidate it on the client**
|
|
352
|
-
|
|
353
|
-
A common (but inefficient) approach is:
|
|
372
|
+
```tsx
|
|
373
|
+
const useCountStore = createStore({ count: 0 });
|
|
354
374
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
375
|
+
function Page({ initialCount }) {
|
|
376
|
+
const { count } = useCountStore({
|
|
377
|
+
initialState: { count: initialCount }, // e.g. 3
|
|
378
|
+
});
|
|
358
379
|
|
|
359
|
-
|
|
380
|
+
return <>count is {count}</>; // Output: count is 3
|
|
381
|
+
}
|
|
382
|
+
```
|
|
360
383
|
|
|
361
|
-
|
|
384
|
+
### Initialize Query Data from Server
|
|
362
385
|
|
|
363
|
-
|
|
386
|
+
```tsx
|
|
387
|
+
async function MyServerComponent() {
|
|
388
|
+
const data = await getData(); // e.g. { count: 3 }
|
|
389
|
+
return <MyClientComponent initialData={data} />;
|
|
390
|
+
}
|
|
364
391
|
|
|
365
|
-
|
|
392
|
+
const myQuery = createQuery(getData);
|
|
393
|
+
const useMyQuery = myQuery();
|
|
366
394
|
|
|
367
|
-
|
|
395
|
+
function MyClientComponent({ initialData }) {
|
|
396
|
+
const { data } = useMyQuery({
|
|
397
|
+
initialData: initialData,
|
|
398
|
+
// initialDataIsStale: true <-- Optional, default to false (no immediate revalidation)
|
|
399
|
+
});
|
|
368
400
|
|
|
369
|
-
|
|
370
|
-
|
|
401
|
+
return <>count is {data.count}</>; // Output: count is 3
|
|
402
|
+
}
|
|
403
|
+
```
|
|
@@ -182,7 +182,33 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
|
|
|
182
182
|
* // While loading userId=2, still show userId=1 data
|
|
183
183
|
* useQuery({ id: userId }, { keepPreviousData: true });
|
|
184
184
|
*/ keepPreviousData?: boolean;
|
|
185
|
-
}
|
|
185
|
+
} & ({
|
|
186
|
+
/**
|
|
187
|
+
* Initial data used on first render (and will also update the store state right after that)
|
|
188
|
+
*
|
|
189
|
+
* If provided, `initialData` will be applied **once per query-store instance**
|
|
190
|
+
*/
|
|
191
|
+
initialData: TData;
|
|
192
|
+
/**
|
|
193
|
+
* Whether the provided `initialData` should be treated as stale.
|
|
194
|
+
*
|
|
195
|
+
* @remarks
|
|
196
|
+
* - If `true`, a revalidation (refetch) will be triggered immediately.
|
|
197
|
+
* - If `false` (default), `initialData` is treated as fresh and will not be revalidated.
|
|
198
|
+
*
|
|
199
|
+
* @default false
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* useQuery({
|
|
203
|
+
* initialData: dataFromSSR,
|
|
204
|
+
* initialDataIsStale: true, // force refetch
|
|
205
|
+
* });
|
|
206
|
+
*/
|
|
207
|
+
initialDataIsStale?: boolean;
|
|
208
|
+
} | {
|
|
209
|
+
initialData?: never;
|
|
210
|
+
initialDataIsStale?: never;
|
|
211
|
+
})) => QueryState<TData, TError>) & {
|
|
186
212
|
metadata: {
|
|
187
213
|
isInvalidated?: boolean;
|
|
188
214
|
promise?: Promise<QueryState<TData, TError>> | undefined;
|
|
@@ -25,4 +25,9 @@ import { type InitStoreOptions } from 'floppy-disk/vanilla';
|
|
|
25
25
|
*
|
|
26
26
|
* useMyStore.setState({ foo: 2 }); // only components using foo will re-render
|
|
27
27
|
*/
|
|
28
|
-
export declare const createStore: <TState extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => ((
|
|
28
|
+
export declare const createStore: <TState extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => ((options?: {
|
|
29
|
+
/**
|
|
30
|
+
* Initial state used on first render (and will also update the store state right after that)
|
|
31
|
+
*/
|
|
32
|
+
initialState?: Partial<TState>;
|
|
33
|
+
}) => TState) & import("../vanilla.d.mts").StoreApi<TState>;
|
|
@@ -30,7 +30,14 @@ import { type InitStoreOptions } from 'floppy-disk/vanilla';
|
|
|
30
30
|
* return <div>{state.name}</div>;
|
|
31
31
|
* }
|
|
32
32
|
*/
|
|
33
|
-
export declare const createStores: <TState extends Record<string, any>, TKey extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => (key?: TKey) => ((
|
|
33
|
+
export declare const createStores: <TState extends Record<string, any>, TKey extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => (key?: TKey) => ((options?: {
|
|
34
|
+
/**
|
|
35
|
+
* Initial state used on first render (and will also update the store state right after that)
|
|
36
|
+
*
|
|
37
|
+
* If provided, `initialState` will be applied **once per store instance**
|
|
38
|
+
*/
|
|
39
|
+
initialState?: Partial<TState>;
|
|
40
|
+
}) => TState) & {
|
|
34
41
|
delete: () => boolean;
|
|
35
42
|
setState: (value: import("../vanilla.d.mts").SetState<TState>) => void;
|
|
36
43
|
getState: () => TState;
|
|
@@ -4,10 +4,14 @@ export declare const getValueByPath: (obj: any, path: Path) => any;
|
|
|
4
4
|
export declare const isPrefixPath: (candidatePrefix: Path, targetPath: Path) => boolean;
|
|
5
5
|
export declare const compressPaths: (paths: Path[]) => Path[];
|
|
6
6
|
export declare const useStoreStateProxy: <TState extends Record<string, any>>(storeState: TState) => readonly [TState, import("react").RefObject<Path[]>];
|
|
7
|
+
export declare const NO_INITIAL_VALUE: {};
|
|
8
|
+
export declare const useStoreStateWithInitializer: <TState extends Record<string, any>>(store: StoreApi<TState>, initialState?: Partial<TState>) => readonly [TState, import("react").RefObject<WeakMap<StoreApi<TState>, number>>];
|
|
7
9
|
/**
|
|
8
10
|
* React hook for subscribing to a store using automatic dependency tracking.
|
|
9
11
|
*
|
|
10
12
|
* @param store - The store instance to subscribe to
|
|
13
|
+
* @param options - Optional configuration
|
|
14
|
+
* @param options.initialState - Initial state used on first render (and will also update the store state right after that)
|
|
11
15
|
*
|
|
12
16
|
* @returns A proxied version of the store state
|
|
13
17
|
*
|
|
@@ -17,12 +21,14 @@ export declare const useStoreStateProxy: <TState extends Record<string, any>>(st
|
|
|
17
21
|
* - State must be treated as **immutable**:
|
|
18
22
|
* - Updates must replace objects rather than mutate them
|
|
19
23
|
* - Otherwise, changes may not be detected
|
|
20
|
-
* -
|
|
24
|
+
* - If provided, `initialState` will be applied **once per store instance**
|
|
21
25
|
*
|
|
22
26
|
* @example
|
|
23
27
|
* const state = useStoreState(store);
|
|
24
28
|
* return <div>{state.user.name}</div>;
|
|
25
29
|
* // Component will only re-render if `user.name` changes
|
|
26
30
|
*/
|
|
27
|
-
export declare const useStoreState: <TState extends Record<string, any>>(
|
|
31
|
+
export declare const useStoreState: <TState extends Record<string, any>>(store: StoreApi<TState>, options?: {
|
|
32
|
+
initialState?: Partial<TState>;
|
|
33
|
+
}) => TState;
|
|
28
34
|
export {};
|
package/esm/react.mjs
CHANGED
|
@@ -49,11 +49,24 @@ const useStoreStateProxy = (storeState) => {
|
|
|
49
49
|
}, [storeState]);
|
|
50
50
|
return [trackedState, usedPathsRef];
|
|
51
51
|
};
|
|
52
|
-
const
|
|
53
|
-
|
|
52
|
+
const NO_INITIAL_VALUE = {};
|
|
53
|
+
const useStoreStateWithInitializer = (store, initialState = NO_INITIAL_VALUE) => {
|
|
54
|
+
const initiatedAt = useRef(new WeakMap([[store, 0]]));
|
|
55
|
+
useIsomorphicLayoutEffect(() => {
|
|
56
|
+
if (initialState === NO_INITIAL_VALUE || initiatedAt.current.get(store)) return;
|
|
57
|
+
store.setState(initialState);
|
|
58
|
+
initiatedAt.current.set(store, Date.now());
|
|
59
|
+
}, [store, initialState]);
|
|
60
|
+
const storeState = store.getState();
|
|
61
|
+
const finalState = initialState === NO_INITIAL_VALUE || initiatedAt.current.get(store) ? storeState : { ...storeState, ...initialState };
|
|
62
|
+
return [finalState, initiatedAt];
|
|
63
|
+
};
|
|
64
|
+
const useStoreState = (store, options = {}) => {
|
|
65
|
+
const [state] = useStoreStateWithInitializer(store, options.initialState);
|
|
66
|
+
const [trackedState, usedPathsRef] = useStoreStateProxy(state);
|
|
54
67
|
const [, reRender] = useState({});
|
|
55
68
|
useIsomorphicLayoutEffect(() => {
|
|
56
|
-
return subscribe((nextState, prevState, changedKeys) => {
|
|
69
|
+
return store.subscribe((nextState, prevState, changedKeys) => {
|
|
57
70
|
const paths = compressPaths(usedPathsRef.current);
|
|
58
71
|
for (const path of paths) {
|
|
59
72
|
const rootKey = path[0];
|
|
@@ -63,13 +76,13 @@ const useStoreState = (storeState, subscribe) => {
|
|
|
63
76
|
if (!Object.is(prevVal, nextVal)) return reRender({});
|
|
64
77
|
}
|
|
65
78
|
});
|
|
66
|
-
}, [
|
|
79
|
+
}, [store]);
|
|
67
80
|
return trackedState;
|
|
68
81
|
};
|
|
69
82
|
|
|
70
83
|
const createStore = (initialState, options) => {
|
|
71
84
|
const store = initStore(initialState, options);
|
|
72
|
-
const useStore = () => useStoreState(store
|
|
85
|
+
const useStore = (options2) => useStoreState(store, options2);
|
|
73
86
|
return Object.assign(useStore, store);
|
|
74
87
|
};
|
|
75
88
|
|
|
@@ -84,7 +97,7 @@ const createStores = (initialState, options) => {
|
|
|
84
97
|
store = initStore(initialState, options);
|
|
85
98
|
stores.set(keyHash, store);
|
|
86
99
|
}
|
|
87
|
-
const useStore = () => useStoreState(store
|
|
100
|
+
const useStore = (options2) => useStoreState(store, options2);
|
|
88
101
|
return Object.assign(useStore, {
|
|
89
102
|
...store,
|
|
90
103
|
delete: () => {
|
|
@@ -367,8 +380,20 @@ const createQuery = (queryFn, options = {}) => {
|
|
|
367
380
|
internals.set(store, configureInternals(store, variable, variableHash));
|
|
368
381
|
}
|
|
369
382
|
const useStore = (options2 = {}) => {
|
|
370
|
-
const {
|
|
371
|
-
|
|
383
|
+
const {
|
|
384
|
+
initialData = NO_INITIAL_VALUE,
|
|
385
|
+
initialDataIsStale = false,
|
|
386
|
+
revalidateOnMount = true,
|
|
387
|
+
keepPreviousData
|
|
388
|
+
} = options2;
|
|
389
|
+
const [storeState, initialDataInitiatedAt] = useStoreStateWithInitializer(
|
|
390
|
+
store,
|
|
391
|
+
initialData === NO_INITIAL_VALUE ? void 0 : {
|
|
392
|
+
state: "SUCCESS",
|
|
393
|
+
isSuccess: true,
|
|
394
|
+
data: initialData
|
|
395
|
+
}
|
|
396
|
+
);
|
|
372
397
|
const prevState = useRef({});
|
|
373
398
|
let storeStateToBeUsed = storeState;
|
|
374
399
|
if (storeState.state !== "INITIAL") {
|
|
@@ -404,8 +429,16 @@ const createQuery = (queryFn, options = {}) => {
|
|
|
404
429
|
});
|
|
405
430
|
}, [store]);
|
|
406
431
|
useIsomorphicLayoutEffect(() => {
|
|
407
|
-
if (revalidateOnMount !== false)
|
|
408
|
-
|
|
432
|
+
if (revalidateOnMount !== false) {
|
|
433
|
+
if (!initialDataIsStale) {
|
|
434
|
+
const dataInitiatedAt = initialDataInitiatedAt.current.get(store);
|
|
435
|
+
if (dataInitiatedAt && dataInitiatedAt + staleTime > Date.now()) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
revalidate(store, variable, false);
|
|
440
|
+
}
|
|
441
|
+
}, [store, revalidateOnMount, initialDataIsStale]);
|
|
409
442
|
if (keepPreviousData) {
|
|
410
443
|
!!trackedState.error;
|
|
411
444
|
}
|
|
@@ -483,7 +516,7 @@ const createMutation = (mutationFn, options = {}) => {
|
|
|
483
516
|
let ongoingPromise;
|
|
484
517
|
const resolveFns = /* @__PURE__ */ new Set([]);
|
|
485
518
|
const store = initStore(initialState, options);
|
|
486
|
-
const useStore = () => useStoreState(store
|
|
519
|
+
const useStore = () => useStoreState(store);
|
|
487
520
|
const execute = (variable) => {
|
|
488
521
|
let currentResolveFn;
|
|
489
522
|
const stateBeforeExecute = store.getState();
|
package/esm/vanilla/store.d.mts
CHANGED
|
@@ -51,6 +51,12 @@ export type InitStoreOptions<TState extends Record<string, any>> = {
|
|
|
51
51
|
onSubscribe?: (state: TState, store: StoreApi<TState>) => void;
|
|
52
52
|
onUnsubscribe?: (state: TState, store: StoreApi<TState>) => void;
|
|
53
53
|
onLastUnsubscribe?: (state: TState, store: StoreApi<TState>) => void;
|
|
54
|
+
/**
|
|
55
|
+
* Called whenever the state changes, without counting as a subscriber.
|
|
56
|
+
* Acts like a "spy" on state updates.
|
|
57
|
+
* Useful for devtools, logging, or debugging state changes.
|
|
58
|
+
*/
|
|
59
|
+
onStateChange?: (state: TState, prevState: TState, changedKeys: Array<keyof TState>) => void;
|
|
54
60
|
/**
|
|
55
61
|
* By default, calling `setState` on the server is disallowed to prevent shared state across requests.
|
|
56
62
|
* Set this to `true` only if you explicitly intend to mutate state during server execution.
|
package/esm/vanilla.mjs
CHANGED
|
@@ -35,6 +35,7 @@ const initStore = (initialState, options = {}) => {
|
|
|
35
35
|
onSubscribe = noop,
|
|
36
36
|
onUnsubscribe = noop,
|
|
37
37
|
onLastUnsubscribe = noop,
|
|
38
|
+
onStateChange = noop,
|
|
38
39
|
allowSetStateServerSide = false
|
|
39
40
|
} = options;
|
|
40
41
|
const subscribers = /* @__PURE__ */ new Set();
|
|
@@ -68,6 +69,7 @@ const initStore = (initialState, options = {}) => {
|
|
|
68
69
|
}
|
|
69
70
|
if (changedKeys.length === 0) return;
|
|
70
71
|
state = { ...prevState, ...newValue };
|
|
72
|
+
onStateChange(state, prevState, changedKeys);
|
|
71
73
|
[...subscribers].forEach((subscriber) => subscriber(state, prevState, changedKeys));
|
|
72
74
|
};
|
|
73
75
|
const storeApi = {
|
package/package.json
CHANGED
package/react/create-query.d.ts
CHANGED
|
@@ -182,7 +182,33 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
|
|
|
182
182
|
* // While loading userId=2, still show userId=1 data
|
|
183
183
|
* useQuery({ id: userId }, { keepPreviousData: true });
|
|
184
184
|
*/ keepPreviousData?: boolean;
|
|
185
|
-
}
|
|
185
|
+
} & ({
|
|
186
|
+
/**
|
|
187
|
+
* Initial data used on first render (and will also update the store state right after that)
|
|
188
|
+
*
|
|
189
|
+
* If provided, `initialData` will be applied **once per query-store instance**
|
|
190
|
+
*/
|
|
191
|
+
initialData: TData;
|
|
192
|
+
/**
|
|
193
|
+
* Whether the provided `initialData` should be treated as stale.
|
|
194
|
+
*
|
|
195
|
+
* @remarks
|
|
196
|
+
* - If `true`, a revalidation (refetch) will be triggered immediately.
|
|
197
|
+
* - If `false` (default), `initialData` is treated as fresh and will not be revalidated.
|
|
198
|
+
*
|
|
199
|
+
* @default false
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* useQuery({
|
|
203
|
+
* initialData: dataFromSSR,
|
|
204
|
+
* initialDataIsStale: true, // force refetch
|
|
205
|
+
* });
|
|
206
|
+
*/
|
|
207
|
+
initialDataIsStale?: boolean;
|
|
208
|
+
} | {
|
|
209
|
+
initialData?: never;
|
|
210
|
+
initialDataIsStale?: never;
|
|
211
|
+
})) => QueryState<TData, TError>) & {
|
|
186
212
|
metadata: {
|
|
187
213
|
isInvalidated?: boolean;
|
|
188
214
|
promise?: Promise<QueryState<TData, TError>> | undefined;
|
package/react/create-store.d.ts
CHANGED
|
@@ -25,4 +25,9 @@ import { type InitStoreOptions } from 'floppy-disk/vanilla';
|
|
|
25
25
|
*
|
|
26
26
|
* useMyStore.setState({ foo: 2 }); // only components using foo will re-render
|
|
27
27
|
*/
|
|
28
|
-
export declare const createStore: <TState extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => ((
|
|
28
|
+
export declare const createStore: <TState extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => ((options?: {
|
|
29
|
+
/**
|
|
30
|
+
* Initial state used on first render (and will also update the store state right after that)
|
|
31
|
+
*/
|
|
32
|
+
initialState?: Partial<TState>;
|
|
33
|
+
}) => TState) & import("../vanilla.ts").StoreApi<TState>;
|
package/react/create-stores.d.ts
CHANGED
|
@@ -30,7 +30,14 @@ import { type InitStoreOptions } from 'floppy-disk/vanilla';
|
|
|
30
30
|
* return <div>{state.name}</div>;
|
|
31
31
|
* }
|
|
32
32
|
*/
|
|
33
|
-
export declare const createStores: <TState extends Record<string, any>, TKey extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => (key?: TKey) => ((
|
|
33
|
+
export declare const createStores: <TState extends Record<string, any>, TKey extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => (key?: TKey) => ((options?: {
|
|
34
|
+
/**
|
|
35
|
+
* Initial state used on first render (and will also update the store state right after that)
|
|
36
|
+
*
|
|
37
|
+
* If provided, `initialState` will be applied **once per store instance**
|
|
38
|
+
*/
|
|
39
|
+
initialState?: Partial<TState>;
|
|
40
|
+
}) => TState) & {
|
|
34
41
|
delete: () => boolean;
|
|
35
42
|
setState: (value: import("../vanilla.ts").SetState<TState>) => void;
|
|
36
43
|
getState: () => TState;
|
package/react/use-store.d.ts
CHANGED
|
@@ -4,10 +4,14 @@ export declare const getValueByPath: (obj: any, path: Path) => any;
|
|
|
4
4
|
export declare const isPrefixPath: (candidatePrefix: Path, targetPath: Path) => boolean;
|
|
5
5
|
export declare const compressPaths: (paths: Path[]) => Path[];
|
|
6
6
|
export declare const useStoreStateProxy: <TState extends Record<string, any>>(storeState: TState) => readonly [TState, import("react").RefObject<Path[]>];
|
|
7
|
+
export declare const NO_INITIAL_VALUE: {};
|
|
8
|
+
export declare const useStoreStateWithInitializer: <TState extends Record<string, any>>(store: StoreApi<TState>, initialState?: Partial<TState>) => readonly [TState, import("react").RefObject<WeakMap<StoreApi<TState>, number>>];
|
|
7
9
|
/**
|
|
8
10
|
* React hook for subscribing to a store using automatic dependency tracking.
|
|
9
11
|
*
|
|
10
12
|
* @param store - The store instance to subscribe to
|
|
13
|
+
* @param options - Optional configuration
|
|
14
|
+
* @param options.initialState - Initial state used on first render (and will also update the store state right after that)
|
|
11
15
|
*
|
|
12
16
|
* @returns A proxied version of the store state
|
|
13
17
|
*
|
|
@@ -17,12 +21,14 @@ export declare const useStoreStateProxy: <TState extends Record<string, any>>(st
|
|
|
17
21
|
* - State must be treated as **immutable**:
|
|
18
22
|
* - Updates must replace objects rather than mutate them
|
|
19
23
|
* - Otherwise, changes may not be detected
|
|
20
|
-
* -
|
|
24
|
+
* - If provided, `initialState` will be applied **once per store instance**
|
|
21
25
|
*
|
|
22
26
|
* @example
|
|
23
27
|
* const state = useStoreState(store);
|
|
24
28
|
* return <div>{state.user.name}</div>;
|
|
25
29
|
* // Component will only re-render if `user.name` changes
|
|
26
30
|
*/
|
|
27
|
-
export declare const useStoreState: <TState extends Record<string, any>>(
|
|
31
|
+
export declare const useStoreState: <TState extends Record<string, any>>(store: StoreApi<TState>, options?: {
|
|
32
|
+
initialState?: Partial<TState>;
|
|
33
|
+
}) => TState;
|
|
28
34
|
export {};
|
package/react.js
CHANGED
|
@@ -51,11 +51,24 @@ const useStoreStateProxy = (storeState) => {
|
|
|
51
51
|
}, [storeState]);
|
|
52
52
|
return [trackedState, usedPathsRef];
|
|
53
53
|
};
|
|
54
|
-
const
|
|
55
|
-
|
|
54
|
+
const NO_INITIAL_VALUE = {};
|
|
55
|
+
const useStoreStateWithInitializer = (store, initialState = NO_INITIAL_VALUE) => {
|
|
56
|
+
const initiatedAt = react.useRef(new WeakMap([[store, 0]]));
|
|
57
|
+
useIsomorphicLayoutEffect(() => {
|
|
58
|
+
if (initialState === NO_INITIAL_VALUE || initiatedAt.current.get(store)) return;
|
|
59
|
+
store.setState(initialState);
|
|
60
|
+
initiatedAt.current.set(store, Date.now());
|
|
61
|
+
}, [store, initialState]);
|
|
62
|
+
const storeState = store.getState();
|
|
63
|
+
const finalState = initialState === NO_INITIAL_VALUE || initiatedAt.current.get(store) ? storeState : { ...storeState, ...initialState };
|
|
64
|
+
return [finalState, initiatedAt];
|
|
65
|
+
};
|
|
66
|
+
const useStoreState = (store, options = {}) => {
|
|
67
|
+
const [state] = useStoreStateWithInitializer(store, options.initialState);
|
|
68
|
+
const [trackedState, usedPathsRef] = useStoreStateProxy(state);
|
|
56
69
|
const [, reRender] = react.useState({});
|
|
57
70
|
useIsomorphicLayoutEffect(() => {
|
|
58
|
-
return subscribe((nextState, prevState, changedKeys) => {
|
|
71
|
+
return store.subscribe((nextState, prevState, changedKeys) => {
|
|
59
72
|
const paths = compressPaths(usedPathsRef.current);
|
|
60
73
|
for (const path of paths) {
|
|
61
74
|
const rootKey = path[0];
|
|
@@ -65,13 +78,13 @@ const useStoreState = (storeState, subscribe) => {
|
|
|
65
78
|
if (!Object.is(prevVal, nextVal)) return reRender({});
|
|
66
79
|
}
|
|
67
80
|
});
|
|
68
|
-
}, [
|
|
81
|
+
}, [store]);
|
|
69
82
|
return trackedState;
|
|
70
83
|
};
|
|
71
84
|
|
|
72
85
|
const createStore = (initialState, options) => {
|
|
73
86
|
const store = vanilla.initStore(initialState, options);
|
|
74
|
-
const useStore = () => useStoreState(store
|
|
87
|
+
const useStore = (options2) => useStoreState(store, options2);
|
|
75
88
|
return Object.assign(useStore, store);
|
|
76
89
|
};
|
|
77
90
|
|
|
@@ -86,7 +99,7 @@ const createStores = (initialState, options) => {
|
|
|
86
99
|
store = vanilla.initStore(initialState, options);
|
|
87
100
|
stores.set(keyHash, store);
|
|
88
101
|
}
|
|
89
|
-
const useStore = () => useStoreState(store
|
|
102
|
+
const useStore = (options2) => useStoreState(store, options2);
|
|
90
103
|
return Object.assign(useStore, {
|
|
91
104
|
...store,
|
|
92
105
|
delete: () => {
|
|
@@ -369,8 +382,20 @@ const createQuery = (queryFn, options = {}) => {
|
|
|
369
382
|
internals.set(store, configureInternals(store, variable, variableHash));
|
|
370
383
|
}
|
|
371
384
|
const useStore = (options2 = {}) => {
|
|
372
|
-
const {
|
|
373
|
-
|
|
385
|
+
const {
|
|
386
|
+
initialData = NO_INITIAL_VALUE,
|
|
387
|
+
initialDataIsStale = false,
|
|
388
|
+
revalidateOnMount = true,
|
|
389
|
+
keepPreviousData
|
|
390
|
+
} = options2;
|
|
391
|
+
const [storeState, initialDataInitiatedAt] = useStoreStateWithInitializer(
|
|
392
|
+
store,
|
|
393
|
+
initialData === NO_INITIAL_VALUE ? void 0 : {
|
|
394
|
+
state: "SUCCESS",
|
|
395
|
+
isSuccess: true,
|
|
396
|
+
data: initialData
|
|
397
|
+
}
|
|
398
|
+
);
|
|
374
399
|
const prevState = react.useRef({});
|
|
375
400
|
let storeStateToBeUsed = storeState;
|
|
376
401
|
if (storeState.state !== "INITIAL") {
|
|
@@ -406,8 +431,16 @@ const createQuery = (queryFn, options = {}) => {
|
|
|
406
431
|
});
|
|
407
432
|
}, [store]);
|
|
408
433
|
useIsomorphicLayoutEffect(() => {
|
|
409
|
-
if (revalidateOnMount !== false)
|
|
410
|
-
|
|
434
|
+
if (revalidateOnMount !== false) {
|
|
435
|
+
if (!initialDataIsStale) {
|
|
436
|
+
const dataInitiatedAt = initialDataInitiatedAt.current.get(store);
|
|
437
|
+
if (dataInitiatedAt && dataInitiatedAt + staleTime > Date.now()) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
revalidate(store, variable, false);
|
|
442
|
+
}
|
|
443
|
+
}, [store, revalidateOnMount, initialDataIsStale]);
|
|
411
444
|
if (keepPreviousData) {
|
|
412
445
|
!!trackedState.error;
|
|
413
446
|
}
|
|
@@ -485,7 +518,7 @@ const createMutation = (mutationFn, options = {}) => {
|
|
|
485
518
|
let ongoingPromise;
|
|
486
519
|
const resolveFns = /* @__PURE__ */ new Set([]);
|
|
487
520
|
const store = vanilla.initStore(initialState, options);
|
|
488
|
-
const useStore = () => useStoreState(store
|
|
521
|
+
const useStore = () => useStoreState(store);
|
|
489
522
|
const execute = (variable) => {
|
|
490
523
|
let currentResolveFn;
|
|
491
524
|
const stateBeforeExecute = store.getState();
|
package/vanilla/store.d.ts
CHANGED
|
@@ -51,6 +51,12 @@ export type InitStoreOptions<TState extends Record<string, any>> = {
|
|
|
51
51
|
onSubscribe?: (state: TState, store: StoreApi<TState>) => void;
|
|
52
52
|
onUnsubscribe?: (state: TState, store: StoreApi<TState>) => void;
|
|
53
53
|
onLastUnsubscribe?: (state: TState, store: StoreApi<TState>) => void;
|
|
54
|
+
/**
|
|
55
|
+
* Called whenever the state changes, without counting as a subscriber.
|
|
56
|
+
* Acts like a "spy" on state updates.
|
|
57
|
+
* Useful for devtools, logging, or debugging state changes.
|
|
58
|
+
*/
|
|
59
|
+
onStateChange?: (state: TState, prevState: TState, changedKeys: Array<keyof TState>) => void;
|
|
54
60
|
/**
|
|
55
61
|
* By default, calling `setState` on the server is disallowed to prevent shared state across requests.
|
|
56
62
|
* Set this to `true` only if you explicitly intend to mutate state during server execution.
|
package/vanilla.js
CHANGED
|
@@ -37,6 +37,7 @@ const initStore = (initialState, options = {}) => {
|
|
|
37
37
|
onSubscribe = noop,
|
|
38
38
|
onUnsubscribe = noop,
|
|
39
39
|
onLastUnsubscribe = noop,
|
|
40
|
+
onStateChange = noop,
|
|
40
41
|
allowSetStateServerSide = false
|
|
41
42
|
} = options;
|
|
42
43
|
const subscribers = /* @__PURE__ */ new Set();
|
|
@@ -70,6 +71,7 @@ const initStore = (initialState, options = {}) => {
|
|
|
70
71
|
}
|
|
71
72
|
if (changedKeys.length === 0) return;
|
|
72
73
|
state = { ...prevState, ...newValue };
|
|
74
|
+
onStateChange(state, prevState, changedKeys);
|
|
73
75
|
[...subscribers].forEach((subscriber) => subscriber(state, prevState, changedKeys));
|
|
74
76
|
};
|
|
75
77
|
const storeApi = {
|