floppy-disk 3.0.0-experimental.1 → 3.0.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.
Files changed (76) hide show
  1. package/README.md +256 -676
  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,398 @@
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
+ Demo: https://afiiif.github.io/floppy-disk/
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
+ **Installation:**
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
  ```
23
-
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
- }));
13
+ npm install floppy-disk
109
14
  ```
110
15
 
111
- Use the hook anywhere, no providers are needed.
16
+ ## Global Store
112
17
 
113
- ```jsx
114
- function Cat() {
115
- const { age } = useCatStore((state) => [state.age]);
116
- return <div>Cat's age: {age}</div>;
117
- }
18
+ Here's how to create and use a store:
118
19
 
119
- function Control() {
120
- const { increaseAge } = useCatStore((state) => [state.increaseAge]);
121
- return <button onClick={increaseAge}>Increase cat's age</button>;
122
- }
123
- ```
20
+ ```tsx
21
+ import { createStore } from 'floppy-disk/react';
124
22
 
125
- > Example: [https://codesandbox.io/.../examples/react/basic](https://codesandbox.io/p/sandbox/github/afiiif/floppy-disk-site/tree/main/examples/react/basic)
23
+ const useDigimon = createStore({
24
+ age: 7,
25
+ level: 'Rookie',
26
+ });
27
+ ```
126
28
 
127
- Control the reactivity. The concept is same as useEffect dependency array.
29
+ You can use the store both inside and outside of React components.
128
30
 
129
- ```jsx
130
- function YourComponent() {
131
- const { age, isSleeping } = useCatStore();
132
- // Will re-render every state change ^
133
- ...
31
+ ```tsx
32
+ function MyDigimon() {
33
+ const { age } = useDigimon();
34
+ return <div>Digimon age: {age}</div>;
35
+ // This component will only re-render when `age` changes.
36
+ // Changes to `level` will NOT trigger a re-render.
134
37
  }
135
38
 
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
- }
39
+ function Control() {
40
+ return (
41
+ <>
42
+ <button
43
+ onClick={() => {
44
+ // You can setState directly
45
+ useDigimon.setState((prev) => ({ age: prev.age + 1 }));
46
+ }}
47
+ >
48
+ Increase digimon's age
49
+ </button>
142
50
 
143
- function YourComponent() {
144
- const { age, isSleeping } = useCatStore((state) => [state.age, state.isSleeping]);
145
- // Will re-render when age or isSleeping is updated ^
146
- ...
51
+ <button onClick={evolve}>Evolve</button>
52
+ </>
53
+ );
147
54
  }
148
55
 
149
- function YourComponent() {
150
- const { age, isSleeping } = useCatStore((state) => [state.age > 3]);
151
- // Will only re-render when (age>3) is updated
152
- ...
153
- }
154
- ```
56
+ // You can create a custom actions
57
+ const evolve = () => {
58
+ const { level } = useDigimon.getState();
155
59
 
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)
60
+ const order = ['In-Training', 'Rookie', 'Champion', 'Ultimate'];
61
+ const nextLevel = order[order.indexOf(level) + 1];
157
62
 
158
- Reading/writing state and reacting to changes outside of components.
63
+ if (!nextLevel) return console.warn('Already at ultimate level');
159
64
 
160
- ```js
161
- const alertCatAge = () => {
162
- alert(useCatStore.get().age);
65
+ useDigimon.setState({ level: nextLevel });
163
66
  };
67
+ ```
164
68
 
165
- const toggleIsSleeping = () => {
166
- useCatStore.set((state) => ({ isSleeping: !state.isSleeping }));
167
- };
69
+ ### Store Subscription
168
70
 
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
- ```
71
+ At its core, FloppyDisk is a **pub-sub store**.
179
72
 
180
- ### Advanced Concept
73
+ You can subscribe manually:
181
74
 
182
- Set the state **silently** (without broadcast the state change to **any subscribers**).
75
+ ```tsx
76
+ const unsubscribe = useMyStore.subscribe((state, prev) => {
77
+ console.log('New state:', state);
78
+ });
183
79
 
184
- ```jsx
185
- const decreaseAgeSilently = () => {
186
- useCatStore.set((state) => ({ age: state.age }), true);
187
- // ^silent param
188
- };
189
- // 👇 Will not re-render
190
- function Cat() {
191
- const { age } = useCatStore((state) => [state.age]);
192
- return <div>Cat's age: {age}</div>;
193
- }
80
+ // Later
81
+ unsubscribe();
194
82
  ```
195
83
 
196
- Store events & interception.
84
+ FloppyDisk provides lifecycle hooks tied to subscription count.
197
85
 
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
- }),
86
+ ```tsx
87
+ const useTowerDefense = createStore(
88
+ { archers: 3, mages: 1, barracks: 2, artillery: 1 },
206
89
  {
207
- onFirstSubscribe: (state) => {
208
- console.log('onFirstSubscribe', state);
209
- },
210
- onSubscribe: (state) => {
211
- console.log('onSubscribe', state);
90
+ onFirstSubscribe: () => {
91
+ console.log('First subscriber! We’re officially popular 🎉');
212
92
  },
213
- onUnsubscribe: (state) => {
214
- console.log('onUnsubscribe', state);
93
+ onSubscribe: () => {
94
+ console.log('New subscriber joined. Welcome aboard 🫡');
215
95
  },
216
- onLastUnsubscribe: (state) => {
217
- console.log('onLastUnsubscribe', state);
96
+ onUnsubscribe: () => {
97
+ console.log('Subscriber left... was it something I said? 😭');
218
98
  },
219
- intercept: (nextState, prevState) => {
220
- if (nextState.age !== prevState.age) {
221
- return { ...nextState, isSleeping: false };
222
- }
223
- return nextState;
99
+ onLastUnsubscribe: () => {
100
+ console.log('Everyone left. Guess I’ll just exist quietly now...');
224
101
  },
225
102
  },
226
103
  );
227
104
  ```
228
105
 
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
- ```
106
+ ### Differences from Zustand
256
107
 
257
- Prevent re-render using `Watch`.
108
+ If you're coming from Zustand, this should feel very familiar.\
109
+ Key differences:
258
110
 
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
- }
111
+ 1. **No Selectors Needed**\
112
+ You don't need selectors when using hooks.
113
+ FloppyDisk automatically tracks which parts of the state are used and optimizes re-renders accordingly.
114
+ 2. **Object-Only Store Initialization**\
115
+ In FloppyDisk, stores **must** be initialized with an object. Primitive values or function initializers are not allowed.
272
116
 
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
- }
288
- ```
117
+ Zustand examples:
289
118
 
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)
119
+ ```tsx
120
+ const useDate = create(new Date(2021, 01, 11));
291
121
 
292
- Want a local state instead of global state?
293
- Or, want to set the initial state inside component?
122
+ const useCounter = create((set) => ({
123
+ value: 1,
124
+ increment: () => set((prev) => ({ value: prev.value + 1 })),
125
+ }));
126
+ ```
294
127
 
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
- );
128
+ FloppyDisk equivalents:
304
129
 
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
- }
130
+ ```tsx
131
+ const useDate = createStore({ value: new Date(2021, 01, 11) });
328
132
 
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
- }
343
- ```
133
+ const useCounter = createStore({ value: 1 });
134
+ const increment = () => useCounter.setState((prev) => ({ value: prev.value + 1 }));
135
+ // Unlike Zustand, defining actions inside the store is **discouraged** in FloppyDisk.
136
+ // This improves tree-shakeability and keeps your store minimal.
344
137
 
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)
138
+ // However, it's still possible to mix actions with the state if you understand how closures work:
139
+ const useCounterAlt = createStore({
140
+ value: 1,
141
+ increment: () => useCounterAlt.setState((prev) => ({ value: prev.value + 1 })),
142
+ });
143
+ ```
346
144
 
347
- Set default reactivity.
145
+ ## Async State (Query & Mutation)
348
146
 
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
- );
147
+ FloppyDisk also provides a powerful async state layer, inspired by [TanStack-Query](https://tanstack.com/query) but with a simpler API.
361
148
 
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
- ```
149
+ It is agnostic to the type of async operation,
150
+ it works with any Promise-based operation—whether it's a network request, local computation, storage access, or something else.
368
151
 
369
- ## Stores
152
+ Because of that, we intentionally avoid terms like "fetch" or "refetch".\
153
+ Instead, we use:
370
154
 
371
- The concept is same as [store](#store), but this can be used for multiple stores.
155
+ - **execute** run the async operation (same as "fetch" in TanStack-Query)
156
+ - **revalidate** → re-run while keeping existing data (same as "refetch" in TanStack-Query)
372
157
 
373
- You need to specify the store key (an object) as identifier.
158
+ ### Query vs Mutation
374
159
 
375
- ```js
376
- import { createStores } from 'floppy-disk';
160
+ <details>
377
161
 
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
- );
162
+ <summary>Query Read Operations</summary>
393
163
 
394
- function CatPage() {
395
- const [catId, setCatId] = useState(1);
164
+ Queries are designed for reading data.\
165
+ They assume:
396
166
 
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>
167
+ - no side effects
168
+ - no data mutation
169
+ - safe to run multiple times
402
170
 
403
- <Cat catId={catId} />
404
- <Control catId={catId} />
405
- </>
406
- );
407
- }
171
+ Because of this, queries come with helpful defaults:
408
172
 
409
- function Cat({ catId }) {
410
- const { age } = useCatStores({ catId }, (state) => [state.age]);
411
- return <div>Cat's age: {age}</div>;
412
- }
173
+ - ✅ Retry mechanism (for transient failures)
174
+ - Revalidation (keep data fresh automatically)
175
+ - Caching & staleness control
413
176
 
414
- function Control({ catId }) {
415
- const { increaseAge } = useCatStores({ catId }, (state) => [state.increaseAge]);
416
- return <button onClick={increaseAge}>Increase cat's age</button>;
417
- }
418
- ```
177
+ Use queries when:
419
178
 
420
- > Example: [https://codesandbox.io/.../examples/react/stores](https://codesandbox.io/p/sandbox/github/afiiif/floppy-disk-site/tree/main/examples/react/stores)
179
+ - fetching data
180
+ - reading from storage
181
+ - running idempotent async logic
421
182
 
422
- <br><br>
183
+ </details>
423
184
 
424
- <p align="center">
425
- — ✨ 💾 ✨ —
426
- </p>
427
- <br>
185
+ <details>
428
186
 
429
- ## Query & Mutation
187
+ <summary>Mutation Write Operations</summary>
430
188
 
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.
189
+ Mutations are designed for changing data.\
190
+ Examples:
432
191
 
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.
192
+ - insert
193
+ - update
194
+ - delete
195
+ - triggering side effects
434
196
 
435
- ### Query State & Network Fetching State
197
+ Because mutations are **not safe to repeat blindly**, FloppyDisk does **not** include:
436
198
 
437
- There are 2 types of state: query (data) state & network fetching state.
199
+ - automatic retry
200
+ - ❌ automatic revalidation
201
+ - ❌ implicit re-execution
438
202
 
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:
203
+ This is intentional.\
204
+ Mutations should be explicit and controlled, not automatic.
442
205
 
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.
206
+ If you need retry mechanism, then you can always add it manually.
455
207
 
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.
208
+ </details>
458
209
 
459
- ### Inherited from createStores
210
+ ### Single Query
460
211
 
461
- The `createQuery` function inherits functionality from the `createStores` function, allowing us to perform the same result and actions available in `createStores`.
212
+ Create a query using `createQuery`:
462
213
 
463
214
  ```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
- ```
215
+ import { createQuery } from 'floppy-disk/react';
476
216
 
477
- Custom reactivity (dependency array) also works:
217
+ const myCoolQuery = createQuery(
218
+ myAsyncFn,
219
+ // { staleTime: 5000, revalidateOnFocus: false } <-- optional options
220
+ );
478
221
 
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>;
222
+ const useMyCoolQuery = myCoolQuery();
223
+
224
+ // Use it inside your component:
225
+
226
+ function MyComponent() {
227
+ const query = useMyCoolQuery();
228
+ if (query.state === 'INITIAL') return <div>Loading...</div>;
229
+ if (query.error) return <div>Error: {query.error.message}</div>;
230
+ return <div>{JSON.stringify(query.data)}</div>;
485
231
  }
486
232
  ```
487
233
 
488
- ### Single Query
234
+ ### Query State: Two Independent Dimensions
489
235
 
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
- });
236
+ FloppyDisk tracks two things separately:
496
237
 
497
- function SingleQuery() {
498
- const { isLoading, data } = useGitHubQuery();
238
+ - Is it running? → `isPending`\
239
+ (value: `boolean`)
240
+ - What's the result? → `state`\
241
+ (value: `INITIAL | 'SUCCESS' | 'ERROR' | 'SUCCESS_BUT_REVALIDATION_ERROR'`)
499
242
 
500
- if (isLoading) return <div>Loading...</div>;
243
+ They are **independent**.
501
244
 
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
- ```
245
+ ### Automatic Re-render Optimization
512
246
 
513
- > Example: [https://codesandbox.io/.../examples/react/query](https://codesandbox.io/p/sandbox/github/afiiif/floppy-disk-site/tree/main/examples/react/query)
247
+ Just like the global store, FloppyDisk tracks usage automatically:
514
248
 
515
- Actions:
249
+ ```tsx
250
+ const { data } = useMyQuery();
251
+ // ^Only data changes will trigger a re-render
516
252
 
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.
253
+ const value = useMyQuery().data?.foo.bar.baz;
254
+ // ^Only data.foo.bar.baz changes will trigger a re-render
255
+ ```
519
256
 
520
- ```jsx
521
- function Actions() {
522
- const { fetch, forceFetch, reset } = useGitHubQuery.get();
257
+ ### Keyed Query (Dynamic Params)
523
258
 
524
- // Or like this:
525
- // const { isLoading, data, error, fetch, forceFetch, reset } = useGitHubQuery();
259
+ You can create parameterized queries:
526
260
 
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
- ```
261
+ ```tsx
262
+ import { getUserById, type GetUserByIdResponse } from '../utils';
536
263
 
537
- Options:
264
+ type MyQueryParam = { id: string };
538
265
 
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
- },
266
+ const userQuery = createQuery<GetUserByIdResponse, MyQueryParam>(
267
+ getUserById,
268
+ // { staleTime: 5000, revalidateOnFocus: false } <-- optional options
556
269
  );
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
270
  ```
573
271
 
574
- Get data or do something outside component:
575
-
576
- ```jsx
577
- const getData = () => console.log(useGitHubQuery.get().data);
578
- const resetQuery = () => useGitHubQuery.get().reset();
272
+ Use it with parameters:
579
273
 
580
- // Works just like createStores
581
- useMyQuery.get(/* ... */);
582
- useMyQuery.set(/* ... */);
583
- useMyQuery.subscribe(/* ... */);
584
- useMyQuery.getSubscribers(/* ... */);
274
+ ```tsx
275
+ function UserDetail({ id }) {
276
+ const useUserQuery = userQuery({ id: 1 });
277
+ const query = useUserQuery();
278
+ if (query.state === 'INITIAL') return <div>Loading...</div>;
279
+ if (query.error) return <div>Error: {query.error.message}</div>;
280
+ return <div>{JSON.stringify(query.data)}</div>;
281
+ }
585
282
  ```
586
283
 
587
- ### Single Query with Params
284
+ Each unique parameter creates its own cache entry.
588
285
 
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
- });
286
+ ### Infinite Query
595
287
 
596
- function PokemonPage() {
597
- const [currentPokemon, setCurrentPokemon] = useState();
598
- const { isLoading, data } = usePokemonQuery({ pokemon: currentPokemon });
288
+ FloppyDisk does **not provide** a dedicated "infinite query" API.\
289
+ Instead, it embraces a simpler and more flexible approach:
599
290
 
600
- if (isLoading) return <div>Loading...</div>;
291
+ > Infinite queries are just **composition** + **recursion**.
601
292
 
602
- return (
603
- <div>
604
- <h1>{data.name}</h1>
605
- <div>Weight: {data.weight}</div>
606
- </div>
607
- );
608
- }
609
- ```
293
+ Why? Because async state is already powerful enough:
610
294
 
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)
295
+ - keyed queries handle parameters
296
+ - components handle composition
297
+ - recursion handles pagination
612
298
 
613
- Get data or do something outside component:
299
+ No special abstraction needed.
614
300
 
615
- ```jsx
616
- const getDitto = () => {
617
- console.log(usePokemonQuery.get({ pokemon: 'ditto' }).data);
618
- };
301
+ Here is the example on how to implement infinite query properly:
619
302
 
620
- const resetDitto = () => {
621
- usePokemonQuery.get({ pokemon: 'ditto' }).reset();
303
+ ```tsx
304
+ type GetPostParams = {
305
+ cursor?: string; // For pagination
306
+ };
307
+ type GetPostsResponse = {
308
+ posts: Post[];
309
+ meta: { nextCursor: string };
622
310
  };
623
311
 
624
- function Actions() {
625
- return (
626
- <>
627
- <button onClick={getDitto}>Get Ditto Data</button>
628
- <button onClick={resetDitto}>Reset Ditto</button>
629
- </>
630
- );
312
+ const postsQuery = createQuery<GetPostsResponse, GetPostParams>(getPosts, {
313
+ staleTime: Infinity,
314
+ revalidateOnFocus: false,
315
+ revalidateOnReconnect: false,
316
+ });
317
+
318
+ function Main() {
319
+ return <Page cursor={undefined} />;
631
320
  }
632
- ```
633
321
 
634
- ### Paginated Query or Infinite Query
322
+ function Page({ cursor }: { cursor?: string }) {
323
+ const usePostsQuery = postsQuery({ cursor });
324
+ const { state, data, error } = usePostsQuery();
635
325
 
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
- },
643
- {
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
- },
649
- },
650
- );
651
-
652
- function PokemonListPage() {
653
- const { data = [], fetchNextPage, hasNextPage, isWaitingNextPage } = usePokemonsInfQuery();
326
+ if (state === 'INITIAL') return <div>Loading...</div>;
327
+ if (error) return <div>Error</div>;
654
328
 
655
329
  return (
656
- <div>
657
- {data.map((pokemon) => (
658
- <div key={pokemon.name}>{pokemon.name}</div>
330
+ <>
331
+ {data.posts.map((post) => (
332
+ <PostCard key={post.id} post={post} />
659
333
  ))}
660
- {isWaitingNextPage ? (
661
- <div>Loading more...</div>
662
- ) : (
663
- hasNextPage && <button onClick={fetchNextPage}>Load more</button>
664
- )}
665
- </div>
334
+ {data.meta.nextCursor && <LoadMore nextCursor={data.meta.nextCursor} />}
335
+ </>
666
336
  );
667
337
  }
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
-
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.
680
338
 
681
- ### Mutation
339
+ function LoadMore({ nextCursor }: { nextCursor?: string }) {
340
+ const [isNextPageRequested, setIsNextPageRequested] = useState(() => {
341
+ const stateOfNextPageQuery = postsQuery({ cursor: nextCursor }).getState();
342
+ return stateOfNextPageQuery.isPending || stateOfNextPageQuery.isSuccess;
343
+ });
682
344
 
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
- );
345
+ if (isNextPageRequested) {
346
+ return <Page cursor={nextCursor} />;
347
+ }
699
348
 
700
- function Login() {
701
- const { mutate, isWaiting } = useLoginMutation();
702
- const showToast = useToast();
703
- 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>
720
- );
349
+ return <BottomObserver onReachBottom={() => setIsNextPageRequested(true)} />;
721
350
  }
722
351
  ```
723
352
 
724
- Optimistic update:
353
+ When implementing infinite queries, it is **highly recommended to disable automatic revalidation**.
725
354
 
726
- ```jsx
727
- function SaveProduct() {
728
- const { mutate, isWaiting } = useEditProductMutation();
729
- const { getValues } = useFormContext();
355
+ Why?\
356
+ In an infinite list, users may scroll through many pages ("_doom-scrolling_").\
357
+ If revalidation is triggered:
730
358
 
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
- ```
359
+ - All previously loaded pages may re-execute
360
+ - Content at the top may change without the user noticing
361
+ - Layout shifts can occur unexpectedly
755
362
 
756
- > Example: [https://codesandbox.io/.../examples/react/mutation](https://codesandbox.io/p/sandbox/github/afiiif/floppy-disk-site/tree/main/examples/react/mutation)
363
+ This leads to a **confusing and unstable user experience**.\
364
+ Revalidating dozens of previously viewed pages rarely provides value to the user.
757
365
 
758
- <br><br>
366
+ ## SSR Guidance
759
367
 
760
- <p align="center">
761
- — ✨ 💾 ✨ —
762
- </p>
763
- <br>
368
+ FloppyDisk is designed primarily for **client-side [sync/async] state**.
764
369
 
765
- ## Important Notes
370
+ If your data is already fetched on the server (e.g. via SSR/ISR, Server Components, or Server Actions), then:
766
371
 
767
- Don't mutate. (unless you use Immer JS library or something similar)
372
+ > **You most likely don't need this library.**
768
373
 
769
- ```js
770
- import { createStore } from 'floppy-disk';
374
+ This is the same philosophy as TanStack Query. 💡
771
375
 
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
- ```
376
+ In many cases, developers mix SSR/ISR with client-side state because they want:
781
377
 
782
- Don't use conditional reactivity selector.
378
+ 1. Data to be rendered into HTML on the server
379
+ 2. The ability to **revalidate it on the client**
783
380
 
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
- ```
381
+ A common (but inefficient) approach is:
791
382
 
792
- No need to memoize the reactivity selector.
383
+ - fetch on the server
384
+ - hydrate it into a client-side cache
385
+ - then revalidate using a query library
793
386
 
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
- ```
387
+ While this works, it introduces additional complexity.
801
388
 
802
- No need to memoize the store key / query key.
389
+ Instead, we encourage a simpler approach:
803
390
 
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
- ```
391
+ > If your data is fetched on the server, revalidate it using **your framework's built-in mechanism** (e.g. Next.js route revalidation).
811
392
 
812
- <br>
393
+ Because of this philosophy, FloppyDisk **does not support** hydrating server-fetched data into the client store.
813
394
 
814
- ---
395
+ This keeps the mental model clean:
815
396
 
816
- <p align="center">
817
- View official documentation on <a href="https://floppy-disk.vercel.app/">floppy-disk.vercel.app</a>
818
- </p>
397
+ - server data → handled by the framework
398
+ - client async state handled by FloppyDisk