floppy-disk 3.2.0 → 3.2.2
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 +412 -126
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A unified state model for **sync & async** data.
|
|
4
4
|
|
|
5
|
-
If you know [Zustand](https://zustand.docs.pmnd.rs) & [TanStack-Query](https://tanstack.com/query), you already know FloppyDisk.\
|
|
5
|
+
If you know [Zustand](https://zustand.docs.pmnd.rs) & [TanStack-Query](https://tanstack.com/query), you already know FloppyDisk(.ts).\
|
|
6
6
|
It keeps what works, removes unnecessary complexity, and unifies everything into a simpler API.\
|
|
7
7
|
No relearning—just a better experience.
|
|
8
8
|
|
|
@@ -16,79 +16,125 @@ Demo: https://afiiif.github.io/floppy-disk/
|
|
|
16
16
|
npm install floppy-disk
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
##
|
|
19
|
+
## In short, it is:
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
- **Like Zustand, but has additional capability:**
|
|
22
|
+
- No selector: auto optimize re-render.
|
|
23
|
+
- Store events: `onSubscribe`, `onUnSubscribe`, etc.
|
|
24
|
+
- Easier to set initial state from server
|
|
25
|
+
- Smaller bundle
|
|
26
|
+
- **Like TanStack Query, but:**
|
|
27
|
+
- DX is very similar to Zustand → One mental model for sync & async
|
|
28
|
+
- Extremely less bundle size → With almost the same capabilities
|
|
29
|
+
|
|
30
|
+
# Store (Global State)
|
|
31
|
+
|
|
32
|
+
A store is a global state container that can be used both **inside and outside** React.\
|
|
33
|
+
With FloppyDisk, creating a store is simple:
|
|
22
34
|
|
|
23
35
|
```tsx
|
|
24
36
|
import { createStore } from "floppy-disk/react";
|
|
25
37
|
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
38
|
+
const useLawn = createStore({
|
|
39
|
+
plants: 3,
|
|
40
|
+
zombies: 1,
|
|
29
41
|
});
|
|
30
42
|
```
|
|
31
43
|
|
|
32
|
-
|
|
44
|
+
Use it inside a component:
|
|
33
45
|
|
|
34
46
|
```tsx
|
|
35
|
-
function
|
|
36
|
-
const {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
// Changes to `level` will NOT trigger a re-render.
|
|
47
|
+
function MyPlants() {
|
|
48
|
+
const { plants } = useLawn(); // No selectors needed.
|
|
49
|
+
|
|
50
|
+
return <div>Plants: {plants}</div>; // Only re-render when plants state changes
|
|
40
51
|
}
|
|
52
|
+
```
|
|
41
53
|
|
|
42
|
-
|
|
43
|
-
return (
|
|
44
|
-
<>
|
|
45
|
-
<button
|
|
46
|
-
onClick={() => {
|
|
47
|
-
// You can setState directly
|
|
48
|
-
useDigimon.setState((prev) => ({ age: prev.age + 1 }));
|
|
49
|
-
}}
|
|
50
|
-
>
|
|
51
|
-
Increase digimon's age
|
|
52
|
-
</button>
|
|
54
|
+
Update the state **anywhere**:
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
);
|
|
57
|
-
}
|
|
56
|
+
```tsx
|
|
57
|
+
const addPlant = () => {
|
|
58
|
+
useLawn.setState(prev => ({ plants: prev.plants + 1 }));
|
|
59
|
+
};
|
|
60
|
+
```
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
const evolve = () => {
|
|
61
|
-
const { level } = useDigimon.getState();
|
|
62
|
+
## Updating State
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
const nextLevel = order[order.indexOf(level) + 1];
|
|
64
|
+
You can update state using `setState`:
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
```tsx
|
|
67
|
+
const useLawn = createStore({ plants: 3, zombies: 1 });
|
|
68
|
+
// Current state: { plants: 3, zombies: 1 }
|
|
67
69
|
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
+
useLawn.setState({ plants: 5, zombies: 5 });
|
|
71
|
+
// Current state: { plants: 5, zombies: 5 }
|
|
72
|
+
|
|
73
|
+
useLawn.setState({ plants: 7 }); // 👈 Partial update
|
|
74
|
+
// Current state: { plants: 7, zombies: 5 }
|
|
75
|
+
|
|
76
|
+
useLawn.setState(prev => ({ plants: prev.plant + 2 })); // 👈 Using function
|
|
77
|
+
// Current state: { plants: 9, zombies: 5 }
|
|
70
78
|
```
|
|
71
79
|
|
|
72
|
-
|
|
80
|
+
## Reading State Outside React
|
|
73
81
|
|
|
74
|
-
|
|
82
|
+
Stores are not limited to React. You can access state **anywhere**:
|
|
75
83
|
|
|
76
|
-
|
|
84
|
+
```tsx
|
|
85
|
+
const state = useLawn.getState();
|
|
86
|
+
console.log(state.plants);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Subscribing to Changes
|
|
90
|
+
|
|
91
|
+
You can subscribe to state changes:
|
|
77
92
|
|
|
78
93
|
```tsx
|
|
79
|
-
const
|
|
80
|
-
console.log("
|
|
94
|
+
const unsubscribeLawn = useLawn.subscribe((currentState, prevState) => {
|
|
95
|
+
console.log("State changed:", currentState);
|
|
81
96
|
});
|
|
82
97
|
|
|
83
98
|
// Later
|
|
84
|
-
|
|
99
|
+
unsubscribeLawn(); // when you no longer need it
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Transient Updates (No Re-render)
|
|
103
|
+
|
|
104
|
+
Sometimes you want to listen to changes **without triggering re-renders**.
|
|
105
|
+
You can do this by simply subscribing to the store:
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
function MyComponent() {
|
|
109
|
+
|
|
110
|
+
useEffect(() => useLawn.subscribe((currentState, prevState) => {
|
|
111
|
+
if (currentState.zombies !== prevState.zombies) {
|
|
112
|
+
console.log("Zombie updated");
|
|
113
|
+
// Do something ...
|
|
114
|
+
}
|
|
115
|
+
}), []);
|
|
116
|
+
|
|
117
|
+
...
|
|
118
|
+
}
|
|
85
119
|
```
|
|
86
120
|
|
|
87
|
-
|
|
121
|
+
# Store Events
|
|
122
|
+
|
|
123
|
+
FloppyDisk provides lifecycle events to help you understand when **subscribers are added or removed**, and react accordingly.
|
|
124
|
+
|
|
125
|
+
Each store exposes the following events:
|
|
126
|
+
|
|
127
|
+
- `onFirstSubscribe` → triggered right after the first subscriber is added
|
|
128
|
+
- `onSubscribe` → triggered after any subscriber is added (including the first)
|
|
129
|
+
- `onUnsubscribe` → triggered right after a subscriber is removed
|
|
130
|
+
- `onLastUnsubscribe` → triggered after the last subscriber is removed
|
|
88
131
|
|
|
89
132
|
```tsx
|
|
90
|
-
const
|
|
91
|
-
{
|
|
133
|
+
const useLawn = createStore(
|
|
134
|
+
{
|
|
135
|
+
plants: 3,
|
|
136
|
+
zombies: 1,
|
|
137
|
+
},
|
|
92
138
|
{
|
|
93
139
|
onFirstSubscribe: () => {
|
|
94
140
|
console.log("First subscriber! We’re officially popular 🎉");
|
|
@@ -106,46 +152,47 @@ const useTowerDefense = createStore(
|
|
|
106
152
|
);
|
|
107
153
|
```
|
|
108
154
|
|
|
109
|
-
|
|
155
|
+
## Use Cases
|
|
110
156
|
|
|
111
|
-
|
|
112
|
-
|
|
157
|
+
These events let you control resource lifecycle based on usage.\
|
|
158
|
+
You know exactly:
|
|
159
|
+
- when something starts being used
|
|
160
|
+
- when it's no longer needed
|
|
113
161
|
|
|
114
|
-
1. **No Selectors Needed**\
|
|
115
|
-
You don't need selectors when using hooks.
|
|
116
|
-
FloppyDisk automatically tracks which parts of the state are used and optimizes re-renders accordingly.
|
|
117
|
-
2. **Object-Only Store Initialization**\
|
|
118
|
-
In FloppyDisk, stores **must** be initialized with an object. Primitive values or function initializers are not allowed.
|
|
119
162
|
|
|
120
|
-
|
|
163
|
+
**Perfect for:**
|
|
164
|
+
- opening / closing connections
|
|
165
|
+
- starting / stopping polling
|
|
166
|
+
- initializing expensive resources
|
|
167
|
+
- adding / removing window event listeners
|
|
121
168
|
|
|
122
|
-
|
|
123
|
-
const useDate = create(new Date(2021, 01, 11));
|
|
124
|
-
|
|
125
|
-
const useCounter = create((set) => ({
|
|
126
|
-
value: 1,
|
|
127
|
-
increment: () => set((prev) => ({ value: prev.value + 1 })),
|
|
128
|
-
}));
|
|
129
|
-
```
|
|
169
|
+
## State Changes Event
|
|
130
170
|
|
|
131
|
-
|
|
171
|
+
Sometimes you want to observe state changes **without becoming a subscriber**.
|
|
132
172
|
|
|
133
|
-
|
|
134
|
-
|
|
173
|
+
In addition to lifecycle events, FloppyDisk provides `onStateChange` event.
|
|
174
|
+
It listens to changes, but does NOT count as a subscriber.
|
|
175
|
+
It Acts like a "**spy**" on state updates.
|
|
135
176
|
|
|
136
|
-
|
|
137
|
-
const increment = () => useCounter.setState((prev) => ({ value: prev.value + 1 }));
|
|
138
|
-
// Unlike Zustand, defining actions inside the store is **discouraged** in FloppyDisk.
|
|
139
|
-
// This improves tree-shakeability and keeps your store minimal.
|
|
177
|
+
Useful for devtools, logging, or debugging state changes.
|
|
140
178
|
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
179
|
+
```tsx
|
|
180
|
+
const useLawn = createStore(
|
|
181
|
+
{
|
|
182
|
+
plants: 3,
|
|
183
|
+
zombies: 1,
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
onStateChange: (currentState, prevState) => {
|
|
187
|
+
if (currentState.zombies === 0 && prevState.zombies > 30) {
|
|
188
|
+
toast("🏆 Achievement unlocked! Clear more than 30 zombies at once!");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
);
|
|
146
193
|
```
|
|
147
194
|
|
|
148
|
-
|
|
195
|
+
# Query & Mutation Store for Async State
|
|
149
196
|
|
|
150
197
|
FloppyDisk also provides a powerful async state layer, inspired by [TanStack-Query](https://tanstack.com/query) but with a simpler API.
|
|
151
198
|
|
|
@@ -158,7 +205,7 @@ Instead, we use:
|
|
|
158
205
|
- **execute** → run the async operation (same as "fetch" in TanStack-Query)
|
|
159
206
|
- **revalidate** → re-run while keeping existing data (same as "refetch" in TanStack-Query)
|
|
160
207
|
|
|
161
|
-
|
|
208
|
+
## Query vs Mutation
|
|
162
209
|
|
|
163
210
|
<details>
|
|
164
211
|
|
|
@@ -210,64 +257,56 @@ If you need retry mechanism, then you can always add it manually.
|
|
|
210
257
|
|
|
211
258
|
</details>
|
|
212
259
|
|
|
213
|
-
|
|
260
|
+
## Single Query
|
|
214
261
|
|
|
215
262
|
Create a query using `createQuery`:
|
|
216
263
|
|
|
217
264
|
```tsx
|
|
218
265
|
import { createQuery } from "floppy-disk/react";
|
|
219
266
|
|
|
220
|
-
const
|
|
267
|
+
const myQuery = createQuery(
|
|
221
268
|
myAsyncFn,
|
|
222
269
|
// { staleTime: 5000, revalidateOnFocus: false } <-- optional options
|
|
223
270
|
);
|
|
271
|
+
```
|
|
224
272
|
|
|
225
|
-
|
|
273
|
+
Use it inside your component:
|
|
226
274
|
|
|
227
|
-
|
|
275
|
+
```tsx
|
|
276
|
+
const useMyQuery = myQuery();
|
|
228
277
|
|
|
229
278
|
function MyComponent() {
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
if (
|
|
233
|
-
return <div>{
|
|
279
|
+
const { data, error } = useMyQuery();
|
|
280
|
+
|
|
281
|
+
if (!data && !error) return <div>Loading...</div>;
|
|
282
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
283
|
+
|
|
284
|
+
return <div>{data.foo} {data.bar}</div>;
|
|
234
285
|
}
|
|
235
286
|
```
|
|
236
287
|
|
|
237
|
-
|
|
288
|
+
## Query State: Two Independent Dimensions
|
|
238
289
|
|
|
239
290
|
FloppyDisk tracks two things separately:
|
|
240
291
|
|
|
241
|
-
- Is it running? → `isPending
|
|
242
|
-
|
|
243
|
-
- What's the result? → `state`\
|
|
244
|
-
(value: `INITIAL | 'SUCCESS' | 'ERROR' | 'SUCCESS_BUT_REVALIDATION_ERROR'`)
|
|
292
|
+
- Is it running? → `isPending`
|
|
293
|
+
- What's the result? → `state`
|
|
245
294
|
|
|
246
295
|
They are **independent**.
|
|
247
296
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
Just like the global store, FloppyDisk tracks usage automatically:
|
|
251
|
-
|
|
252
|
-
```tsx
|
|
253
|
-
const { data } = useMyQuery();
|
|
254
|
-
// ^Only data changes will trigger a re-render
|
|
255
|
-
|
|
256
|
-
const value = useMyQuery().data?.foo.bar.baz;
|
|
257
|
-
// ^Only data.foo.bar.baz changes will trigger a re-render
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
### Keyed Query (Dynamic Params)
|
|
297
|
+
## Keyed Query (Dynamic Params)
|
|
261
298
|
|
|
262
299
|
You can create parameterized queries:
|
|
263
300
|
|
|
264
301
|
```tsx
|
|
265
|
-
import {
|
|
302
|
+
import { createQuery } from "floppy-disk/react";
|
|
266
303
|
|
|
267
|
-
type
|
|
304
|
+
import { getZombieById, type GetZombieByIdResponse } from "../utils"; // Your own module
|
|
268
305
|
|
|
269
|
-
|
|
270
|
-
|
|
306
|
+
type ZombieQueryParam = { id: string };
|
|
307
|
+
|
|
308
|
+
const zombieQuery = createQuery<GetZombieByIdResponse, ZombieQueryParam>(
|
|
309
|
+
getZombieById,
|
|
271
310
|
// { staleTime: 5000, revalidateOnFocus: false } <-- optional options
|
|
272
311
|
);
|
|
273
312
|
```
|
|
@@ -275,18 +314,44 @@ const userQuery = createQuery<GetUserByIdResponse, MyQueryParam>(
|
|
|
275
314
|
Use it with parameters:
|
|
276
315
|
|
|
277
316
|
```tsx
|
|
278
|
-
function
|
|
279
|
-
const
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
if (
|
|
283
|
-
return <div>{
|
|
317
|
+
function ZombieDetail({ id }) {
|
|
318
|
+
const useZombieQuery = zombieQuery({ id });
|
|
319
|
+
const { data, error } = useZombieQuery();
|
|
320
|
+
|
|
321
|
+
if (!data && !error) return <div>Loading...</div>;
|
|
322
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
323
|
+
|
|
324
|
+
return <div>Name: {data.name}, hp: {data.hp}</div>;
|
|
284
325
|
}
|
|
285
326
|
```
|
|
286
327
|
|
|
287
328
|
Each unique parameter creates its own cache entry.
|
|
288
329
|
|
|
289
|
-
|
|
330
|
+
## Store Inheritance
|
|
331
|
+
|
|
332
|
+
Queries in FloppyDisk are built on top of the core store.
|
|
333
|
+
This means every query inherits the same capabilities, such as `subscribe`, `getState`, and store events.
|
|
334
|
+
It also gets **automatic reactivity** out of the box, so components rerender only when the state they use actually changes.
|
|
335
|
+
|
|
336
|
+
```tsx
|
|
337
|
+
const { data } = useMyQuery();
|
|
338
|
+
// ^Only data changes will trigger a re-render
|
|
339
|
+
|
|
340
|
+
const value = useMyQuery().data?.foo.bar.baz;
|
|
341
|
+
// ^Only data.foo.bar.baz changes will trigger a re-render
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Get state outside React:
|
|
345
|
+
|
|
346
|
+
```tsx
|
|
347
|
+
const myPlantQuery = createQuery<MyPlantResponse>(getMyPlant); // Query without paramerer
|
|
348
|
+
const zombieQuery = createQuery<GetZombieByIdResponse, { id: string }>(getZombieById); // Parameterized query
|
|
349
|
+
|
|
350
|
+
const getMyPlantQueryData = () => myPlantQuery().getState().data;
|
|
351
|
+
const getUserQueryData = ({ id }) => zombieQuery({ id }).getState().data;
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Infinite Query
|
|
290
355
|
|
|
291
356
|
FloppyDisk does **not provide** a dedicated "infinite query" API.\
|
|
292
357
|
Instead, it embraces a simpler and more flexible approach:
|
|
@@ -304,15 +369,15 @@ No special abstraction needed.
|
|
|
304
369
|
Here is the example on how to implement infinite query properly:
|
|
305
370
|
|
|
306
371
|
```tsx
|
|
307
|
-
type
|
|
372
|
+
type GetPlantParams = {
|
|
308
373
|
cursor?: string; // For pagination
|
|
309
374
|
};
|
|
310
|
-
type
|
|
311
|
-
|
|
375
|
+
type GetPlantsResponse = {
|
|
376
|
+
plants: Plant[];
|
|
312
377
|
meta: { nextCursor: string };
|
|
313
378
|
};
|
|
314
379
|
|
|
315
|
-
const
|
|
380
|
+
const plantsQuery = createQuery<GetPlantsResponse, GetPlantParams>(getPlants, {
|
|
316
381
|
staleTime: Infinity,
|
|
317
382
|
revalidateOnFocus: false,
|
|
318
383
|
revalidateOnReconnect: false,
|
|
@@ -323,16 +388,16 @@ function Main() {
|
|
|
323
388
|
}
|
|
324
389
|
|
|
325
390
|
function Page({ cursor }: { cursor?: string }) {
|
|
326
|
-
const
|
|
327
|
-
const { state, data, error } =
|
|
391
|
+
const usePlantsQuery = plantsQuery({ cursor });
|
|
392
|
+
const { state, data, error } = usePlantsQuery();
|
|
328
393
|
|
|
329
|
-
if (
|
|
394
|
+
if (!data && !error) return <div>Loading...</div>;
|
|
330
395
|
if (error) return <div>Error</div>;
|
|
331
396
|
|
|
332
397
|
return (
|
|
333
398
|
<>
|
|
334
|
-
{data.
|
|
335
|
-
<
|
|
399
|
+
{data.plants.map((plant) => (
|
|
400
|
+
<PlantCard key={plant.id} plant={plant} />
|
|
336
401
|
))}
|
|
337
402
|
{data.meta.nextCursor && <LoadMore nextCursor={data.meta.nextCursor} />}
|
|
338
403
|
</>
|
|
@@ -341,7 +406,7 @@ function Page({ cursor }: { cursor?: string }) {
|
|
|
341
406
|
|
|
342
407
|
function LoadMore({ nextCursor }: { nextCursor?: string }) {
|
|
343
408
|
const [isNextPageRequested, setIsNextPageRequested] = useState(() => {
|
|
344
|
-
const stateOfNextPageQuery =
|
|
409
|
+
const stateOfNextPageQuery = plantsQuery({ cursor: nextCursor }).getState();
|
|
345
410
|
return stateOfNextPageQuery.isPending || stateOfNextPageQuery.isSuccess;
|
|
346
411
|
});
|
|
347
412
|
|
|
@@ -349,7 +414,7 @@ function LoadMore({ nextCursor }: { nextCursor?: string }) {
|
|
|
349
414
|
return <Page cursor={nextCursor} />;
|
|
350
415
|
}
|
|
351
416
|
|
|
352
|
-
return <
|
|
417
|
+
return <DomObserver onReachBottom={() => setIsNextPageRequested(true)} />;
|
|
353
418
|
}
|
|
354
419
|
```
|
|
355
420
|
|
|
@@ -366,11 +431,104 @@ If revalidation is triggered:
|
|
|
366
431
|
This leads to a **confusing and unstable user experience**.\
|
|
367
432
|
Revalidating dozens of previously viewed pages rarely provides value to the user.
|
|
368
433
|
|
|
369
|
-
##
|
|
434
|
+
## Mutation
|
|
435
|
+
|
|
436
|
+
Mutations are used to perform write operations—such as creating, updating, or deleting data.
|
|
437
|
+
|
|
438
|
+
FloppyDisk provides two ways to use mutations:
|
|
439
|
+
- Global mutation → shared state across components
|
|
440
|
+
- Local mutation → isolated per component
|
|
441
|
+
|
|
442
|
+
### Global Mutation
|
|
443
|
+
|
|
444
|
+
Create a global mutation using `createMutation`:
|
|
445
|
+
|
|
446
|
+
```tsx
|
|
447
|
+
import { createMutation } from "floppy-disk/react";
|
|
448
|
+
|
|
449
|
+
const useUpdatePlant = createMutation(updatePlant, {
|
|
450
|
+
onSuccess: (data) => {
|
|
451
|
+
console.log("Global success:", data);
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
Use it inside any component:
|
|
457
|
+
|
|
458
|
+
```tsx
|
|
459
|
+
function UpdateButton() {
|
|
460
|
+
const { isPending } = useUpdatePlant();
|
|
461
|
+
|
|
462
|
+
return (
|
|
463
|
+
<button
|
|
464
|
+
disabled={isPending}
|
|
465
|
+
onClick={() => {
|
|
466
|
+
useUpdatePlant.execute({ id: 1, name: "Sunflower", hp: 300 });
|
|
467
|
+
}}
|
|
468
|
+
>
|
|
469
|
+
Update User
|
|
470
|
+
</button>
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
Characteristics:
|
|
476
|
+
- Shared across all components
|
|
477
|
+
- Single source of truth for mutation state
|
|
478
|
+
- Can be triggered from anywhere using `.execute()`
|
|
479
|
+
- Useful for global actions (e.g. forms, shared actions)
|
|
480
|
+
|
|
481
|
+
### Local Mutation
|
|
482
|
+
|
|
483
|
+
Create a local mutation using `useMutation`:
|
|
484
|
+
|
|
485
|
+
```tsx
|
|
486
|
+
import { useMutation } from "floppy-disk/react";
|
|
487
|
+
|
|
488
|
+
function UpdateForm() {
|
|
489
|
+
const [result, { execute }] = useMutation(updateZombie, {
|
|
490
|
+
onSuccess: (data) => {
|
|
491
|
+
console.log("Local success:", data);
|
|
492
|
+
},
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
return (
|
|
496
|
+
<div>
|
|
497
|
+
<button
|
|
498
|
+
disabled={result.isPending}
|
|
499
|
+
onClick={() => {
|
|
500
|
+
execute({ id: 27, name: "Gargantuar", hp: 3000 });
|
|
501
|
+
}}
|
|
502
|
+
>
|
|
503
|
+
Submit
|
|
504
|
+
</button>
|
|
505
|
+
|
|
506
|
+
{result.isError && <div>Error occurred</div>}
|
|
507
|
+
</div>
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
Characteristics:
|
|
513
|
+
- Isolated per component instance
|
|
514
|
+
- Each usage has its own state
|
|
515
|
+
- No shared side effects
|
|
516
|
+
- Ideal for component-scoped interactions
|
|
517
|
+
|
|
518
|
+
### Execution
|
|
519
|
+
|
|
520
|
+
Both global and local mutations:
|
|
521
|
+
|
|
522
|
+
- Execute via `execute(input)`
|
|
523
|
+
- Return a Promise that **never throw**.\
|
|
524
|
+
It returns `{ variable: TVariable; data?: TData; error?: TError }` instead.
|
|
525
|
+
- Update state automatically (`isPending`, `isSuccess`, `isError`, etc.)
|
|
526
|
+
|
|
527
|
+
# SSR Guidance
|
|
370
528
|
|
|
371
529
|
Examples for using stores and queries in SSR with isolated data (no shared state between users).
|
|
372
530
|
|
|
373
|
-
|
|
531
|
+
## Initialize Store State from Server
|
|
374
532
|
|
|
375
533
|
```tsx
|
|
376
534
|
const useCountStore = createStore({ count: 0 });
|
|
@@ -384,7 +542,7 @@ function Page({ initialCount }) {
|
|
|
384
542
|
}
|
|
385
543
|
```
|
|
386
544
|
|
|
387
|
-
|
|
545
|
+
## Initialize Query Data from Server
|
|
388
546
|
|
|
389
547
|
```tsx
|
|
390
548
|
async function MyServerComponent() {
|
|
@@ -404,3 +562,131 @@ function MyClientComponent({ initialData }) {
|
|
|
404
562
|
return <>count is {data.count}</>; // Output: count is 3
|
|
405
563
|
}
|
|
406
564
|
```
|
|
565
|
+
|
|
566
|
+
# Query State Machine
|
|
567
|
+
|
|
568
|
+
This is how the query state transition flow looks like:
|
|
569
|
+
|
|
570
|
+
```
|
|
571
|
+
initial failed, won't retry
|
|
572
|
+
┌────────────────────────────┐ ┌────────────────────────────┐
|
|
573
|
+
│ isPending: false │ ■│ isPending: false │
|
|
574
|
+
│ isRevalidating: false │ │ isRevalidating: false │
|
|
575
|
+
│ │ ┌──────────────────▶ │ │
|
|
576
|
+
│ state: "INITIAL" │ │ ■│ state: "ERROR" │
|
|
577
|
+
│ isSuccess: false │ │ │ isSuccess: false │
|
|
578
|
+
│ data: undefined │ │ │ data: undefined │
|
|
579
|
+
│ dataUpdatedAt: undefined │ │ │ dataUpdatedAt: undefined │
|
|
580
|
+
│ dataStaleAt: undefined │ │ │ dataStaleAt: undefined │
|
|
581
|
+
│ isError: false │ │ ■│ isError: true │
|
|
582
|
+
│ error: undefined │ │ ■│ error: <TError> │
|
|
583
|
+
│ errorUpdatedAt: undefined │ │ ■│ errorUpdatedAt: <number> │
|
|
584
|
+
│ │ │ │ │
|
|
585
|
+
│ willRetryAt: undefined │ │ │ willRetryAt: undefined │
|
|
586
|
+
│ isRetrying: false │ │ •│ isRetrying: false │
|
|
587
|
+
│ retryCount: 0 │ │ •│ retryCount: 0 (reset) │
|
|
588
|
+
└─────────────┬──────────────┘ │ └────────────────────────────┘
|
|
589
|
+
│ │
|
|
590
|
+
│ execute │
|
|
591
|
+
▼ │ waiting retry delay
|
|
592
|
+
┌────────────────────────────┐ (N) ┌────────────────────────────┐
|
|
593
|
+
■│ isPending: true [ƒ]│ │ ■│ isPending: false │
|
|
594
|
+
│ isRevalidating: false │ fail │ │ isRevalidating: false │
|
|
595
|
+
│ ├──────────▶ Should retry? ────(Y)────▶ │ │
|
|
596
|
+
│ state: "INITIAL" │ ▲ │ state: "INITIAL" │
|
|
597
|
+
│ isSuccess: false │ │ │ isSuccess: false │
|
|
598
|
+
│ data: undefined │ │ │ data: undefined │
|
|
599
|
+
│ dataUpdatedAt: undefined │ │ │ dataUpdatedAt: undefined │
|
|
600
|
+
│ dataStaleAt: undefined │ │ │ dataStaleAt: undefined │
|
|
601
|
+
│ isError: false │ │ │ isError: false │
|
|
602
|
+
│ error: undefined │ │ │ error: undefined │
|
|
603
|
+
│ errorUpdatedAt: undefined │ │ │ errorUpdatedAt: undefined │
|
|
604
|
+
│ │ │ │ │
|
|
605
|
+
│ willRetryAt: undefined │ │ ■│ willRetryAt: <number> │
|
|
606
|
+
│ isRetrying: false │ │ │ isRetrying: false │
|
|
607
|
+
│ retryCount: 0 │ │ │ retryCount: <number> │
|
|
608
|
+
└─────────────┬──────────────┘ │ └─────────────┬──────────────┘
|
|
609
|
+
│ │ │
|
|
610
|
+
│ success │ │ retrying
|
|
611
|
+
▼ │ ▼
|
|
612
|
+
┌────────────────────────────┐ │ ┌────────────────────────────┐
|
|
613
|
+
■│ isPending: false │ │ ■│ isPending: true [ƒ]│
|
|
614
|
+
│ isRevalidating: false │ │ fail │ isRevalidating: false │
|
|
615
|
+
│ │ └────────────────────┤ │
|
|
616
|
+
■│ state: "SUCCESS" │ │ state: "INITIAL" │
|
|
617
|
+
■│ isSuccess: true │ │ isSuccess: false │
|
|
618
|
+
■│ data: <TData> │ │ data: undefined │
|
|
619
|
+
■│ dataUpdatedAt: <number> │ │ dataUpdatedAt: undefined │
|
|
620
|
+
■│ dataStaleAt: <number> │ │ dataStaleAt: undefined │
|
|
621
|
+
│ isError: false │ │ isError: false │
|
|
622
|
+
│ error: undefined │ │ error: undefined │
|
|
623
|
+
│ errorUpdatedAt: undefined │ success │ errorUpdatedAt: undefined │
|
|
624
|
+
│ │ ◀─────────────────────────────────────┤ │
|
|
625
|
+
│ willRetryAt: undefined │ ■│ willRetryAt: undefined │
|
|
626
|
+
•│ isRetrying: false │ ■│ isRetrying: true │
|
|
627
|
+
•│ retryCount: 0 (reset) │ ■│ retryCount: <number> (+1) │
|
|
628
|
+
└────────────────────────────┘ └────────────────────────────┘
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
And then after success:
|
|
632
|
+
|
|
633
|
+
```
|
|
634
|
+
success failed, won't retry
|
|
635
|
+
┌────────────────────────────┐ ┌─────────────────────────────────────────┐
|
|
636
|
+
│ isPending: false │ ■│ isPending: false │
|
|
637
|
+
│ isRevalidating: false │ ■│ isRevalidating: false │
|
|
638
|
+
│ │ ┌──────────────────▶ │ │
|
|
639
|
+
│ state: "SUCCESS" │ │ ■│ state: "SUCCESS_BUT_REVALIDATION_ERROR" │
|
|
640
|
+
│ isSuccess: true │ │ │ isSuccess: true │
|
|
641
|
+
│ data: <TData> │ │ │ data: <TData> │
|
|
642
|
+
│ dataUpdatedAt: <number> │ │ │ dataUpdatedAt: <number> │
|
|
643
|
+
│ dataStaleAt: <number> │ │ │ dataStaleAt: <number> │
|
|
644
|
+
│ isError: false │ │ │ isError: false │
|
|
645
|
+
│ error: undefined │ │ ■│ error: <TError> │
|
|
646
|
+
│ errorUpdatedAt: undefined │ │ ■│ errorUpdatedAt: <number> │
|
|
647
|
+
│ │ │ │ │
|
|
648
|
+
│ willRetryAt: undefined │ │ │ willRetryAt: undefined │
|
|
649
|
+
│ isRetrying: false │ │ •│ isRetrying: false │
|
|
650
|
+
│ retryCount: 0 │ │ •│ retryCount: 0 (reset) │
|
|
651
|
+
└─────────────┬──────────────┘ │ └─────────────────────────────────────────┘
|
|
652
|
+
│ │
|
|
653
|
+
│ revalidate │
|
|
654
|
+
▼ │ waiting retry delay
|
|
655
|
+
┌────────────────────────────┐ (N) ┌────────────────────────────┐
|
|
656
|
+
■│ isPending: true [ƒ]│ │ ■│ isPending: false │
|
|
657
|
+
■│ isRevalidating: true │ fail │ ■│ isRevalidating: false │
|
|
658
|
+
│ ├──────────▶ Should retry? ────(Y)────▶ │ │
|
|
659
|
+
│ state: "SUCCESS" │ ▲ │ state: "SUCCESS" │
|
|
660
|
+
│ isSuccess: true │ │ │ isSuccess: true │
|
|
661
|
+
│ data: <TData> │ │ │ data: <TData> │
|
|
662
|
+
│ dataUpdatedAt: <number> │ │ │ dataUpdatedAt: <number> │
|
|
663
|
+
│ dataStaleAt: <number> │ │ │ dataStaleAt: <number> │
|
|
664
|
+
│ isError: false │ │ │ isError: false │
|
|
665
|
+
│ error: undefined │ │ │ error: undefined │
|
|
666
|
+
│ errorUpdatedAt: undefined │ │ │ errorUpdatedAt: undefined │
|
|
667
|
+
│ │ │ │ │
|
|
668
|
+
│ willRetryAt: undefined │ │ ■│ willRetryAt: <number> │
|
|
669
|
+
│ isRetrying: false │ │ │ isRetrying: false │
|
|
670
|
+
│ retryCount: 0 │ │ │ retryCount: <number> │
|
|
671
|
+
└─────────────┬──────────────┘ │ └─────────────┬──────────────┘
|
|
672
|
+
│ │ │
|
|
673
|
+
│ success │ │ retrying
|
|
674
|
+
▼ │ ▼
|
|
675
|
+
┌────────────────────────────┐ │ ┌────────────────────────────┐
|
|
676
|
+
■│ isPending: false │ │ ■│ isPending: true [ƒ]│
|
|
677
|
+
■│ isRevalidating: false │ │ fail ■│ isRevalidating: true │
|
|
678
|
+
│ │ └────────────────────┤ │
|
|
679
|
+
│ state: "SUCCESS" │ │ state: "SUCCESS" │
|
|
680
|
+
│ isSuccess: true │ │ isSuccess: true │
|
|
681
|
+
■│ data: <TData> │ │ data: <TData> │
|
|
682
|
+
■│ dataUpdatedAt: <number> │ │ dataUpdatedAt: <number> │
|
|
683
|
+
■│ dataStaleAt: <number> │ │ dataStaleAt: <number> │
|
|
684
|
+
│ isError: false │ │ isError: false │
|
|
685
|
+
│ error: undefined │ │ error: undefined │
|
|
686
|
+
│ errorUpdatedAt: undefined │ success │ errorUpdatedAt: undefined │
|
|
687
|
+
│ │ ◀─────────────────────────────────────┤ │
|
|
688
|
+
│ willRetryAt: undefined │ ■│ willRetryAt: undefined │
|
|
689
|
+
•│ isRetrying: false │ ■│ isRetrying: true │
|
|
690
|
+
•│ retryCount: 0 (reset) │ ■│ retryCount: <number> (+1) │
|
|
691
|
+
└────────────────────────────┘ └────────────────────────────┘
|
|
692
|
+
```
|