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.
- package/README.md +237 -685
- package/esm/index.d.mts +1 -0
- package/esm/index.mjs +1 -0
- package/esm/react/create-mutation.d.mts +151 -0
- package/esm/react/create-query.d.mts +344 -0
- package/esm/react/create-store.d.mts +28 -0
- package/esm/react/create-stores.d.mts +39 -0
- package/esm/react/use-isomorphic-layout-effect.d.mts +6 -0
- package/esm/react/use-mutation.d.mts +82 -0
- package/esm/react/use-store.d.mts +28 -0
- package/esm/react.d.mts +7 -0
- package/esm/react.mjs +697 -0
- package/esm/vanilla/basic.d.mts +13 -0
- package/esm/vanilla/hash.d.mts +7 -0
- package/esm/vanilla/store.d.mts +89 -0
- package/esm/vanilla.d.mts +3 -0
- package/esm/vanilla.mjs +82 -0
- package/index.d.ts +1 -0
- package/index.js +12 -0
- package/package.json +47 -45
- package/react/create-mutation.d.ts +151 -0
- package/react/create-query.d.ts +344 -0
- package/react/create-store.d.ts +28 -0
- package/react/create-stores.d.ts +39 -0
- package/react/use-isomorphic-layout-effect.d.ts +6 -0
- package/react/use-mutation.d.ts +82 -0
- package/react/use-store.d.ts +28 -0
- package/react.d.ts +7 -0
- package/react.js +705 -0
- package/ts_version_4.5_and_above_is_required.d.ts +0 -0
- package/vanilla/basic.d.ts +13 -0
- package/vanilla/hash.d.ts +7 -0
- package/vanilla/store.d.ts +89 -0
- package/vanilla.d.ts +3 -0
- package/vanilla.js +89 -0
- package/esm/index.d.ts +0 -8
- package/esm/index.js +0 -8
- package/esm/react/create-bi-direction-query.d.ts +0 -166
- package/esm/react/create-bi-direction-query.js +0 -74
- package/esm/react/create-mutation.d.ts +0 -39
- package/esm/react/create-mutation.js +0 -56
- package/esm/react/create-query.d.ts +0 -319
- package/esm/react/create-query.js +0 -434
- package/esm/react/create-store.d.ts +0 -38
- package/esm/react/create-store.js +0 -38
- package/esm/react/create-stores.d.ts +0 -61
- package/esm/react/create-stores.js +0 -99
- package/esm/react/with-context.d.ts +0 -5
- package/esm/react/with-context.js +0 -14
- package/esm/utils.d.ts +0 -24
- package/esm/utils.js +0 -31
- package/esm/vanilla/fetcher.d.ts +0 -27
- package/esm/vanilla/fetcher.js +0 -95
- package/esm/vanilla/init-store.d.ts +0 -24
- package/esm/vanilla/init-store.js +0 -51
- package/lib/index.d.ts +0 -8
- package/lib/index.js +0 -11
- package/lib/react/create-bi-direction-query.d.ts +0 -166
- package/lib/react/create-bi-direction-query.js +0 -78
- package/lib/react/create-mutation.d.ts +0 -39
- package/lib/react/create-mutation.js +0 -60
- package/lib/react/create-query.d.ts +0 -319
- package/lib/react/create-query.js +0 -438
- package/lib/react/create-store.d.ts +0 -38
- package/lib/react/create-store.js +0 -42
- package/lib/react/create-stores.d.ts +0 -61
- package/lib/react/create-stores.js +0 -104
- package/lib/react/with-context.d.ts +0 -5
- package/lib/react/with-context.js +0 -18
- package/lib/utils.d.ts +0 -24
- package/lib/utils.js +0 -39
- package/lib/vanilla/fetcher.d.ts +0 -27
- package/lib/vanilla/fetcher.js +0 -99
- package/lib/vanilla/init-store.d.ts +0 -24
- package/lib/vanilla/init-store.js +0 -55
- package/utils/package.json +0 -6
package/README.md
CHANGED
|
@@ -1,818 +1,370 @@
|
|
|
1
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
8
|
+
Comparison: https://github.com/afiiif/floppy-disk/tree/beta/comparison
|
|
9
9
|
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
// Will re-render when age or isSleeping is updated ^
|
|
146
|
-
...
|
|
147
|
-
}
|
|
22
|
+
```tsx
|
|
23
|
+
import { createStore } from 'floppy-disk/react';
|
|
148
24
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
25
|
+
const useDigimon = createStore({
|
|
26
|
+
age: 3,
|
|
27
|
+
level: 'Rookie',
|
|
28
|
+
});
|
|
154
29
|
```
|
|
155
30
|
|
|
156
|
-
|
|
31
|
+
You can use the store both inside and outside of React components.
|
|
157
32
|
|
|
158
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
51
|
+
<button onClick={evolve}>Evolve</button>
|
|
52
|
+
</>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
168
55
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
(
|
|
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
|
-
|
|
60
|
+
const order = ['In-Training', 'Rookie', 'Champion', 'Ultimate'];
|
|
61
|
+
const nextLevel = order[order.indexOf(level) + 1];
|
|
181
62
|
|
|
182
|
-
|
|
63
|
+
if (!nextLevel) return console.warn('Already at ultimate level');
|
|
183
64
|
|
|
184
|
-
|
|
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
|
-
|
|
69
|
+
### Differences from Zustand
|
|
197
70
|
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
80
|
+
Zustand examples:
|
|
258
81
|
|
|
259
|
-
```
|
|
260
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
91
|
+
FloppyDisk equivalents:
|
|
291
92
|
|
|
292
|
-
|
|
293
|
-
|
|
93
|
+
```tsx
|
|
94
|
+
const useExample1 = createStore({ value: 123 });
|
|
294
95
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
108
|
+
## Async State (Query & Mutation)
|
|
346
109
|
|
|
347
|
-
|
|
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
|
-
|
|
350
|
-
|
|
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
|
-
|
|
363
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
+
### Query vs Mutation
|
|
372
122
|
|
|
373
|
-
|
|
123
|
+
<details>
|
|
374
124
|
|
|
375
|
-
|
|
376
|
-
import { createStores } from 'floppy-disk';
|
|
125
|
+
<summary>Query → Read Operations</summary>
|
|
377
126
|
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
395
|
-
|
|
130
|
+
- no side effects
|
|
131
|
+
- no data mutation
|
|
132
|
+
- safe to run multiple times
|
|
396
133
|
|
|
397
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
);
|
|
407
|
-
}
|
|
136
|
+
- ✅ Retry mechanism (for transient failures)
|
|
137
|
+
- ✅ Revalidation (keep data fresh automatically)
|
|
138
|
+
- ✅ Caching & staleness control
|
|
408
139
|
|
|
409
|
-
|
|
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
|
-
|
|
142
|
+
- fetching data
|
|
143
|
+
- reading from storage
|
|
144
|
+
- running idempotent async logic
|
|
421
145
|
|
|
422
|
-
|
|
146
|
+
</details>
|
|
423
147
|
|
|
424
|
-
<
|
|
425
|
-
— ✨ 💾 ✨ —
|
|
426
|
-
</p>
|
|
427
|
-
<br>
|
|
148
|
+
<details>
|
|
428
149
|
|
|
429
|
-
|
|
150
|
+
<summary>Mutation → Write Operations</summary>
|
|
430
151
|
|
|
431
|
-
|
|
152
|
+
Mutations are designed for changing data.\
|
|
153
|
+
Examples:
|
|
432
154
|
|
|
433
|
-
|
|
155
|
+
- insert
|
|
156
|
+
- update
|
|
157
|
+
- delete
|
|
158
|
+
- triggering side effects
|
|
434
159
|
|
|
435
|
-
|
|
160
|
+
Because mutations are **not safe to repeat blindly**, FloppyDisk does **not** include:
|
|
436
161
|
|
|
437
|
-
|
|
162
|
+
- ❌ automatic retry
|
|
163
|
+
- ❌ automatic revalidation
|
|
164
|
+
- ❌ implicit re-execution
|
|
438
165
|
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
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
|
-
|
|
457
|
-
The value will be `true` if the query is called and still waiting for the response.
|
|
171
|
+
</details>
|
|
458
172
|
|
|
459
|
-
###
|
|
173
|
+
### Single Query
|
|
460
174
|
|
|
461
|
-
|
|
175
|
+
Create a query using `createQuery`:
|
|
462
176
|
|
|
463
177
|
```tsx
|
|
464
|
-
|
|
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
|
-
|
|
180
|
+
const myCoolQuery = createQuery(
|
|
181
|
+
myAsyncFn,
|
|
182
|
+
// { staleTime: 5000, revalidateOnFocus: false } <-- optional options
|
|
183
|
+
);
|
|
478
184
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
###
|
|
197
|
+
### Query State: Two Independent Dimensions
|
|
489
198
|
|
|
490
|
-
|
|
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
|
-
|
|
498
|
-
|
|
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
|
-
|
|
206
|
+
They are **independent**.
|
|
501
207
|
|
|
502
|
-
|
|
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
|
-
|
|
210
|
+
Just like the global store, FloppyDisk tracks usage automatically:
|
|
514
211
|
|
|
515
|
-
|
|
212
|
+
```tsx
|
|
213
|
+
const { data } = useMyQuery();
|
|
214
|
+
// ^Only data changes will trigger a re-render
|
|
516
215
|
|
|
517
|
-
|
|
518
|
-
|
|
216
|
+
const value = useMyQuery().data?.foo.bar.baz;
|
|
217
|
+
// ^Only data.foo.bar.baz changes will trigger a re-render
|
|
218
|
+
```
|
|
519
219
|
|
|
520
|
-
|
|
521
|
-
function Actions() {
|
|
522
|
-
const { fetch, forceFetch, reset } = useGitHubQuery.get();
|
|
220
|
+
### Keyed Query (Dynamic Params)
|
|
523
221
|
|
|
524
|
-
|
|
525
|
-
// const { isLoading, data, error, fetch, forceFetch, reset } = useGitHubQuery();
|
|
222
|
+
You can create parameterized queries:
|
|
526
223
|
|
|
527
|
-
|
|
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
|
-
|
|
227
|
+
type MyQueryParam = { id: string };
|
|
538
228
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
|
|
235
|
+
Use it with parameters:
|
|
575
236
|
|
|
576
|
-
```
|
|
577
|
-
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
-
|
|
247
|
+
Each unique parameter creates its own cache entry.
|
|
588
248
|
|
|
589
|
-
|
|
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
|
-
|
|
597
|
-
|
|
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
|
-
|
|
254
|
+
> Infinite queries are just **composition** + **recursion**.
|
|
601
255
|
|
|
602
|
-
|
|
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
|
-
|
|
258
|
+
- keyed queries handle parameters
|
|
259
|
+
- components handle composition
|
|
260
|
+
- recursion handles pagination
|
|
612
261
|
|
|
613
|
-
|
|
262
|
+
No special abstraction needed.
|
|
614
263
|
|
|
615
|
-
|
|
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
|
-
|
|
621
|
-
|
|
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
|
-
|
|
625
|
-
|
|
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
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
return i * 10;
|
|
648
|
-
},
|
|
278
|
+
staleTime: Infinity,
|
|
279
|
+
revalidateOnFocus: false,
|
|
280
|
+
revalidateOnReconnect: false,
|
|
649
281
|
},
|
|
650
282
|
);
|
|
651
283
|
|
|
652
|
-
function
|
|
653
|
-
|
|
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
|
-
|
|
657
|
-
{data.map(
|
|
658
|
-
<
|
|
296
|
+
<>
|
|
297
|
+
{data.posts.map(post => (
|
|
298
|
+
<PostCard key={post.id} post={post} />
|
|
659
299
|
))}
|
|
660
|
-
{
|
|
661
|
-
<
|
|
662
|
-
) : (
|
|
663
|
-
hasNextPage && <button onClick={fetchNextPage}>Load more</button>
|
|
300
|
+
{data.meta.nextCursor && (
|
|
301
|
+
<LoadMore nextCursor={data.meta.nextCursor} />
|
|
664
302
|
)}
|
|
665
|
-
|
|
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
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
705
|
-
|
|
706
|
-
|
|
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
|
-
|
|
325
|
+
When implementing infinite queries, it is **highly recommended to disable automatic revalidation**.
|
|
725
326
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
-
|
|
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
|
-
|
|
338
|
+
## SSR Guidance
|
|
759
339
|
|
|
760
|
-
|
|
761
|
-
— ✨ 💾 ✨ —
|
|
762
|
-
</p>
|
|
763
|
-
<br>
|
|
340
|
+
FloppyDisk is designed primarily for **client-side [sync/async] state**.
|
|
764
341
|
|
|
765
|
-
|
|
342
|
+
If your data is already fetched on the server (e.g. via SSR/ISR, Server Components, or Server Actions), then:
|
|
766
343
|
|
|
767
|
-
|
|
344
|
+
> **You most likely don't need this library.**
|
|
768
345
|
|
|
769
|
-
|
|
770
|
-
import { createStore } from 'floppy-disk';
|
|
346
|
+
This is the same philosophy as TanStack Query. 💡
|
|
771
347
|
|
|
772
|
-
|
|
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
|
-
|
|
350
|
+
1. Data to be rendered into HTML on the server
|
|
351
|
+
2. The ability to **revalidate it on the client**
|
|
783
352
|
|
|
784
|
-
|
|
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
|
-
|
|
355
|
+
- fetch on the server
|
|
356
|
+
- hydrate it into a client-side cache
|
|
357
|
+
- then revalidate using a query library
|
|
793
358
|
|
|
794
|
-
|
|
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
|
-
|
|
361
|
+
Instead, we encourage a simpler approach:
|
|
803
362
|
|
|
804
|
-
|
|
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
|
-
|
|
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
|
-
|
|
817
|
-
|
|
818
|
-
</p>
|
|
369
|
+
- server data → handled by the framework
|
|
370
|
+
- client async state → handled by FloppyDisk
|