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.
- package/README.md +256 -676
- 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,398 @@
|
|
|
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
|
+
Demo: https://afiiif.github.io/floppy-disk/
|
|
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
|
+
**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
|
-
|
|
16
|
+
## Global Store
|
|
112
17
|
|
|
113
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
return <button onClick={increaseAge}>Increase cat's age</button>;
|
|
122
|
-
}
|
|
123
|
-
```
|
|
20
|
+
```tsx
|
|
21
|
+
import { createStore } from 'floppy-disk/react';
|
|
124
22
|
|
|
125
|
-
|
|
23
|
+
const useDigimon = createStore({
|
|
24
|
+
age: 7,
|
|
25
|
+
level: 'Rookie',
|
|
26
|
+
});
|
|
27
|
+
```
|
|
126
28
|
|
|
127
|
-
|
|
29
|
+
You can use the store both inside and outside of React components.
|
|
128
30
|
|
|
129
|
-
```
|
|
130
|
-
function
|
|
131
|
-
const { age
|
|
132
|
-
|
|
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
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
...
|
|
51
|
+
<button onClick={evolve}>Evolve</button>
|
|
52
|
+
</>
|
|
53
|
+
);
|
|
147
54
|
}
|
|
148
55
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
...
|
|
153
|
-
}
|
|
154
|
-
```
|
|
56
|
+
// You can create a custom actions
|
|
57
|
+
const evolve = () => {
|
|
58
|
+
const { level } = useDigimon.getState();
|
|
155
59
|
|
|
156
|
-
|
|
60
|
+
const order = ['In-Training', 'Rookie', 'Champion', 'Ultimate'];
|
|
61
|
+
const nextLevel = order[order.indexOf(level) + 1];
|
|
157
62
|
|
|
158
|
-
|
|
63
|
+
if (!nextLevel) return console.warn('Already at ultimate level');
|
|
159
64
|
|
|
160
|
-
|
|
161
|
-
const alertCatAge = () => {
|
|
162
|
-
alert(useCatStore.get().age);
|
|
65
|
+
useDigimon.setState({ level: nextLevel });
|
|
163
66
|
};
|
|
67
|
+
```
|
|
164
68
|
|
|
165
|
-
|
|
166
|
-
useCatStore.set((state) => ({ isSleeping: !state.isSleeping }));
|
|
167
|
-
};
|
|
69
|
+
### Store Subscription
|
|
168
70
|
|
|
169
|
-
|
|
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
|
-
|
|
73
|
+
You can subscribe manually:
|
|
181
74
|
|
|
182
|
-
|
|
75
|
+
```tsx
|
|
76
|
+
const unsubscribe = useMyStore.subscribe((state, prev) => {
|
|
77
|
+
console.log('New state:', state);
|
|
78
|
+
});
|
|
183
79
|
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
84
|
+
FloppyDisk provides lifecycle hooks tied to subscription count.
|
|
197
85
|
|
|
198
|
-
```
|
|
199
|
-
const
|
|
200
|
-
|
|
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: (
|
|
208
|
-
console.log('
|
|
209
|
-
},
|
|
210
|
-
onSubscribe: (state) => {
|
|
211
|
-
console.log('onSubscribe', state);
|
|
90
|
+
onFirstSubscribe: () => {
|
|
91
|
+
console.log('First subscriber! We’re officially popular 🎉');
|
|
212
92
|
},
|
|
213
|
-
|
|
214
|
-
console.log('
|
|
93
|
+
onSubscribe: () => {
|
|
94
|
+
console.log('New subscriber joined. Welcome aboard 🫡');
|
|
215
95
|
},
|
|
216
|
-
|
|
217
|
-
console.log('
|
|
96
|
+
onUnsubscribe: () => {
|
|
97
|
+
console.log('Subscriber left... was it something I said? 😭');
|
|
218
98
|
},
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
+
If you're coming from Zustand, this should feel very familiar.\
|
|
109
|
+
Key differences:
|
|
258
110
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
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
|
-
|
|
119
|
+
```tsx
|
|
120
|
+
const useDate = create(new Date(2021, 01, 11));
|
|
291
121
|
|
|
292
|
-
|
|
293
|
-
|
|
122
|
+
const useCounter = create((set) => ({
|
|
123
|
+
value: 1,
|
|
124
|
+
increment: () => set((prev) => ({ value: prev.value + 1 })),
|
|
125
|
+
}));
|
|
126
|
+
```
|
|
294
127
|
|
|
295
|
-
|
|
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
|
-
|
|
306
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
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
|
-
|
|
145
|
+
## Async State (Query & Mutation)
|
|
348
146
|
|
|
349
|
-
|
|
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
|
-
|
|
363
|
-
|
|
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
|
-
|
|
152
|
+
Because of that, we intentionally avoid terms like "fetch" or "refetch".\
|
|
153
|
+
Instead, we use:
|
|
370
154
|
|
|
371
|
-
|
|
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
|
-
|
|
158
|
+
### Query vs Mutation
|
|
374
159
|
|
|
375
|
-
|
|
376
|
-
import { createStores } from 'floppy-disk';
|
|
160
|
+
<details>
|
|
377
161
|
|
|
378
|
-
|
|
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
|
-
|
|
395
|
-
|
|
164
|
+
Queries are designed for reading data.\
|
|
165
|
+
They assume:
|
|
396
166
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
404
|
-
<Control catId={catId} />
|
|
405
|
-
</>
|
|
406
|
-
);
|
|
407
|
-
}
|
|
171
|
+
Because of this, queries come with helpful defaults:
|
|
408
172
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
173
|
+
- ✅ Retry mechanism (for transient failures)
|
|
174
|
+
- ✅ Revalidation (keep data fresh automatically)
|
|
175
|
+
- ✅ Caching & staleness control
|
|
413
176
|
|
|
414
|
-
|
|
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
|
-
|
|
179
|
+
- fetching data
|
|
180
|
+
- reading from storage
|
|
181
|
+
- running idempotent async logic
|
|
421
182
|
|
|
422
|
-
|
|
183
|
+
</details>
|
|
423
184
|
|
|
424
|
-
<
|
|
425
|
-
— ✨ 💾 ✨ —
|
|
426
|
-
</p>
|
|
427
|
-
<br>
|
|
185
|
+
<details>
|
|
428
186
|
|
|
429
|
-
|
|
187
|
+
<summary>Mutation → Write Operations</summary>
|
|
430
188
|
|
|
431
|
-
|
|
189
|
+
Mutations are designed for changing data.\
|
|
190
|
+
Examples:
|
|
432
191
|
|
|
433
|
-
|
|
192
|
+
- insert
|
|
193
|
+
- update
|
|
194
|
+
- delete
|
|
195
|
+
- triggering side effects
|
|
434
196
|
|
|
435
|
-
|
|
197
|
+
Because mutations are **not safe to repeat blindly**, FloppyDisk does **not** include:
|
|
436
198
|
|
|
437
|
-
|
|
199
|
+
- ❌ automatic retry
|
|
200
|
+
- ❌ automatic revalidation
|
|
201
|
+
- ❌ implicit re-execution
|
|
438
202
|
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
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
|
-
|
|
457
|
-
The value will be `true` if the query is called and still waiting for the response.
|
|
208
|
+
</details>
|
|
458
209
|
|
|
459
|
-
###
|
|
210
|
+
### Single Query
|
|
460
211
|
|
|
461
|
-
|
|
212
|
+
Create a query using `createQuery`:
|
|
462
213
|
|
|
463
214
|
```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
|
-
```
|
|
215
|
+
import { createQuery } from 'floppy-disk/react';
|
|
476
216
|
|
|
477
|
-
|
|
217
|
+
const myCoolQuery = createQuery(
|
|
218
|
+
myAsyncFn,
|
|
219
|
+
// { staleTime: 5000, revalidateOnFocus: false } <-- optional options
|
|
220
|
+
);
|
|
478
221
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
###
|
|
234
|
+
### Query State: Two Independent Dimensions
|
|
489
235
|
|
|
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
|
-
});
|
|
236
|
+
FloppyDisk tracks two things separately:
|
|
496
237
|
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
|
|
243
|
+
They are **independent**.
|
|
501
244
|
|
|
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
|
-
```
|
|
245
|
+
### Automatic Re-render Optimization
|
|
512
246
|
|
|
513
|
-
|
|
247
|
+
Just like the global store, FloppyDisk tracks usage automatically:
|
|
514
248
|
|
|
515
|
-
|
|
249
|
+
```tsx
|
|
250
|
+
const { data } = useMyQuery();
|
|
251
|
+
// ^Only data changes will trigger a re-render
|
|
516
252
|
|
|
517
|
-
|
|
518
|
-
|
|
253
|
+
const value = useMyQuery().data?.foo.bar.baz;
|
|
254
|
+
// ^Only data.foo.bar.baz changes will trigger a re-render
|
|
255
|
+
```
|
|
519
256
|
|
|
520
|
-
|
|
521
|
-
function Actions() {
|
|
522
|
-
const { fetch, forceFetch, reset } = useGitHubQuery.get();
|
|
257
|
+
### Keyed Query (Dynamic Params)
|
|
523
258
|
|
|
524
|
-
|
|
525
|
-
// const { isLoading, data, error, fetch, forceFetch, reset } = useGitHubQuery();
|
|
259
|
+
You can create parameterized queries:
|
|
526
260
|
|
|
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
|
-
```
|
|
261
|
+
```tsx
|
|
262
|
+
import { getUserById, type GetUserByIdResponse } from '../utils';
|
|
536
263
|
|
|
537
|
-
|
|
264
|
+
type MyQueryParam = { id: string };
|
|
538
265
|
|
|
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
|
-
},
|
|
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
|
-
|
|
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
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
284
|
+
Each unique parameter creates its own cache entry.
|
|
588
285
|
|
|
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
|
-
});
|
|
286
|
+
### Infinite Query
|
|
595
287
|
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
|
|
291
|
+
> Infinite queries are just **composition** + **recursion**.
|
|
601
292
|
|
|
602
|
-
|
|
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
|
-
|
|
295
|
+
- keyed queries handle parameters
|
|
296
|
+
- components handle composition
|
|
297
|
+
- recursion handles pagination
|
|
612
298
|
|
|
613
|
-
|
|
299
|
+
No special abstraction needed.
|
|
614
300
|
|
|
615
|
-
|
|
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
|
-
|
|
621
|
-
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
-
|
|
322
|
+
function Page({ cursor }: { cursor?: string }) {
|
|
323
|
+
const usePostsQuery = postsQuery({ cursor });
|
|
324
|
+
const { state, data, error } = usePostsQuery();
|
|
635
325
|
|
|
636
|
-
|
|
637
|
-
|
|
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
|
-
|
|
657
|
-
{data.map((
|
|
658
|
-
<
|
|
330
|
+
<>
|
|
331
|
+
{data.posts.map((post) => (
|
|
332
|
+
<PostCard key={post.id} post={post} />
|
|
659
333
|
))}
|
|
660
|
-
{
|
|
661
|
-
|
|
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
|
-
|
|
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
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
-
|
|
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
|
-
|
|
353
|
+
When implementing infinite queries, it is **highly recommended to disable automatic revalidation**.
|
|
725
354
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
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
|
-
|
|
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
|
-
|
|
366
|
+
## SSR Guidance
|
|
759
367
|
|
|
760
|
-
|
|
761
|
-
— ✨ 💾 ✨ —
|
|
762
|
-
</p>
|
|
763
|
-
<br>
|
|
368
|
+
FloppyDisk is designed primarily for **client-side [sync/async] state**.
|
|
764
369
|
|
|
765
|
-
|
|
370
|
+
If your data is already fetched on the server (e.g. via SSR/ISR, Server Components, or Server Actions), then:
|
|
766
371
|
|
|
767
|
-
|
|
372
|
+
> **You most likely don't need this library.**
|
|
768
373
|
|
|
769
|
-
|
|
770
|
-
import { createStore } from 'floppy-disk';
|
|
374
|
+
This is the same philosophy as TanStack Query. 💡
|
|
771
375
|
|
|
772
|
-
|
|
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
|
-
|
|
378
|
+
1. Data to be rendered into HTML on the server
|
|
379
|
+
2. The ability to **revalidate it on the client**
|
|
783
380
|
|
|
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
|
-
```
|
|
381
|
+
A common (but inefficient) approach is:
|
|
791
382
|
|
|
792
|
-
|
|
383
|
+
- fetch on the server
|
|
384
|
+
- hydrate it into a client-side cache
|
|
385
|
+
- then revalidate using a query library
|
|
793
386
|
|
|
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
|
-
```
|
|
387
|
+
While this works, it introduces additional complexity.
|
|
801
388
|
|
|
802
|
-
|
|
389
|
+
Instead, we encourage a simpler approach:
|
|
803
390
|
|
|
804
|
-
|
|
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
|
-
|
|
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
|
-
|
|
817
|
-
|
|
818
|
-
</p>
|
|
397
|
+
- server data → handled by the framework
|
|
398
|
+
- client async state → handled by FloppyDisk
|