floppy-disk 3.2.1 → 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 +282 -140
- package/package.json +3 -2
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
|
+
```
|
|
61
|
+
|
|
62
|
+
## Updating State
|
|
58
63
|
|
|
59
|
-
|
|
60
|
-
const evolve = () => {
|
|
61
|
-
const { level } = useDigimon.getState();
|
|
64
|
+
You can update state using `setState`:
|
|
62
65
|
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
```tsx
|
|
67
|
+
const useLawn = createStore({ plants: 3, zombies: 1 });
|
|
68
|
+
// Current state: { plants: 3, zombies: 1 }
|
|
65
69
|
|
|
66
|
-
|
|
70
|
+
useLawn.setState({ plants: 5, zombies: 5 });
|
|
71
|
+
// Current state: { plants: 5, zombies: 5 }
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
}
|
|
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
|
|
81
|
+
|
|
82
|
+
Stores are not limited to React. You can access state **anywhere**:
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
const state = useLawn.getState();
|
|
86
|
+
console.log(state.plants);
|
|
87
|
+
```
|
|
73
88
|
|
|
74
|
-
|
|
89
|
+
## Subscribing to Changes
|
|
75
90
|
|
|
76
|
-
You can subscribe
|
|
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,7 +257,7 @@ 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
|
|
|
@@ -221,10 +268,12 @@ 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
279
|
const { data, error } = useMyQuery();
|
|
@@ -236,7 +285,7 @@ function MyComponent() {
|
|
|
236
285
|
}
|
|
237
286
|
```
|
|
238
287
|
|
|
239
|
-
|
|
288
|
+
## Query State: Two Independent Dimensions
|
|
240
289
|
|
|
241
290
|
FloppyDisk tracks two things separately:
|
|
242
291
|
|
|
@@ -245,19 +294,19 @@ FloppyDisk tracks two things separately:
|
|
|
245
294
|
|
|
246
295
|
They are **independent**.
|
|
247
296
|
|
|
248
|
-
|
|
297
|
+
## Keyed Query (Dynamic Params)
|
|
249
298
|
|
|
250
299
|
You can create parameterized queries:
|
|
251
300
|
|
|
252
301
|
```tsx
|
|
253
302
|
import { createQuery } from "floppy-disk/react";
|
|
254
303
|
|
|
255
|
-
import {
|
|
304
|
+
import { getZombieById, type GetZombieByIdResponse } from "../utils"; // Your own module
|
|
256
305
|
|
|
257
|
-
type
|
|
306
|
+
type ZombieQueryParam = { id: string };
|
|
258
307
|
|
|
259
|
-
const
|
|
260
|
-
|
|
308
|
+
const zombieQuery = createQuery<GetZombieByIdResponse, ZombieQueryParam>(
|
|
309
|
+
getZombieById,
|
|
261
310
|
// { staleTime: 5000, revalidateOnFocus: false } <-- optional options
|
|
262
311
|
);
|
|
263
312
|
```
|
|
@@ -265,20 +314,20 @@ const userQuery = createQuery<GetUserByIdResponse, MyQueryParam>(
|
|
|
265
314
|
Use it with parameters:
|
|
266
315
|
|
|
267
316
|
```tsx
|
|
268
|
-
function
|
|
269
|
-
const
|
|
270
|
-
const { data, error } =
|
|
317
|
+
function ZombieDetail({ id }) {
|
|
318
|
+
const useZombieQuery = zombieQuery({ id });
|
|
319
|
+
const { data, error } = useZombieQuery();
|
|
271
320
|
|
|
272
321
|
if (!data && !error) return <div>Loading...</div>;
|
|
273
322
|
if (error) return <div>Error: {error.message}</div>;
|
|
274
323
|
|
|
275
|
-
return <div>Name: {data.name},
|
|
324
|
+
return <div>Name: {data.name}, hp: {data.hp}</div>;
|
|
276
325
|
}
|
|
277
326
|
```
|
|
278
327
|
|
|
279
328
|
Each unique parameter creates its own cache entry.
|
|
280
329
|
|
|
281
|
-
|
|
330
|
+
## Store Inheritance
|
|
282
331
|
|
|
283
332
|
Queries in FloppyDisk are built on top of the core store.
|
|
284
333
|
This means every query inherits the same capabilities, such as `subscribe`, `getState`, and store events.
|
|
@@ -295,14 +344,14 @@ const value = useMyQuery().data?.foo.bar.baz;
|
|
|
295
344
|
Get state outside React:
|
|
296
345
|
|
|
297
346
|
```tsx
|
|
298
|
-
const
|
|
299
|
-
const
|
|
347
|
+
const myPlantQuery = createQuery<MyPlantResponse>(getMyPlant); // Query without paramerer
|
|
348
|
+
const zombieQuery = createQuery<GetZombieByIdResponse, { id: string }>(getZombieById); // Parameterized query
|
|
300
349
|
|
|
301
|
-
const
|
|
302
|
-
const getUserQueryData = ({ id }) =>
|
|
350
|
+
const getMyPlantQueryData = () => myPlantQuery().getState().data;
|
|
351
|
+
const getUserQueryData = ({ id }) => zombieQuery({ id }).getState().data;
|
|
303
352
|
```
|
|
304
353
|
|
|
305
|
-
|
|
354
|
+
## Infinite Query
|
|
306
355
|
|
|
307
356
|
FloppyDisk does **not provide** a dedicated "infinite query" API.\
|
|
308
357
|
Instead, it embraces a simpler and more flexible approach:
|
|
@@ -320,15 +369,15 @@ No special abstraction needed.
|
|
|
320
369
|
Here is the example on how to implement infinite query properly:
|
|
321
370
|
|
|
322
371
|
```tsx
|
|
323
|
-
type
|
|
372
|
+
type GetPlantParams = {
|
|
324
373
|
cursor?: string; // For pagination
|
|
325
374
|
};
|
|
326
|
-
type
|
|
327
|
-
|
|
375
|
+
type GetPlantsResponse = {
|
|
376
|
+
plants: Plant[];
|
|
328
377
|
meta: { nextCursor: string };
|
|
329
378
|
};
|
|
330
379
|
|
|
331
|
-
const
|
|
380
|
+
const plantsQuery = createQuery<GetPlantsResponse, GetPlantParams>(getPlants, {
|
|
332
381
|
staleTime: Infinity,
|
|
333
382
|
revalidateOnFocus: false,
|
|
334
383
|
revalidateOnReconnect: false,
|
|
@@ -339,16 +388,16 @@ function Main() {
|
|
|
339
388
|
}
|
|
340
389
|
|
|
341
390
|
function Page({ cursor }: { cursor?: string }) {
|
|
342
|
-
const
|
|
343
|
-
const { state, data, error } =
|
|
391
|
+
const usePlantsQuery = plantsQuery({ cursor });
|
|
392
|
+
const { state, data, error } = usePlantsQuery();
|
|
344
393
|
|
|
345
394
|
if (!data && !error) return <div>Loading...</div>;
|
|
346
395
|
if (error) return <div>Error</div>;
|
|
347
396
|
|
|
348
397
|
return (
|
|
349
398
|
<>
|
|
350
|
-
{data.
|
|
351
|
-
<
|
|
399
|
+
{data.plants.map((plant) => (
|
|
400
|
+
<PlantCard key={plant.id} plant={plant} />
|
|
352
401
|
))}
|
|
353
402
|
{data.meta.nextCursor && <LoadMore nextCursor={data.meta.nextCursor} />}
|
|
354
403
|
</>
|
|
@@ -357,7 +406,7 @@ function Page({ cursor }: { cursor?: string }) {
|
|
|
357
406
|
|
|
358
407
|
function LoadMore({ nextCursor }: { nextCursor?: string }) {
|
|
359
408
|
const [isNextPageRequested, setIsNextPageRequested] = useState(() => {
|
|
360
|
-
const stateOfNextPageQuery =
|
|
409
|
+
const stateOfNextPageQuery = plantsQuery({ cursor: nextCursor }).getState();
|
|
361
410
|
return stateOfNextPageQuery.isPending || stateOfNextPageQuery.isSuccess;
|
|
362
411
|
});
|
|
363
412
|
|
|
@@ -365,7 +414,7 @@ function LoadMore({ nextCursor }: { nextCursor?: string }) {
|
|
|
365
414
|
return <Page cursor={nextCursor} />;
|
|
366
415
|
}
|
|
367
416
|
|
|
368
|
-
return <
|
|
417
|
+
return <DomObserver onReachBottom={() => setIsNextPageRequested(true)} />;
|
|
369
418
|
}
|
|
370
419
|
```
|
|
371
420
|
|
|
@@ -382,11 +431,104 @@ If revalidation is triggered:
|
|
|
382
431
|
This leads to a **confusing and unstable user experience**.\
|
|
383
432
|
Revalidating dozens of previously viewed pages rarely provides value to the user.
|
|
384
433
|
|
|
385
|
-
##
|
|
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
|
|
386
528
|
|
|
387
529
|
Examples for using stores and queries in SSR with isolated data (no shared state between users).
|
|
388
530
|
|
|
389
|
-
|
|
531
|
+
## Initialize Store State from Server
|
|
390
532
|
|
|
391
533
|
```tsx
|
|
392
534
|
const useCountStore = createStore({ count: 0 });
|
|
@@ -400,7 +542,7 @@ function Page({ initialCount }) {
|
|
|
400
542
|
}
|
|
401
543
|
```
|
|
402
544
|
|
|
403
|
-
|
|
545
|
+
## Initialize Query Data from Server
|
|
404
546
|
|
|
405
547
|
```tsx
|
|
406
548
|
async function MyServerComponent() {
|
|
@@ -421,24 +563,24 @@ function MyClientComponent({ initialData }) {
|
|
|
421
563
|
}
|
|
422
564
|
```
|
|
423
565
|
|
|
424
|
-
|
|
566
|
+
# Query State Machine
|
|
425
567
|
|
|
426
568
|
This is how the query state transition flow looks like:
|
|
427
569
|
|
|
428
570
|
```
|
|
429
571
|
initial failed, won't retry
|
|
430
572
|
┌────────────────────────────┐ ┌────────────────────────────┐
|
|
431
|
-
│ isPending: false │
|
|
573
|
+
│ isPending: false │ ■│ isPending: false │
|
|
432
574
|
│ isRevalidating: false │ │ isRevalidating: false │
|
|
433
575
|
│ │ ┌──────────────────▶ │ │
|
|
434
|
-
│ state: "INITIAL" │ │
|
|
576
|
+
│ state: "INITIAL" │ │ ■│ state: "ERROR" │
|
|
435
577
|
│ isSuccess: false │ │ │ isSuccess: false │
|
|
436
578
|
│ data: undefined │ │ │ data: undefined │
|
|
437
579
|
│ dataUpdatedAt: undefined │ │ │ dataUpdatedAt: undefined │
|
|
438
580
|
│ dataStaleAt: undefined │ │ │ dataStaleAt: undefined │
|
|
439
|
-
│ isError: false │ │
|
|
440
|
-
│ error: undefined │ │
|
|
441
|
-
│ errorUpdatedAt: undefined │ │
|
|
581
|
+
│ isError: false │ │ ■│ isError: true │
|
|
582
|
+
│ error: undefined │ │ ■│ error: <TError> │
|
|
583
|
+
│ errorUpdatedAt: undefined │ │ ■│ errorUpdatedAt: <number> │
|
|
442
584
|
│ │ │ │ │
|
|
443
585
|
│ willRetryAt: undefined │ │ │ willRetryAt: undefined │
|
|
444
586
|
│ isRetrying: false │ │ •│ isRetrying: false │
|
|
@@ -448,7 +590,7 @@ This is how the query state transition flow looks like:
|
|
|
448
590
|
│ execute │
|
|
449
591
|
▼ │ waiting retry delay
|
|
450
592
|
┌────────────────────────────┐ (N) ┌────────────────────────────┐
|
|
451
|
-
|
|
593
|
+
■│ isPending: true [ƒ]│ │ ■│ isPending: false │
|
|
452
594
|
│ isRevalidating: false │ fail │ │ isRevalidating: false │
|
|
453
595
|
│ ├──────────▶ Should retry? ────(Y)────▶ │ │
|
|
454
596
|
│ state: "INITIAL" │ ▲ │ state: "INITIAL" │
|
|
@@ -460,7 +602,7 @@ This is how the query state transition flow looks like:
|
|
|
460
602
|
│ error: undefined │ │ │ error: undefined │
|
|
461
603
|
│ errorUpdatedAt: undefined │ │ │ errorUpdatedAt: undefined │
|
|
462
604
|
│ │ │ │ │
|
|
463
|
-
│ willRetryAt: undefined │ │
|
|
605
|
+
│ willRetryAt: undefined │ │ ■│ willRetryAt: <number> │
|
|
464
606
|
│ isRetrying: false │ │ │ isRetrying: false │
|
|
465
607
|
│ retryCount: 0 │ │ │ retryCount: <number> │
|
|
466
608
|
└─────────────┬──────────────┘ │ └─────────────┬──────────────┘
|
|
@@ -468,21 +610,21 @@ This is how the query state transition flow looks like:
|
|
|
468
610
|
│ success │ │ retrying
|
|
469
611
|
▼ │ ▼
|
|
470
612
|
┌────────────────────────────┐ │ ┌────────────────────────────┐
|
|
471
|
-
|
|
613
|
+
■│ isPending: false │ │ ■│ isPending: true [ƒ]│
|
|
472
614
|
│ isRevalidating: false │ │ fail │ isRevalidating: false │
|
|
473
615
|
│ │ └────────────────────┤ │
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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 │
|
|
479
621
|
│ isError: false │ │ isError: false │
|
|
480
622
|
│ error: undefined │ │ error: undefined │
|
|
481
623
|
│ errorUpdatedAt: undefined │ success │ errorUpdatedAt: undefined │
|
|
482
624
|
│ │ ◀─────────────────────────────────────┤ │
|
|
483
|
-
│ willRetryAt: undefined │
|
|
484
|
-
•│ isRetrying: false │
|
|
485
|
-
•│ retryCount: 0 (reset) │
|
|
625
|
+
│ willRetryAt: undefined │ ■│ willRetryAt: undefined │
|
|
626
|
+
•│ isRetrying: false │ ■│ isRetrying: true │
|
|
627
|
+
•│ retryCount: 0 (reset) │ ■│ retryCount: <number> (+1) │
|
|
486
628
|
└────────────────────────────┘ └────────────────────────────┘
|
|
487
629
|
```
|
|
488
630
|
|
|
@@ -491,17 +633,17 @@ And then after success:
|
|
|
491
633
|
```
|
|
492
634
|
success failed, won't retry
|
|
493
635
|
┌────────────────────────────┐ ┌─────────────────────────────────────────┐
|
|
494
|
-
│ isPending: false │
|
|
495
|
-
│ isRevalidating: false │
|
|
636
|
+
│ isPending: false │ ■│ isPending: false │
|
|
637
|
+
│ isRevalidating: false │ ■│ isRevalidating: false │
|
|
496
638
|
│ │ ┌──────────────────▶ │ │
|
|
497
|
-
│ state: "SUCCESS" │ │
|
|
639
|
+
│ state: "SUCCESS" │ │ ■│ state: "SUCCESS_BUT_REVALIDATION_ERROR" │
|
|
498
640
|
│ isSuccess: true │ │ │ isSuccess: true │
|
|
499
641
|
│ data: <TData> │ │ │ data: <TData> │
|
|
500
642
|
│ dataUpdatedAt: <number> │ │ │ dataUpdatedAt: <number> │
|
|
501
643
|
│ dataStaleAt: <number> │ │ │ dataStaleAt: <number> │
|
|
502
644
|
│ isError: false │ │ │ isError: false │
|
|
503
|
-
│ error: undefined │ │
|
|
504
|
-
│ errorUpdatedAt: undefined │ │
|
|
645
|
+
│ error: undefined │ │ ■│ error: <TError> │
|
|
646
|
+
│ errorUpdatedAt: undefined │ │ ■│ errorUpdatedAt: <number> │
|
|
505
647
|
│ │ │ │ │
|
|
506
648
|
│ willRetryAt: undefined │ │ │ willRetryAt: undefined │
|
|
507
649
|
│ isRetrying: false │ │ •│ isRetrying: false │
|
|
@@ -511,8 +653,8 @@ And then after success:
|
|
|
511
653
|
│ revalidate │
|
|
512
654
|
▼ │ waiting retry delay
|
|
513
655
|
┌────────────────────────────┐ (N) ┌────────────────────────────┐
|
|
514
|
-
|
|
515
|
-
|
|
656
|
+
■│ isPending: true [ƒ]│ │ ■│ isPending: false │
|
|
657
|
+
■│ isRevalidating: true │ fail │ ■│ isRevalidating: false │
|
|
516
658
|
│ ├──────────▶ Should retry? ────(Y)────▶ │ │
|
|
517
659
|
│ state: "SUCCESS" │ ▲ │ state: "SUCCESS" │
|
|
518
660
|
│ isSuccess: true │ │ │ isSuccess: true │
|
|
@@ -523,7 +665,7 @@ And then after success:
|
|
|
523
665
|
│ error: undefined │ │ │ error: undefined │
|
|
524
666
|
│ errorUpdatedAt: undefined │ │ │ errorUpdatedAt: undefined │
|
|
525
667
|
│ │ │ │ │
|
|
526
|
-
│ willRetryAt: undefined │ │
|
|
668
|
+
│ willRetryAt: undefined │ │ ■│ willRetryAt: <number> │
|
|
527
669
|
│ isRetrying: false │ │ │ isRetrying: false │
|
|
528
670
|
│ retryCount: 0 │ │ │ retryCount: <number> │
|
|
529
671
|
└─────────────┬──────────────┘ │ └─────────────┬──────────────┘
|
|
@@ -531,20 +673,20 @@ And then after success:
|
|
|
531
673
|
│ success │ │ retrying
|
|
532
674
|
▼ │ ▼
|
|
533
675
|
┌────────────────────────────┐ │ ┌────────────────────────────┐
|
|
534
|
-
|
|
535
|
-
|
|
676
|
+
■│ isPending: false │ │ ■│ isPending: true [ƒ]│
|
|
677
|
+
■│ isRevalidating: false │ │ fail ■│ isRevalidating: true │
|
|
536
678
|
│ │ └────────────────────┤ │
|
|
537
679
|
│ state: "SUCCESS" │ │ state: "SUCCESS" │
|
|
538
680
|
│ isSuccess: true │ │ isSuccess: true │
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
681
|
+
■│ data: <TData> │ │ data: <TData> │
|
|
682
|
+
■│ dataUpdatedAt: <number> │ │ dataUpdatedAt: <number> │
|
|
683
|
+
■│ dataStaleAt: <number> │ │ dataStaleAt: <number> │
|
|
542
684
|
│ isError: false │ │ isError: false │
|
|
543
685
|
│ error: undefined │ │ error: undefined │
|
|
544
686
|
│ errorUpdatedAt: undefined │ success │ errorUpdatedAt: undefined │
|
|
545
687
|
│ │ ◀─────────────────────────────────────┤ │
|
|
546
|
-
│ willRetryAt: undefined │
|
|
547
|
-
•│ isRetrying: false │
|
|
548
|
-
•│ retryCount: 0 (reset) │
|
|
688
|
+
│ willRetryAt: undefined │ ■│ willRetryAt: undefined │
|
|
689
|
+
•│ isRetrying: false │ ■│ isRetrying: true │
|
|
690
|
+
•│ retryCount: 0 (reset) │ ■│ retryCount: <number> (+1) │
|
|
549
691
|
└────────────────────────────┘ └────────────────────────────┘
|
|
550
692
|
```
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "floppy-disk",
|
|
3
3
|
"description": "Lightweight unified state management for sync and async data.",
|
|
4
4
|
"private": false,
|
|
5
|
-
"version": "3.2.
|
|
5
|
+
"version": "3.2.2",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"utilities",
|
|
8
8
|
"store",
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
},
|
|
71
|
+
"packageManager": "pnpm@10.32.1",
|
|
71
72
|
"peerDependencies": {
|
|
72
73
|
"@types/react": ">=17.0",
|
|
73
74
|
"react": ">=17.0"
|
|
@@ -80,4 +81,4 @@
|
|
|
80
81
|
"optional": true
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
|
-
}
|
|
84
|
+
}
|