floppy-disk 3.0.0-experimental.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +237 -685
  2. package/esm/index.d.mts +1 -0
  3. package/esm/index.mjs +1 -0
  4. package/esm/react/create-mutation.d.mts +151 -0
  5. package/esm/react/create-query.d.mts +344 -0
  6. package/esm/react/create-store.d.mts +28 -0
  7. package/esm/react/create-stores.d.mts +39 -0
  8. package/esm/react/use-isomorphic-layout-effect.d.mts +6 -0
  9. package/esm/react/use-mutation.d.mts +82 -0
  10. package/esm/react/use-store.d.mts +28 -0
  11. package/esm/react.d.mts +7 -0
  12. package/esm/react.mjs +697 -0
  13. package/esm/vanilla/basic.d.mts +13 -0
  14. package/esm/vanilla/hash.d.mts +7 -0
  15. package/esm/vanilla/store.d.mts +89 -0
  16. package/esm/vanilla.d.mts +3 -0
  17. package/esm/vanilla.mjs +82 -0
  18. package/index.d.ts +1 -0
  19. package/index.js +12 -0
  20. package/package.json +47 -45
  21. package/react/create-mutation.d.ts +151 -0
  22. package/react/create-query.d.ts +344 -0
  23. package/react/create-store.d.ts +28 -0
  24. package/react/create-stores.d.ts +39 -0
  25. package/react/use-isomorphic-layout-effect.d.ts +6 -0
  26. package/react/use-mutation.d.ts +82 -0
  27. package/react/use-store.d.ts +28 -0
  28. package/react.d.ts +7 -0
  29. package/react.js +705 -0
  30. package/ts_version_4.5_and_above_is_required.d.ts +0 -0
  31. package/vanilla/basic.d.ts +13 -0
  32. package/vanilla/hash.d.ts +7 -0
  33. package/vanilla/store.d.ts +89 -0
  34. package/vanilla.d.ts +3 -0
  35. package/vanilla.js +89 -0
  36. package/esm/index.d.ts +0 -8
  37. package/esm/index.js +0 -8
  38. package/esm/react/create-bi-direction-query.d.ts +0 -166
  39. package/esm/react/create-bi-direction-query.js +0 -74
  40. package/esm/react/create-mutation.d.ts +0 -39
  41. package/esm/react/create-mutation.js +0 -56
  42. package/esm/react/create-query.d.ts +0 -319
  43. package/esm/react/create-query.js +0 -434
  44. package/esm/react/create-store.d.ts +0 -38
  45. package/esm/react/create-store.js +0 -38
  46. package/esm/react/create-stores.d.ts +0 -61
  47. package/esm/react/create-stores.js +0 -99
  48. package/esm/react/with-context.d.ts +0 -5
  49. package/esm/react/with-context.js +0 -14
  50. package/esm/utils.d.ts +0 -24
  51. package/esm/utils.js +0 -31
  52. package/esm/vanilla/fetcher.d.ts +0 -27
  53. package/esm/vanilla/fetcher.js +0 -95
  54. package/esm/vanilla/init-store.d.ts +0 -24
  55. package/esm/vanilla/init-store.js +0 -51
  56. package/lib/index.d.ts +0 -8
  57. package/lib/index.js +0 -11
  58. package/lib/react/create-bi-direction-query.d.ts +0 -166
  59. package/lib/react/create-bi-direction-query.js +0 -78
  60. package/lib/react/create-mutation.d.ts +0 -39
  61. package/lib/react/create-mutation.js +0 -60
  62. package/lib/react/create-query.d.ts +0 -319
  63. package/lib/react/create-query.js +0 -438
  64. package/lib/react/create-store.d.ts +0 -38
  65. package/lib/react/create-store.js +0 -42
  66. package/lib/react/create-stores.d.ts +0 -61
  67. package/lib/react/create-stores.js +0 -104
  68. package/lib/react/with-context.d.ts +0 -5
  69. package/lib/react/with-context.js +0 -18
  70. package/lib/utils.d.ts +0 -24
  71. package/lib/utils.js +0 -39
  72. package/lib/vanilla/fetcher.d.ts +0 -27
  73. package/lib/vanilla/fetcher.js +0 -99
  74. package/lib/vanilla/init-store.d.ts +0 -24
  75. package/lib/vanilla/init-store.js +0 -55
  76. package/utils/package.json +0 -6
package/README.md CHANGED
@@ -1,818 +1,370 @@
1
- # Floppy Disk 💾
1
+ # FloppyDisk.ts 💾
2
2
 
3
3
  A lightweight, simple, and powerful state management library.
4
4
 
5
- This library was highly-inspired by [Zustand](https://www.npmjs.com/package/zustand) and [TanStack-Query](https://tanstack.com/query).
6
- Both are awesome state manager. That's why this Floppy Disk library behaves like them, but with small DX improvement, **more power**, and **less bundle size**.
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
+ FloppyDisk provides a very similar developer experience (DX), while introducing additional features and a smaller bundle size.
7
7
 
8
- **Bundle Size Comparison:**
8
+ Comparison: https://github.com/afiiif/floppy-disk/tree/beta/comparison
9
9
 
10
- ```js
11
- import { create } from 'zustand'; // 3.3 kB (gzipped: 1.5 kB)
12
- import { createStore } from 'floppy-disk'; // 1.4 kB (gzipped: 750 B) 🎉
10
+ Demo: https://afiiif.github.io/floppy-disk/
13
11
 
14
- import {
15
- QueryClient,
16
- QueryClientProvider,
17
- useQuery,
18
- useInfiniteQuery,
19
- useMutation,
20
- } from '@tanstack/react-query'; // 31.7 kB kB (gzipped: 9.2 kB)
21
- import { createQuery, createMutation } from 'floppy-disk'; // 9.7 kB (gzipped: 3.3 kB) 🎉
22
- ```
12
+ **Installation:**
23
13
 
24
- - Using Zustand & React-Query: https://demo-zustand-react-query.vercel.app/
25
- 👉 Total: **309.21 kB**
26
- - Using Floppy Disk: https://demo-floppy-disk.vercel.app/
27
- 👉 Total: **272.63 kB** 🎉
28
-
29
- ## Key Features
30
-
31
- - **Create Store**
32
- - Get/set store inside/outside component
33
- - Very simple way to customize the reactivity (a.k.a. state update subscription)
34
- - Support middleware
35
- - Set state interception
36
- - Store event (`onSubscribe`, `onUnsubscribe`, etc.)
37
- - Use store as local state manager
38
- - **Create Stores**
39
- - Same as store, but controlled with a store key
40
- - **Create Query & Mutation**
41
- - Backend agnostic (support GraphQL & any async function)
42
- - TypeScript ready
43
- - SSR/SSG support
44
- - Custom reactivity (we choose when to re-render)
45
- - **Create query**
46
- - Dedupe multiple request
47
- - Auto-fetch on mount or manual (lazy query)
48
- - Enable/disable query
49
- - Serve stale data while revalidating
50
- - Retry on error (customizable)
51
- - Optimistic update
52
- - Invalidate query
53
- - Reset query
54
- - Query with param (query key)
55
- - Paginated/infinite query
56
- - Prefetch query
57
- - Fetch from inside/outside component
58
- - Get query state inside/outside component
59
- - Suspense mode
60
- - **Create mutation**
61
- - Mutate from inside/outside component
62
- - Get mutation state inside/outside component
63
- - ... and [a lot more](https://floppy-disk.vercel.app/)
64
-
65
- <br>
66
-
67
- ---
68
-
69
- <p align="center">
70
- View official documentation on <a href="https://floppy-disk.vercel.app/">floppy-disk.vercel.app</a>
71
- </p>
72
-
73
- ---
74
-
75
- <br>
76
-
77
- ## Table of Contents
78
-
79
- - [Key Features](#key-features)
80
- - [Table of Contents](#table-of-contents)
81
- - [Store](#store)
82
- - [Basic Concept](#basic-concept)
83
- - [Advanced Concept](#advanced-concept)
84
- - [Stores](#stores)
85
- - [Query \& Mutation](#query--mutation)
86
- - [Query State \& Network Fetching State](#query-state--network-fetching-state)
87
- - [Inherited from createStores](#inherited-from-createstores)
88
- - [Single Query](#single-query)
89
- - [Single Query with Params](#single-query-with-params)
90
- - [Paginated Query or Infinite Query](#paginated-query-or-infinite-query)
91
- - [Mutation](#mutation)
92
- - [Important Notes](#important-notes)
93
-
94
- ## Store
95
-
96
- ### Basic Concept
97
-
98
- Create a store.
99
-
100
- ```js
101
- import { createStore } from 'floppy-disk';
102
-
103
- const useCatStore = createStore(({ set }) => ({
104
- age: 0,
105
- isSleeping: false,
106
- increaseAge: () => set((state) => ({ age: state.age + 1 })),
107
- reset: () => set({ age: 0, isSleeping: false }),
108
- }));
109
14
  ```
110
-
111
- Use the hook anywhere, no providers are needed.
112
-
113
- ```jsx
114
- function Cat() {
115
- const { age } = useCatStore((state) => [state.age]);
116
- return <div>Cat's age: {age}</div>;
117
- }
118
-
119
- function Control() {
120
- const { increaseAge } = useCatStore((state) => [state.increaseAge]);
121
- return <button onClick={increaseAge}>Increase cat's age</button>;
122
- }
15
+ npm install floppy-disk
123
16
  ```
124
17
 
125
- > Example: [https://codesandbox.io/.../examples/react/basic](https://codesandbox.io/p/sandbox/github/afiiif/floppy-disk-site/tree/main/examples/react/basic)
126
-
127
- Control the reactivity. The concept is same as useEffect dependency array.
128
-
129
- ```jsx
130
- function YourComponent() {
131
- const { age, isSleeping } = useCatStore();
132
- // Will re-render every state change ^
133
- ...
134
- }
18
+ ## Global Store
135
19
 
136
- function YourComponent() {
137
- const { age, isSleeping } = useCatStore((state) => [state.isSleeping]);
138
- // Will only re-render when isSleeping is updated ^
139
- // Update on age won't cause re-render this component
140
- ...
141
- }
20
+ Here's how to create and use a store:
142
21
 
143
- function YourComponent() {
144
- const { age, isSleeping } = useCatStore((state) => [state.age, state.isSleeping]);
145
- // Will re-render when age or isSleeping is updated ^
146
- ...
147
- }
22
+ ```tsx
23
+ import { createStore } from 'floppy-disk/react';
148
24
 
149
- function YourComponent() {
150
- const { age, isSleeping } = useCatStore((state) => [state.age > 3]);
151
- // Will only re-render when (age>3) is updated
152
- ...
153
- }
25
+ const useDigimon = createStore({
26
+ age: 3,
27
+ level: 'Rookie',
28
+ });
154
29
  ```
155
30
 
156
- > Example: [https://codesandbox.io/.../examples/react/custom-reactivity](https://codesandbox.io/p/sandbox/github/afiiif/floppy-disk-site/tree/main/examples/react/custom-reactivity)
31
+ You can use the store both inside and outside of React components.
157
32
 
158
- Reading/writing state and reacting to changes outside of components.
33
+ ```tsx
34
+ function MyDigimon() {
35
+ const { age } = useDigimon();
36
+ return <div>Digimon age: {age}</div>;
37
+ // This component will only re-render when `age` changes.
38
+ // Changes to `level` will NOT trigger a re-render.
39
+ }
159
40
 
160
- ```js
161
- const alertCatAge = () => {
162
- alert(useCatStore.get().age);
163
- };
41
+ function Control() {
42
+ return (
43
+ <>
44
+ <button onClick={() => {
45
+ // You can setState directly
46
+ useDigimon.setState(prev => ({ age: prev.age + 1 }));
47
+ }}>
48
+ Increase digimon's age
49
+ </button>
164
50
 
165
- const toggleIsSleeping = () => {
166
- useCatStore.set((state) => ({ isSleeping: !state.isSleeping }));
167
- };
51
+ <button onClick={evolve}>Evolve</button>
52
+ </>
53
+ );
54
+ }
168
55
 
169
- const unsub = useCatStore.subscribe(
170
- // Action
171
- (state) => {
172
- console.log('The value of age is changed!', state.age);
173
- },
174
- // Reactivity dependency (just like useEffect dependency mentioned above)
175
- (state) => [state.age],
176
- // ^If not set, the action will be triggered on every state change
177
- );
178
- ```
56
+ // You can create a custom actions
57
+ const evolve = () => {
58
+ const { level } = useDigimon.getState();
179
59
 
180
- ### Advanced Concept
60
+ const order = ['In-Training', 'Rookie', 'Champion', 'Ultimate'];
61
+ const nextLevel = order[order.indexOf(level) + 1];
181
62
 
182
- Set the state **silently** (without broadcast the state change to **any subscribers**).
63
+ if (!nextLevel) return console.warn('Already at ultimate level');
183
64
 
184
- ```jsx
185
- const decreaseAgeSilently = () => {
186
- useCatStore.set((state) => ({ age: state.age }), true);
187
- // ^silent param
65
+ useDigimon.setState({ level: nextLevel });
188
66
  };
189
- // 👇 Will not re-render
190
- function Cat() {
191
- const { age } = useCatStore((state) => [state.age]);
192
- return <div>Cat's age: {age}</div>;
193
- }
194
67
  ```
195
68
 
196
- Store events & interception.
69
+ ### Differences from Zustand
197
70
 
198
- ```js
199
- const useCatStore = createStore(
200
- ({ set }) => ({
201
- age: 0,
202
- isSleeping: false,
203
- increaseAge: () => set((state) => ({ age: state.age + 1 })),
204
- reset: () => set({ age: 0, isSleeping: false }),
205
- }),
206
- {
207
- onFirstSubscribe: (state) => {
208
- console.log('onFirstSubscribe', state);
209
- },
210
- onSubscribe: (state) => {
211
- console.log('onSubscribe', state);
212
- },
213
- onUnsubscribe: (state) => {
214
- console.log('onUnsubscribe', state);
215
- },
216
- onLastUnsubscribe: (state) => {
217
- console.log('onLastUnsubscribe', state);
218
- },
219
- intercept: (nextState, prevState) => {
220
- if (nextState.age !== prevState.age) {
221
- return { ...nextState, isSleeping: false };
222
- }
223
- return nextState;
224
- },
225
- },
226
- );
227
- ```
71
+ If you're coming from Zustand, this should feel very familiar.\
72
+ Key differences:
228
73
 
229
- > Example:
230
- > [https://codesandbox.io/.../examples/react/store-event](https://codesandbox.io/p/sandbox/github/afiiif/floppy-disk-site/tree/main/examples/react/store-event)
231
- > [https://codesandbox.io/.../examples/react/intercept](https://codesandbox.io/p/sandbox/github/afiiif/floppy-disk-site/tree/main/examples/react/intercept)
232
-
233
- Let's go wild using IIFE.
234
-
235
- ```js
236
- const useCatStore = createStore(
237
- ({ set }) => ({
238
- age: 0,
239
- isSleeping: false,
240
- increaseAge: () => set((state) => ({ age: state.age + 1 })),
241
- reset: () => set({ age: 0, isSleeping: false }),
242
- }),
243
- (() => {
244
- const validateCat = () => {
245
- console.info('Window focus event triggered...');
246
- const { age } = useCatStore.get();
247
- if (age > 5) useCatStore.set({ age: 1 });
248
- };
249
- return {
250
- onFirstSubscribe: () => window.addEventListener('focus', validateCat),
251
- onLastUnsubscribe: () => window.removeEventListener('focus', validateCat),
252
- };
253
- })(),
254
- );
255
- ```
74
+ 1. **No Selectors Needed**\
75
+ You don't need selectors when using hooks.
76
+ FloppyDisk automatically tracks which parts of the state are used and optimizes re-renders accordingly.
77
+ 2. **Object-Only Store Initialization**\
78
+ In FloppyDisk, stores **must** be initialized with an object. Primitive values or function initializers are not allowed.
256
79
 
257
- Prevent re-render using `Watch`.
80
+ Zustand examples:
258
81
 
259
- ```jsx
260
- function CatPage() {
261
- const { age } = useCatStore((state) => [state.age]);
262
- // If age changed, this component will re-render which will cause
263
- // HeavyComponent1 & HeavyComponent2 to be re-rendered as well.
264
- return (
265
- <main>
266
- <HeavyComponent1 />
267
- <div>Cat's age: {age}</div>
268
- <HeavyComponent2 />
269
- </main>
270
- );
271
- }
82
+ ```tsx
83
+ const useExample1 = create(123);
272
84
 
273
- // Optimized
274
- function CatPageOptimized() {
275
- return (
276
- <main>
277
- <HeavyComponent1 />
278
- <useCatStore.Watch
279
- selectDeps={(state) => [state.age]}
280
- render={({ age }) => {
281
- return <div>Cat's age: {age}</div>;
282
- }}
283
- />
284
- <HeavyComponent2 />
285
- </main>
286
- );
287
- }
85
+ const useExample2 = create(set => ({
86
+ value: 1,
87
+ inc: () => set(prev => ({ value: prev.value + 1 })),
88
+ }));
288
89
  ```
289
90
 
290
- > Example: [https://codesandbox.io/.../examples/react/watch-component](https://codesandbox.io/p/sandbox/github/afiiif/floppy-disk-site/tree/main/examples/react/watch-component)
91
+ FloppyDisk equivalents:
291
92
 
292
- Want a local state instead of global state?
293
- Or, want to set the initial state inside component?
93
+ ```tsx
94
+ const useExample1 = createStore({ value: 123 });
294
95
 
295
- ```jsx
296
- const [CatStoreProvider, useCatStoreContext] = withContext(() =>
297
- createStore(({ set }) => ({
298
- age: 0,
299
- isSleeping: false,
300
- increaseAge: () => set((state) => ({ age: state.age + 1 })),
301
- reset: () => set({ age: 0, isSleeping: false }),
302
- })),
303
- );
96
+ // Unlike Zustand, defining actions inside the store is **discouraged** in FloppyDisk.
97
+ // 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 }));
304
100
 
305
- function Parent() {
306
- return (
307
- <>
308
- <CatStoreProvider>
309
- <CatAge />
310
- <CatIsSleeping />
311
- <WillNotReRenderAsCatStateChanged />
312
- </CatStoreProvider>
313
-
314
- <CatStoreProvider>
315
- <CatAge />
316
- <CatIsSleeping />
317
- <WillNotReRenderAsCatStateChanged />
318
- </CatStoreProvider>
319
-
320
- <CatStoreProvider onInitialize={(store) => store.set({ age: 99 })}>
321
- <CatAge />
322
- <CatIsSleeping />
323
- <WillNotReRenderAsCatStateChanged />
324
- </CatStoreProvider>
325
- </>
326
- );
327
- }
328
-
329
- function CatAge() {
330
- const { age } = useCatStoreContext()((state) => [state.age]);
331
- return <div>Age: {age}</div>;
332
- }
333
- function CatIsSleeping() {
334
- const useCatStore = useCatStoreContext();
335
- const { isSleeping } = useCatStore((state) => [state.isSleeping]);
336
- return (
337
- <>
338
- <div>Is Sleeping: {String(isSleeping)}</div>
339
- <button onClick={useCatStore.get().increaseAge}>Increase cat age</button>
340
- </>
341
- );
342
- }
101
+ // However, it's still possible if you understand how closures work:
102
+ const useExample2Alt = createStore({
103
+ value: 1,
104
+ inc: () => useExample2Alt.setState(prev => ({ value: prev.value + 1 })),
105
+ });
343
106
  ```
344
107
 
345
- > Example: [https://codesandbox.io/.../examples/react/local-state](https://codesandbox.io/p/sandbox/github/afiiif/floppy-disk-site/tree/main/examples/react/local-state)
108
+ ## Async State (Query & Mutation)
346
109
 
347
- Set default reactivity.
110
+ FloppyDisk also provides a powerful async state layer, inspired by [TanStack-Query](https://tanstack.com/query) but with a simpler API.
348
111
 
349
- ```jsx
350
- const useCatStore = createStore(
351
- ({ set }) => ({
352
- age: 0,
353
- isSleeping: false,
354
- increaseAge: () => set((state) => ({ age: state.age + 1 })),
355
- reset: () => set({ age: 0, isSleeping: false }),
356
- }),
357
- {
358
- defaultDeps: (state) => [state.age], // 👈
359
- },
360
- );
112
+ It is agnostic to the type of async operation,
113
+ it works with any Promise-based operation—whether it's a network request, local computation, storage access, or something else.
361
114
 
362
- function Cat() {
363
- const { age } = useCatStore();
364
- // ^will only re-render when age changed
365
- return <div>Cat's age: {age}</div>;
366
- }
367
- ```
115
+ Because of that, we intentionally avoid terms like "fetch" or "refetch".\
116
+ Instead, we use:
368
117
 
369
- ## Stores
118
+ - **execute** → run the async operation (same as "fetch" in TanStack-Query)
119
+ - **revalidate** → re-run while keeping existing data (same as "refetch" in TanStack-Query)
370
120
 
371
- The concept is same as [store](#store), but this can be used for multiple stores.
121
+ ### Query vs Mutation
372
122
 
373
- You need to specify the store key (an object) as identifier.
123
+ <details>
374
124
 
375
- ```js
376
- import { createStores } from 'floppy-disk';
125
+ <summary>Query → Read Operations</summary>
377
126
 
378
- const useCatStores = createStores(
379
- ({ set, get, key }) => ({
380
- // ^store key
381
- age: 0,
382
- isSleeping: false,
383
- increaseAge: () => set((state) => ({ age: state.age + 1 })),
384
- reset: () => set({ age: 0, isSleeping: false }),
385
- }),
386
- {
387
- onBeforeChangeKey: (nextKey, prevKey) => {
388
- console.log('Store key changed', nextKey, prevKey);
389
- },
390
- // ... same as createStore
391
- },
392
- );
127
+ Queries are designed for reading data.\
128
+ They assume:
393
129
 
394
- function CatPage() {
395
- const [catId, setCatId] = useState(1);
130
+ - no side effects
131
+ - no data mutation
132
+ - safe to run multiple times
396
133
 
397
- return (
398
- <>
399
- <div>Current cat id: {catId}</div>
400
- <button onClick={() => setCatId((prev) => prev - 1)}>Prev cat</button>
401
- <button onClick={() => setCatId((prev) => prev + 1)}>Next cat</button>
134
+ Because of this, queries come with helpful defaults:
402
135
 
403
- <Cat catId={catId} />
404
- <Control catId={catId} />
405
- </>
406
- );
407
- }
136
+ - Retry mechanism (for transient failures)
137
+ - Revalidation (keep data fresh automatically)
138
+ - ✅ Caching & staleness control
408
139
 
409
- function Cat({ catId }) {
410
- const { age } = useCatStores({ catId }, (state) => [state.age]);
411
- return <div>Cat's age: {age}</div>;
412
- }
413
-
414
- function Control({ catId }) {
415
- const { increaseAge } = useCatStores({ catId }, (state) => [state.increaseAge]);
416
- return <button onClick={increaseAge}>Increase cat's age</button>;
417
- }
418
- ```
140
+ Use queries when:
419
141
 
420
- > Example: [https://codesandbox.io/.../examples/react/stores](https://codesandbox.io/p/sandbox/github/afiiif/floppy-disk-site/tree/main/examples/react/stores)
142
+ - fetching data
143
+ - reading from storage
144
+ - running idempotent async logic
421
145
 
422
- <br><br>
146
+ </details>
423
147
 
424
- <p align="center">
425
- — ✨ 💾 ✨ —
426
- </p>
427
- <br>
148
+ <details>
428
149
 
429
- ## Query & Mutation
150
+ <summary>Mutation Write Operations</summary>
430
151
 
431
- With the power of `createStores` function and a bit creativity, we can easily create a hook just like `useQuery` and `useInfiniteQuery` in [React-Query](https://tanstack.com/query) using `createQuery` function.
152
+ Mutations are designed for changing data.\
153
+ Examples:
432
154
 
433
- It can dedupe multiple request, handle caching, auto-update stale data, handle retry on error, handle infinite query, and many more. With the flexibility given in `createStores`, you can extend its power according to your needs.
155
+ - insert
156
+ - update
157
+ - delete
158
+ - triggering side effects
434
159
 
435
- ### Query State & Network Fetching State
160
+ Because mutations are **not safe to repeat blindly**, FloppyDisk does **not** include:
436
161
 
437
- There are 2 types of state: query (data) state & network fetching state.
162
+ - automatic retry
163
+ - ❌ automatic revalidation
164
+ - ❌ implicit re-execution
438
165
 
439
- `status`, `isLoading`, `isSuccess`, `isError` is a query data state.
440
- It has no relation with network fetching state. ⚠️
441
- Here is the flow of the query data state:
166
+ This is intentional.\
167
+ Mutations should be explicit and controlled, not automatic.
442
168
 
443
- - Initial state when there is no data fetched.
444
- `{ status: 'loading', isLoading: true, isSuccess: false, isError: false }`
445
- - After data fetching:
446
- - If success
447
- `{ status: 'success', isLoading: false, isSuccess: true, isError: false }`
448
- - If error
449
- `{ status: 'error', isLoading: false, isSuccess: false, isError: true }`
450
- - After data fetched successfully, you will **always** get this state:
451
- `{ status: 'success', isLoading: false, isSuccess: true, isError: false }`
452
- - If a refetch is fired and got error, the state would be:
453
- `{ status: 'success', isLoading: false, isSuccess: true, isError: false, isRefetchError: true }`
454
- The previouse success response will be kept.
169
+ If you need retry mechanism, then you can always add it manually.
455
170
 
456
- For network fetching state, we use `isWaiting`.
457
- The value will be `true` if the query is called and still waiting for the response.
171
+ </details>
458
172
 
459
- ### Inherited from createStores
173
+ ### Single Query
460
174
 
461
- The `createQuery` function inherits functionality from the `createStores` function, allowing us to perform the same result and actions available in `createStores`.
175
+ Create a query using `createQuery`:
462
176
 
463
177
  ```tsx
464
- const useMyQuery = createQuery(myQueryFn, {
465
- // 👇 Same as createStores options
466
- defaultDeps: undefined,
467
- onFirstSubscribe: (state) => console.log('onFirstSubscribe', state),
468
- onSubscribe: (state) => console.log('onSubscribe', state),
469
- onUnsubscribe: (state) => console.log('onUnsubscribe', state),
470
- onLastUnsubscribe: (state) => console.log('onLastUnsubscribe', state),
471
- onBeforeChangeKey: (nextKey, prevKey) => console.log('Store key changed', nextKey, prevKey),
472
-
473
- // ... other createQuery options
474
- });
475
- ```
178
+ import { createQuery } from 'floppy-disk/react';
476
179
 
477
- Custom reactivity (dependency array) also works:
180
+ const myCoolQuery = createQuery(
181
+ myAsyncFn,
182
+ // { staleTime: 5000, revalidateOnFocus: false } <-- optional options
183
+ );
478
184
 
479
- ```tsx
480
- function QueryLoader() {
481
- // This component doesn't care whether the query is success or error.
482
- // It just listening to network fetching state. 👇
483
- const { isWaiting } = useMyQuery((state) => [state.isWaiting]);
484
- return <div>Is network fetching? {String(isWaiting)}</div>;
185
+ const useMyCoolQuery = myCoolQuery();
186
+
187
+ // Use it inside your component:
188
+
189
+ function MyComponent() {
190
+ const query = useMyCoolQuery();
191
+ if (query.state === 'INITIAL') return <div>Loading...</div>;
192
+ if (query.error) return <div>Error: {query.error.message}</div>;
193
+ return <div>{JSON.stringify(query.data)}</div>;
485
194
  }
486
195
  ```
487
196
 
488
- ### Single Query
197
+ ### Query State: Two Independent Dimensions
489
198
 
490
- ```jsx
491
- const useGitHubQuery = createQuery(async () => {
492
- const res = await fetch('https://api.github.com/repos/afiiif/floppy-disk');
493
- if (res.ok) return res.json();
494
- throw res;
495
- });
199
+ FloppyDisk tracks two things separately:
496
200
 
497
- function SingleQuery() {
498
- const { isLoading, data } = useGitHubQuery();
201
+ - Is it running? → `isPending`\
202
+ (value: `boolean`)
203
+ - What's the result? → `state`\
204
+ (value: `INITIAL | 'SUCCESS' | 'ERROR' | 'SUCCESS_BUT_REVALIDATION_ERROR'`)
499
205
 
500
- if (isLoading) return <div>Loading...</div>;
206
+ They are **independent**.
501
207
 
502
- return (
503
- <div>
504
- <h1>{data.name}</h1>
505
- <p>{data.description}</p>
506
- <strong>⭐️ {data.stargazers_count}</strong>
507
- <strong>🍴 {data.forks_count}</strong>
508
- </div>
509
- );
510
- }
511
- ```
208
+ ### Automatic Re-render Optimization
512
209
 
513
- > Example: [https://codesandbox.io/.../examples/react/query](https://codesandbox.io/p/sandbox/github/afiiif/floppy-disk-site/tree/main/examples/react/query)
210
+ Just like the global store, FloppyDisk tracks usage automatically:
514
211
 
515
- Actions:
212
+ ```tsx
213
+ const { data } = useMyQuery();
214
+ // ^Only data changes will trigger a re-render
516
215
 
517
- Normally, we don't need reactivity for the actions.
518
- Therefore, using `get` method will be better, since it will not re-render the component when a query state changed.
216
+ const value = useMyQuery().data?.foo.bar.baz;
217
+ // ^Only data.foo.bar.baz changes will trigger a re-render
218
+ ```
519
219
 
520
- ```jsx
521
- function Actions() {
522
- const { fetch, forceFetch, reset } = useGitHubQuery.get();
220
+ ### Keyed Query (Dynamic Params)
523
221
 
524
- // Or like this:
525
- // const { isLoading, data, error, fetch, forceFetch, reset } = useGitHubQuery();
222
+ You can create parameterized queries:
526
223
 
527
- return (
528
- <>
529
- <button onClick={fetch}>Call query if the query data is stale</button>
530
- <button onClick={forceFetch}>Call query</button>
531
- <button onClick={reset}>Reset query</button>
532
- </>
533
- );
534
- }
535
- ```
224
+ ```tsx
225
+ import { getUserById, type GetUserByIdResponse } from '../utils';
536
226
 
537
- Options:
227
+ type MyQueryParam = { id: string };
538
228
 
539
- ```jsx
540
- const useGitHubQuery = createQuery(
541
- async () => {
542
- const res = await fetch('https://api.github.com/repos/afiiif/floppy-disk');
543
- if (res.ok) return res.json();
544
- throw res;
545
- },
546
- {
547
- fetchOnMount: false,
548
- enabled: () => !!useUserQuery.get().data?.user,
549
- select: (response) => response.name,
550
- staleTime: Infinity, // Never stale
551
- retry: 0, // No retry
552
- onSuccess: (response) => {},
553
- onError: (error) => {},
554
- onSettled: () => {},
555
- },
229
+ const userQuery = createQuery<GetUserByIdResponse, MyQueryParam>(
230
+ getUserById,
231
+ // { staleTime: 5000, revalidateOnFocus: false } <-- optional options
556
232
  );
557
-
558
- function MyComponent() {
559
- const { data, response } = useGitHubQuery();
560
- /**
561
- * Since in option we select the data like this:
562
- * select: (response) => response.name
563
- *
564
- * The return will be:
565
- * {
566
- * response: { id: 677863376, name: "floppy-disk", ... },
567
- * data: "floppy-disk",
568
- * ...
569
- * }
570
- */
571
- }
572
233
  ```
573
234
 
574
- Get data or do something outside component:
235
+ Use it with parameters:
575
236
 
576
- ```jsx
577
- const getData = () => console.log(useGitHubQuery.get().data);
578
- const resetQuery = () => useGitHubQuery.get().reset();
579
-
580
- // Works just like createStores
581
- useMyQuery.get(/* ... */);
582
- useMyQuery.set(/* ... */);
583
- useMyQuery.subscribe(/* ... */);
584
- useMyQuery.getSubscribers(/* ... */);
237
+ ```tsx
238
+ function UserDetail({ id }) {
239
+ const useUserQuery = userQuery({ id: 1 });
240
+ const query = useUserQuery();
241
+ if (query.state === 'INITIAL') return <div>Loading...</div>;
242
+ if (query.error) return <div>Error: {query.error.message}</div>;
243
+ return <div>{JSON.stringify(query.data)}</div>;
244
+ }
585
245
  ```
586
246
 
587
- ### Single Query with Params
247
+ Each unique parameter creates its own cache entry.
588
248
 
589
- ```jsx
590
- const usePokemonQuery = createQuery(async ({ pokemon }) => {
591
- const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${pokemon}`);
592
- if (res.ok) return res.json();
593
- throw res;
594
- });
249
+ ### Infinite Query
595
250
 
596
- function PokemonPage() {
597
- const [currentPokemon, setCurrentPokemon] = useState();
598
- const { isLoading, data } = usePokemonQuery({ pokemon: currentPokemon });
251
+ FloppyDisk does **not provide** a dedicated "infinite query" API.\
252
+ Instead, it embraces a simpler and more flexible approach:
599
253
 
600
- if (isLoading) return <div>Loading...</div>;
254
+ > Infinite queries are just **composition** + **recursion**.
601
255
 
602
- return (
603
- <div>
604
- <h1>{data.name}</h1>
605
- <div>Weight: {data.weight}</div>
606
- </div>
607
- );
608
- }
609
- ```
256
+ Why? Because async state is already powerful enough:
610
257
 
611
- > Example: [https://codesandbox.io/.../examples/react/query-with-param](https://codesandbox.io/p/sandbox/github/afiiif/floppy-disk-site/tree/main/examples/react/query-with-param)
258
+ - keyed queries handle parameters
259
+ - components handle composition
260
+ - recursion handles pagination
612
261
 
613
- Get data or do something outside component:
262
+ No special abstraction needed.
614
263
 
615
- ```jsx
616
- const getDitto = () => {
617
- console.log(usePokemonQuery.get({ pokemon: 'ditto' }).data);
618
- };
264
+ Here is the example on how to implement infinite query properly:
619
265
 
620
- const resetDitto = () => {
621
- usePokemonQuery.get({ pokemon: 'ditto' }).reset();
266
+ ```tsx
267
+ type GetPostParams = {
268
+ cursor?: string; // For pagination
269
+ };
270
+ type GetPostsResponse = {
271
+ posts: Post[];
272
+ meta: { nextCursor: string };
622
273
  };
623
274
 
624
- function Actions() {
625
- return (
626
- <>
627
- <button onClick={getDitto}>Get Ditto Data</button>
628
- <button onClick={resetDitto}>Reset Ditto</button>
629
- </>
630
- );
631
- }
632
- ```
633
-
634
- ### Paginated Query or Infinite Query
635
-
636
- ```jsx
637
- const usePokemonsInfQuery = createQuery(
638
- async (_, { pageParam = 0 }) => {
639
- const res = await fetch(`https://pokeapi.co/api/v2/pokemon?limit=10&offset=${pageParam}`);
640
- if (res.ok) return res.json();
641
- throw res;
642
- },
275
+ const postsQuery = createQuery<GetPostsResponse, GetPostParams>(
276
+ getPosts,
643
277
  {
644
- select: (response, { data = [] }) => [...data, ...response.results],
645
- getNextPageParam: (lastPageResponse, i) => {
646
- if (i > 5) return undefined; // Return undefined means you have reached the end of the pages
647
- return i * 10;
648
- },
278
+ staleTime: Infinity,
279
+ revalidateOnFocus: false,
280
+ revalidateOnReconnect: false,
649
281
  },
650
282
  );
651
283
 
652
- function PokemonListPage() {
653
- const { data = [], fetchNextPage, hasNextPage, isWaitingNextPage } = usePokemonsInfQuery();
284
+ function Main() {
285
+ return <Page cursor={undefined} />;
286
+ }
287
+
288
+ function Page({ cursor }: { cursor?: string }) {
289
+ const usePostsQuery = postsQuery({ cursor });
290
+ const { state, data, error } = usePostsQuery();
291
+
292
+ if (state === 'INITIAL') return <div>Loading...</div>;
293
+ if (error) return <div>Error</div>;
654
294
 
655
295
  return (
656
- <div>
657
- {data.map((pokemon) => (
658
- <div key={pokemon.name}>{pokemon.name}</div>
296
+ <>
297
+ {data.posts.map(post => (
298
+ <PostCard key={post.id} post={post} />
659
299
  ))}
660
- {isWaitingNextPage ? (
661
- <div>Loading more...</div>
662
- ) : (
663
- hasNextPage && <button onClick={fetchNextPage}>Load more</button>
300
+ {data.meta.nextCursor && (
301
+ <LoadMore nextCursor={data.meta.nextCursor} />
664
302
  )}
665
- </div>
303
+ </>
666
304
  );
667
305
  }
668
- ```
669
-
670
- > Example: [https://codesandbox.io/.../examples/react/infinite-query](https://codesandbox.io/p/sandbox/github/afiiif/floppy-disk-site/tree/main/examples/react/infinite-query)
671
-
672
- **Note:**
673
306
 
674
- - The default stale time is 3 seconds.
675
- - The default error retry attempt is 1 time, and retry delay is 2 seconds.
676
- - The default reactivity of a query is:
677
- `(s) => [s.data, s.error, s.isWaitingNextPage, s.hasNextPage]`
678
- - Note that by default, subscribers don't listen to `isWaiting` state.
679
- - You can change the `defaultDeps` on `createQuery` options.
307
+ function LoadMore({ nextCursor }: { nextCursor?: string }) {
308
+ const [isNextPageRequested, setIsNextPageRequested] = useState(() => {
309
+ const stateOfNextPageQuery = postsQuery({ cursor: nextCursor }).getState();
310
+ return stateOfNextPageQuery.isPending || stateOfNextPageQuery.isSuccess;
311
+ });
680
312
 
681
- ### Mutation
313
+ if (isNextPageRequested) {
314
+ return <Page cursor={nextCursor} />;
315
+ }
682
316
 
683
- ```jsx
684
- const useLoginMutation = createMutation(
685
- async (variables) => {
686
- const res = await axios.post('/auth/login', {
687
- email: variables.email,
688
- password: variables.password,
689
- });
690
- return res.data;
691
- },
692
- {
693
- onSuccess: (response, variables) => {
694
- console.log(`Logged in as ${variables.email}`);
695
- console.log(`Access token: ${response.data.accessToken}`);
696
- },
697
- },
698
- );
699
-
700
- function Login() {
701
- const { mutate, isWaiting } = useLoginMutation();
702
- const showToast = useToast();
703
317
  return (
704
- <div>
705
- <button
706
- disabled={isWaiting}
707
- onClick={() => {
708
- mutate({ email: 'foo@bar.baz', password: 's3cREt' }).then(({ response, error }) => {
709
- if (error) {
710
- showToast('Login failed');
711
- } else {
712
- showToast('Login success');
713
- }
714
- });
715
- }}
716
- >
717
- Login
718
- </button>
719
- </div>
318
+ <ReachingBottomObserver
319
+ onReachBottom={() => setIsNextPageRequested(true)}
320
+ />
720
321
  );
721
322
  }
722
323
  ```
723
324
 
724
- Optimistic update:
325
+ When implementing infinite queries, it is **highly recommended to disable automatic revalidation**.
725
326
 
726
- ```jsx
727
- function SaveProduct() {
728
- const { mutate, isWaiting } = useEditProductMutation();
729
- const { getValues } = useFormContext();
327
+ Why?\
328
+ In an infinite list, users may scroll through many pages ("_doom-scrolling_").\
329
+ If revalidation is triggered:
730
330
 
731
- return (
732
- <button
733
- disabled={isWaiting}
734
- onClick={() => {
735
- const payload = getValues();
736
-
737
- const { revert, invalidate } = useProductQuery.optimisticUpdate({
738
- key: { id: payload.id },
739
- response: payload,
740
- });
741
-
742
- mutate(payload).then(({ response, error }) => {
743
- if (error) {
744
- revert();
745
- }
746
- invalidate();
747
- });
748
- }}
749
- >
750
- Save
751
- </button>
752
- );
753
- }
754
- ```
331
+ - All previously loaded pages may re-execute
332
+ - Content at the top may change without the user noticing
333
+ - Layout shifts can occur unexpectedly
755
334
 
756
- > Example: [https://codesandbox.io/.../examples/react/mutation](https://codesandbox.io/p/sandbox/github/afiiif/floppy-disk-site/tree/main/examples/react/mutation)
335
+ This leads to a **confusing and unstable user experience**.\
336
+ Revalidating dozens of previously viewed pages rarely provides value to the user.
757
337
 
758
- <br><br>
338
+ ## SSR Guidance
759
339
 
760
- <p align="center">
761
- — ✨ 💾 ✨ —
762
- </p>
763
- <br>
340
+ FloppyDisk is designed primarily for **client-side [sync/async] state**.
764
341
 
765
- ## Important Notes
342
+ If your data is already fetched on the server (e.g. via SSR/ISR, Server Components, or Server Actions), then:
766
343
 
767
- Don't mutate. (unless you use Immer JS library or something similar)
344
+ > **You most likely don't need this library.**
768
345
 
769
- ```js
770
- import { createStore } from 'floppy-disk';
346
+ This is the same philosophy as TanStack Query. 💡
771
347
 
772
- const useCartStore = createStore(({ set, get }) => ({
773
- products: [],
774
- addProduct: (newProduct) => {
775
- const currentProducts = get().products;
776
- product.push(newProduct); // ❌ Don't mutate
777
- set({ product });
778
- },
779
- }));
780
- ```
348
+ In many cases, developers mix SSR/ISR with client-side state because they want:
781
349
 
782
- Don't use conditional reactivity selector.
350
+ 1. Data to be rendered into HTML on the server
351
+ 2. The ability to **revalidate it on the client**
783
352
 
784
- ```jsx
785
- function Cat({ isSomething }) {
786
- const { age } = useCatStore(isSomething ? (state) => [state.age] : null); // ❌
787
- const { age } = useCatStore((state) => (isSomething ? [state.age] : [state.isSleeping])); // ❌
788
- return <div>Cat's age: {age}</div>;
789
- }
790
- ```
353
+ A common (but inefficient) approach is:
791
354
 
792
- No need to memoize the reactivity selector.
355
+ - fetch on the server
356
+ - hydrate it into a client-side cache
357
+ - then revalidate using a query library
793
358
 
794
- ```jsx
795
- function Cat() {
796
- const selectAge = useCallback((state) => [state.age], []); // ❌
797
- const { age } = useCatStore(selectAge);
798
- return <div>Cat's age: {age}</div>;
799
- }
800
- ```
359
+ While this works, it introduces additional complexity.
801
360
 
802
- No need to memoize the store key / query key.
361
+ Instead, we encourage a simpler approach:
803
362
 
804
- ```jsx
805
- function PokemonsPage() {
806
- const queryKey = useMemo(() => ({ generation: 'ii', sort: 'asc' }), []); // ❌
807
- const { isLoading, data } = usePokemonsQuery(queryKey);
808
- return <div>...</div>;
809
- }
810
- ```
363
+ > If your data is fetched on the server, revalidate it using **your framework's built-in mechanism** (e.g. Next.js route revalidation).
811
364
 
812
- <br>
365
+ Because of this philosophy, FloppyDisk **does not support** hydrating server-fetched data into the client store.
813
366
 
814
- ---
367
+ This keeps the mental model clean:
815
368
 
816
- <p align="center">
817
- View official documentation on <a href="https://floppy-disk.vercel.app/">floppy-disk.vercel.app</a>
818
- </p>
369
+ - server data → handled by the framework
370
+ - client async state handled by FloppyDisk