floppy-disk 3.2.2 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -619
- package/esm/react/create-mutation.d.mts +2 -0
- package/esm/react/create-query.d.mts +4 -1
- package/esm/react/create-store.d.mts +2 -0
- package/esm/react/create-stores.d.mts +2 -0
- package/esm/react/use-mutation.d.mts +2 -0
- package/esm/react.mjs +2 -2
- package/package.json +1 -1
- package/react/create-mutation.d.ts +2 -0
- package/react/create-query.d.ts +4 -1
- package/react/create-store.d.ts +2 -0
- package/react/create-stores.d.ts +2 -0
- package/react/use-mutation.d.ts +2 -0
- package/react.js +2 -2
package/README.md
CHANGED
|
@@ -18,16 +18,18 @@ npm install floppy-disk
|
|
|
18
18
|
|
|
19
19
|
## In short, it is:
|
|
20
20
|
|
|
21
|
-
- **Like Zustand, but has additional
|
|
22
|
-
- No
|
|
23
|
-
- Store events: `onSubscribe`, `
|
|
24
|
-
- Easier to set initial state
|
|
21
|
+
- **Like Zustand, but has additional capabilities:**
|
|
22
|
+
- No selectors: automatically optimizes re-renders
|
|
23
|
+
- Store events: `onFirstSubscribe`, `onSubscribe`, `onUnsubscribe`, `onLastUnsubscribe`
|
|
24
|
+
- Easier to set initial state on SSR/SSG
|
|
25
25
|
- Smaller bundle
|
|
26
26
|
- **Like TanStack Query, but:**
|
|
27
27
|
- DX is very similar to Zustand → One mental model for sync & async
|
|
28
|
-
-
|
|
28
|
+
- Much smaller bundle than TanStack Query → With nearly the same capabilities
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
**Docs: https://floppy-disk.vercel.app**
|
|
31
|
+
|
|
32
|
+
## Store (Global State)
|
|
31
33
|
|
|
32
34
|
A store is a global state container that can be used both **inside and outside** React.\
|
|
33
35
|
With FloppyDisk, creating a store is simple:
|
|
@@ -59,634 +61,35 @@ const addPlant = () => {
|
|
|
59
61
|
};
|
|
60
62
|
```
|
|
61
63
|
|
|
62
|
-
##
|
|
63
|
-
|
|
64
|
-
You can update state using `setState`:
|
|
65
|
-
|
|
66
|
-
```tsx
|
|
67
|
-
const useLawn = createStore({ plants: 3, zombies: 1 });
|
|
68
|
-
// Current state: { plants: 3, zombies: 1 }
|
|
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 }
|
|
78
|
-
```
|
|
79
|
-
|
|
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
|
-
```
|
|
88
|
-
|
|
89
|
-
## Subscribing to Changes
|
|
90
|
-
|
|
91
|
-
You can subscribe to state changes:
|
|
92
|
-
|
|
93
|
-
```tsx
|
|
94
|
-
const unsubscribeLawn = useLawn.subscribe((currentState, prevState) => {
|
|
95
|
-
console.log("State changed:", currentState);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// Later
|
|
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
|
-
}
|
|
119
|
-
```
|
|
120
|
-
|
|
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
|
|
131
|
-
|
|
132
|
-
```tsx
|
|
133
|
-
const useLawn = createStore(
|
|
134
|
-
{
|
|
135
|
-
plants: 3,
|
|
136
|
-
zombies: 1,
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
onFirstSubscribe: () => {
|
|
140
|
-
console.log("First subscriber! We’re officially popular 🎉");
|
|
141
|
-
},
|
|
142
|
-
onSubscribe: () => {
|
|
143
|
-
console.log("New subscriber joined. Welcome aboard 🫡");
|
|
144
|
-
},
|
|
145
|
-
onUnsubscribe: () => {
|
|
146
|
-
console.log("Subscriber left... was it something I said? 😭");
|
|
147
|
-
},
|
|
148
|
-
onLastUnsubscribe: () => {
|
|
149
|
-
console.log("Everyone left. Guess I’ll just exist quietly now...");
|
|
150
|
-
},
|
|
151
|
-
},
|
|
152
|
-
);
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
## Use Cases
|
|
156
|
-
|
|
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
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
**Perfect for:**
|
|
164
|
-
- opening / closing connections
|
|
165
|
-
- starting / stopping polling
|
|
166
|
-
- initializing expensive resources
|
|
167
|
-
- adding / removing window event listeners
|
|
168
|
-
|
|
169
|
-
## State Changes Event
|
|
170
|
-
|
|
171
|
-
Sometimes you want to observe state changes **without becoming a subscriber**.
|
|
172
|
-
|
|
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.
|
|
176
|
-
|
|
177
|
-
Useful for devtools, logging, or debugging state changes.
|
|
178
|
-
|
|
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
|
-
);
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
# Query & Mutation Store for Async State
|
|
196
|
-
|
|
197
|
-
FloppyDisk also provides a powerful async state layer, inspired by [TanStack-Query](https://tanstack.com/query) but with a simpler API.
|
|
198
|
-
|
|
199
|
-
It is agnostic to the type of async operation,
|
|
200
|
-
it works with any Promise-based operation—whether it's a network request, local computation, storage access, or something else.
|
|
201
|
-
|
|
202
|
-
Because of that, we intentionally avoid terms like "fetch" or "refetch".\
|
|
203
|
-
Instead, we use:
|
|
204
|
-
|
|
205
|
-
- **execute** → run the async operation (same as "fetch" in TanStack-Query)
|
|
206
|
-
- **revalidate** → re-run while keeping existing data (same as "refetch" in TanStack-Query)
|
|
207
|
-
|
|
208
|
-
## Query vs Mutation
|
|
209
|
-
|
|
210
|
-
<details>
|
|
211
|
-
|
|
212
|
-
<summary>Query → Read Operations</summary>
|
|
213
|
-
|
|
214
|
-
Queries are designed for reading data.\
|
|
215
|
-
They assume:
|
|
216
|
-
|
|
217
|
-
- no side effects
|
|
218
|
-
- no data mutation
|
|
219
|
-
- safe to run multiple times
|
|
220
|
-
|
|
221
|
-
Because of this, queries come with helpful defaults:
|
|
222
|
-
|
|
223
|
-
- ✅ Retry mechanism (for transient failures)
|
|
224
|
-
- ✅ Revalidation (keep data fresh automatically)
|
|
225
|
-
- ✅ Caching & staleness control
|
|
226
|
-
|
|
227
|
-
Use queries when:
|
|
228
|
-
|
|
229
|
-
- fetching data
|
|
230
|
-
- reading from storage
|
|
231
|
-
- running idempotent async logic
|
|
232
|
-
|
|
233
|
-
</details>
|
|
234
|
-
|
|
235
|
-
<details>
|
|
236
|
-
|
|
237
|
-
<summary>Mutation → Write Operations</summary>
|
|
238
|
-
|
|
239
|
-
Mutations are designed for changing data.\
|
|
240
|
-
Examples:
|
|
241
|
-
|
|
242
|
-
- insert
|
|
243
|
-
- update
|
|
244
|
-
- delete
|
|
245
|
-
- triggering side effects
|
|
246
|
-
|
|
247
|
-
Because mutations are **not safe to repeat blindly**, FloppyDisk does **not** include:
|
|
248
|
-
|
|
249
|
-
- ❌ automatic retry
|
|
250
|
-
- ❌ automatic revalidation
|
|
251
|
-
- ❌ implicit re-execution
|
|
252
|
-
|
|
253
|
-
This is intentional.\
|
|
254
|
-
Mutations should be explicit and controlled, not automatic.
|
|
255
|
-
|
|
256
|
-
If you need retry mechanism, then you can always add it manually.
|
|
257
|
-
|
|
258
|
-
</details>
|
|
259
|
-
|
|
260
|
-
## Single Query
|
|
261
|
-
|
|
262
|
-
Create a query using `createQuery`:
|
|
263
|
-
|
|
264
|
-
```tsx
|
|
265
|
-
import { createQuery } from "floppy-disk/react";
|
|
266
|
-
|
|
267
|
-
const myQuery = createQuery(
|
|
268
|
-
myAsyncFn,
|
|
269
|
-
// { staleTime: 5000, revalidateOnFocus: false } <-- optional options
|
|
270
|
-
);
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
Use it inside your component:
|
|
64
|
+
## Query Store for Async State
|
|
274
65
|
|
|
275
|
-
|
|
276
|
-
const useMyQuery = myQuery();
|
|
277
|
-
|
|
278
|
-
function MyComponent() {
|
|
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>;
|
|
285
|
-
}
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
## Query State: Two Independent Dimensions
|
|
289
|
-
|
|
290
|
-
FloppyDisk tracks two things separately:
|
|
291
|
-
|
|
292
|
-
- Is it running? → `isPending`
|
|
293
|
-
- What's the result? → `state`
|
|
294
|
-
|
|
295
|
-
They are **independent**.
|
|
296
|
-
|
|
297
|
-
## Keyed Query (Dynamic Params)
|
|
298
|
-
|
|
299
|
-
You can create parameterized queries:
|
|
66
|
+
Create a query store for async data with `createQuery`:
|
|
300
67
|
|
|
301
68
|
```tsx
|
|
302
69
|
import { createQuery } from "floppy-disk/react";
|
|
303
70
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
getZombieById,
|
|
310
|
-
// { staleTime: 5000, revalidateOnFocus: false } <-- optional options
|
|
71
|
+
const plantDetailQuery = createQuery(
|
|
72
|
+
async ({ id }) => {
|
|
73
|
+
const res = await fetch(`/api/plants/${id}`);
|
|
74
|
+
return res.json();
|
|
75
|
+
},
|
|
311
76
|
);
|
|
312
77
|
```
|
|
313
78
|
|
|
314
|
-
Use it
|
|
79
|
+
Use it in your component:
|
|
315
80
|
|
|
316
81
|
```tsx
|
|
317
|
-
function
|
|
318
|
-
const
|
|
319
|
-
const { data, error } =
|
|
82
|
+
function PlantDetail({ id }) {
|
|
83
|
+
const usePlantDetailQuery = plantDetailQuery({ id });
|
|
84
|
+
const { data, error } = usePlantDetailQuery();
|
|
320
85
|
|
|
321
86
|
if (!data && !error) return <div>Loading...</div>;
|
|
322
87
|
if (error) return <div>Error: {error.message}</div>;
|
|
323
88
|
|
|
324
|
-
return <div>Name: {data.name},
|
|
325
|
-
}
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
Each unique parameter creates its own cache entry.
|
|
329
|
-
|
|
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
|
|
355
|
-
|
|
356
|
-
FloppyDisk does **not provide** a dedicated "infinite query" API.\
|
|
357
|
-
Instead, it embraces a simpler and more flexible approach:
|
|
358
|
-
|
|
359
|
-
> Infinite queries are just **composition** + **recursion**.
|
|
360
|
-
|
|
361
|
-
Why? Because async state is already powerful enough:
|
|
362
|
-
|
|
363
|
-
- keyed queries handle parameters
|
|
364
|
-
- components handle composition
|
|
365
|
-
- recursion handles pagination
|
|
366
|
-
|
|
367
|
-
No special abstraction needed.
|
|
368
|
-
|
|
369
|
-
Here is the example on how to implement infinite query properly:
|
|
370
|
-
|
|
371
|
-
```tsx
|
|
372
|
-
type GetPlantParams = {
|
|
373
|
-
cursor?: string; // For pagination
|
|
374
|
-
};
|
|
375
|
-
type GetPlantsResponse = {
|
|
376
|
-
plants: Plant[];
|
|
377
|
-
meta: { nextCursor: string };
|
|
378
|
-
};
|
|
379
|
-
|
|
380
|
-
const plantsQuery = createQuery<GetPlantsResponse, GetPlantParams>(getPlants, {
|
|
381
|
-
staleTime: Infinity,
|
|
382
|
-
revalidateOnFocus: false,
|
|
383
|
-
revalidateOnReconnect: false,
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
function Main() {
|
|
387
|
-
return <Page cursor={undefined} />;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function Page({ cursor }: { cursor?: string }) {
|
|
391
|
-
const usePlantsQuery = plantsQuery({ cursor });
|
|
392
|
-
const { state, data, error } = usePlantsQuery();
|
|
393
|
-
|
|
394
|
-
if (!data && !error) return <div>Loading...</div>;
|
|
395
|
-
if (error) return <div>Error</div>;
|
|
396
|
-
|
|
397
|
-
return (
|
|
398
|
-
<>
|
|
399
|
-
{data.plants.map((plant) => (
|
|
400
|
-
<PlantCard key={plant.id} plant={plant} />
|
|
401
|
-
))}
|
|
402
|
-
{data.meta.nextCursor && <LoadMore nextCursor={data.meta.nextCursor} />}
|
|
403
|
-
</>
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
function LoadMore({ nextCursor }: { nextCursor?: string }) {
|
|
408
|
-
const [isNextPageRequested, setIsNextPageRequested] = useState(() => {
|
|
409
|
-
const stateOfNextPageQuery = plantsQuery({ cursor: nextCursor }).getState();
|
|
410
|
-
return stateOfNextPageQuery.isPending || stateOfNextPageQuery.isSuccess;
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
if (isNextPageRequested) {
|
|
414
|
-
return <Page cursor={nextCursor} />;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
return <DomObserver onReachBottom={() => setIsNextPageRequested(true)} />;
|
|
89
|
+
return <div>Name: {data.name}, damage: {data.damage}</div>;
|
|
418
90
|
}
|
|
419
91
|
```
|
|
420
92
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
Why?\
|
|
424
|
-
In an infinite list, users may scroll through many pages ("_doom-scrolling_").\
|
|
425
|
-
If revalidation is triggered:
|
|
426
|
-
|
|
427
|
-
- All previously loaded pages may re-execute
|
|
428
|
-
- Content at the top may change without the user noticing
|
|
429
|
-
- Layout shifts can occur unexpectedly
|
|
430
|
-
|
|
431
|
-
This leads to a **confusing and unstable user experience**.\
|
|
432
|
-
Revalidating dozens of previously viewed pages rarely provides value to the user.
|
|
433
|
-
|
|
434
|
-
## Mutation
|
|
435
|
-
|
|
436
|
-
Mutations are used to perform write operations—such as creating, updating, or deleting data.
|
|
93
|
+
---
|
|
437
94
|
|
|
438
|
-
|
|
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
|
|
528
|
-
|
|
529
|
-
Examples for using stores and queries in SSR with isolated data (no shared state between users).
|
|
530
|
-
|
|
531
|
-
## Initialize Store State from Server
|
|
532
|
-
|
|
533
|
-
```tsx
|
|
534
|
-
const useCountStore = createStore({ count: 0 });
|
|
535
|
-
|
|
536
|
-
function Page({ initialCount }) {
|
|
537
|
-
const { count } = useCountStore({
|
|
538
|
-
initialState: { count: initialCount }, // e.g. 3
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
return <>count is {count}</>; // Output: count is 3
|
|
542
|
-
}
|
|
543
|
-
```
|
|
544
|
-
|
|
545
|
-
## Initialize Query Data from Server
|
|
546
|
-
|
|
547
|
-
```tsx
|
|
548
|
-
async function MyServerComponent() {
|
|
549
|
-
const data = await getData(); // e.g. { count: 3 }
|
|
550
|
-
return <MyClientComponent initialData={data} />;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
const myQuery = createQuery(getData);
|
|
554
|
-
const useMyQuery = myQuery();
|
|
555
|
-
|
|
556
|
-
function MyClientComponent({ initialData }) {
|
|
557
|
-
const { data } = useMyQuery({
|
|
558
|
-
initialData: initialData,
|
|
559
|
-
// initialDataIsStale: true <-- Optional, default to false (no immediate revalidation)
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
return <>count is {data.count}</>; // Output: count is 3
|
|
563
|
-
}
|
|
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
|
-
```
|
|
95
|
+
Read the docs → https://floppy-disk.vercel.app
|
|
@@ -104,6 +104,8 @@ export type MutationOptions<TData, TVariable, TError = Error> = InitStoreOptions
|
|
|
104
104
|
*
|
|
105
105
|
* const { isPending } = useCreateUser();
|
|
106
106
|
* const result = await useCreateUser.execute({ name: 'John' });
|
|
107
|
+
*
|
|
108
|
+
* @see https://floppy-disk.vercel.app/docs/async/mutation
|
|
107
109
|
*/
|
|
108
110
|
export declare const createMutation: <TData, TVariable = undefined, TError = Error>(mutationFn: (variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable, TError>) => Promise<TData>, options?: MutationOptions<TData, TVariable, TError>) => (() => MutationState<TData, TVariable, TError>) & {
|
|
109
111
|
subscribe: (subscriber: import("../vanilla.d.mts").Subscriber<MutationState<TData, TVariable, TError>>) => () => void;
|
|
@@ -161,8 +161,10 @@ export type QueryOptions<TData, TVariable extends Record<string, any>, TError =
|
|
|
161
161
|
* const state = useUserQuery();
|
|
162
162
|
* // ...
|
|
163
163
|
* }
|
|
164
|
+
*
|
|
165
|
+
* @see https://floppy-disk.vercel.app/docs/async/query
|
|
164
166
|
*/
|
|
165
|
-
export declare const createQuery: <TData, TVariable extends Record<string, any> = never, TError = Error>(queryFn: (variable: TVariable, currentState: QueryState<TData, TError
|
|
167
|
+
export declare const createQuery: <TData, TVariable extends Record<string, any> = never, TError = Error>(queryFn: (variable: TVariable, currentState: QueryState<TData, TError>, variableHash: string) => Promise<TData>, options?: QueryOptions<TData, TVariable, TError>) => ((variable?: TVariable) => ((options?: {
|
|
166
168
|
/**
|
|
167
169
|
* Whether the query should be ravalidated automatically on mount.
|
|
168
170
|
*
|
|
@@ -218,6 +220,7 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
|
|
|
218
220
|
* Internal data, do not mutate!
|
|
219
221
|
*/
|
|
220
222
|
metadata: {
|
|
223
|
+
variableHash: string;
|
|
221
224
|
isInvalidated?: boolean;
|
|
222
225
|
promise?: Promise<QueryState<TData, TError>> | undefined;
|
|
223
226
|
promiseResolver?: ((value: QueryState<TData, TError> | PromiseLike<QueryState<TData, TError>>) => void) | undefined;
|
|
@@ -24,6 +24,8 @@ import { type InitStoreOptions } from "../vanilla.mjs";
|
|
|
24
24
|
* }
|
|
25
25
|
*
|
|
26
26
|
* useMyStore.setState({ foo: 2 }); // only components using foo will re-render
|
|
27
|
+
*
|
|
28
|
+
* @see https://floppy-disk.vercel.app/docs/sync/store
|
|
27
29
|
*/
|
|
28
30
|
export declare const createStore: <TState extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => ((options?: {
|
|
29
31
|
/**
|
|
@@ -29,6 +29,8 @@ import { type InitStoreOptions } from "../vanilla.mjs";
|
|
|
29
29
|
* const state = useUserStore();
|
|
30
30
|
* return <div>{state.name}</div>;
|
|
31
31
|
* }
|
|
32
|
+
*
|
|
33
|
+
* @see https://floppy-disk.vercel.app/docs/sync/stores
|
|
32
34
|
*/
|
|
33
35
|
export declare const createStores: <TState extends Record<string, any>, TKey extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => (key?: TKey) => ((options?: {
|
|
34
36
|
/**
|
|
@@ -20,6 +20,8 @@ import { type MutationOptions, type MutationState } from "./create-mutation.mjs"
|
|
|
20
20
|
* - If multiple executions triggered at the same time:
|
|
21
21
|
* - Only the latest execution is allowed to update the state.
|
|
22
22
|
* - Results from previous executions are ignored if a newer one exists.
|
|
23
|
+
*
|
|
24
|
+
* @see https://floppy-disk.vercel.app/docs/async/mutation
|
|
23
25
|
*/
|
|
24
26
|
export declare const useMutation: <TData, TVariable = undefined, TError = Error>(
|
|
25
27
|
/**
|
package/esm/react.mjs
CHANGED
|
@@ -208,7 +208,7 @@ const createQuery = (queryFn, options = {}) => {
|
|
|
208
208
|
});
|
|
209
209
|
const internals = /* @__PURE__ */ new WeakMap();
|
|
210
210
|
const configureInternals = (store, variable, variableHash) => ({
|
|
211
|
-
metadata: {},
|
|
211
|
+
metadata: { variableHash },
|
|
212
212
|
setInitialData: (data, revalidate2 = false) => {
|
|
213
213
|
const state = store.getState();
|
|
214
214
|
if (state.state === "INITIAL" && state.data === void 0) {
|
|
@@ -292,7 +292,7 @@ const createQuery = (queryFn, options = {}) => {
|
|
|
292
292
|
isRetrying: !!metadata.retryResolver,
|
|
293
293
|
retryCount: metadata.retryResolver ? stateBeforeExecute.retryCount + 1 : 0
|
|
294
294
|
});
|
|
295
|
-
queryFn(variable, stateBeforeExecute).then((data) => {
|
|
295
|
+
queryFn(variable, stateBeforeExecute, metadata.variableHash).then((data) => {
|
|
296
296
|
var _a;
|
|
297
297
|
if (data === void 0) {
|
|
298
298
|
console.error(
|
package/package.json
CHANGED
|
@@ -104,6 +104,8 @@ export type MutationOptions<TData, TVariable, TError = Error> = InitStoreOptions
|
|
|
104
104
|
*
|
|
105
105
|
* const { isPending } = useCreateUser();
|
|
106
106
|
* const result = await useCreateUser.execute({ name: 'John' });
|
|
107
|
+
*
|
|
108
|
+
* @see https://floppy-disk.vercel.app/docs/async/mutation
|
|
107
109
|
*/
|
|
108
110
|
export declare const createMutation: <TData, TVariable = undefined, TError = Error>(mutationFn: (variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable, TError>) => Promise<TData>, options?: MutationOptions<TData, TVariable, TError>) => (() => MutationState<TData, TVariable, TError>) & {
|
|
109
111
|
subscribe: (subscriber: import("../vanilla.ts").Subscriber<MutationState<TData, TVariable, TError>>) => () => void;
|
package/react/create-query.d.ts
CHANGED
|
@@ -161,8 +161,10 @@ export type QueryOptions<TData, TVariable extends Record<string, any>, TError =
|
|
|
161
161
|
* const state = useUserQuery();
|
|
162
162
|
* // ...
|
|
163
163
|
* }
|
|
164
|
+
*
|
|
165
|
+
* @see https://floppy-disk.vercel.app/docs/async/query
|
|
164
166
|
*/
|
|
165
|
-
export declare const createQuery: <TData, TVariable extends Record<string, any> = never, TError = Error>(queryFn: (variable: TVariable, currentState: QueryState<TData, TError
|
|
167
|
+
export declare const createQuery: <TData, TVariable extends Record<string, any> = never, TError = Error>(queryFn: (variable: TVariable, currentState: QueryState<TData, TError>, variableHash: string) => Promise<TData>, options?: QueryOptions<TData, TVariable, TError>) => ((variable?: TVariable) => ((options?: {
|
|
166
168
|
/**
|
|
167
169
|
* Whether the query should be ravalidated automatically on mount.
|
|
168
170
|
*
|
|
@@ -218,6 +220,7 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
|
|
|
218
220
|
* Internal data, do not mutate!
|
|
219
221
|
*/
|
|
220
222
|
metadata: {
|
|
223
|
+
variableHash: string;
|
|
221
224
|
isInvalidated?: boolean;
|
|
222
225
|
promise?: Promise<QueryState<TData, TError>> | undefined;
|
|
223
226
|
promiseResolver?: ((value: QueryState<TData, TError> | PromiseLike<QueryState<TData, TError>>) => void) | undefined;
|
package/react/create-store.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ import { type InitStoreOptions } from "../vanilla.ts";
|
|
|
24
24
|
* }
|
|
25
25
|
*
|
|
26
26
|
* useMyStore.setState({ foo: 2 }); // only components using foo will re-render
|
|
27
|
+
*
|
|
28
|
+
* @see https://floppy-disk.vercel.app/docs/sync/store
|
|
27
29
|
*/
|
|
28
30
|
export declare const createStore: <TState extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => ((options?: {
|
|
29
31
|
/**
|
package/react/create-stores.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ import { type InitStoreOptions } from "../vanilla.ts";
|
|
|
29
29
|
* const state = useUserStore();
|
|
30
30
|
* return <div>{state.name}</div>;
|
|
31
31
|
* }
|
|
32
|
+
*
|
|
33
|
+
* @see https://floppy-disk.vercel.app/docs/sync/stores
|
|
32
34
|
*/
|
|
33
35
|
export declare const createStores: <TState extends Record<string, any>, TKey extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => (key?: TKey) => ((options?: {
|
|
34
36
|
/**
|
package/react/use-mutation.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ import { type MutationOptions, type MutationState } from "./create-mutation.ts";
|
|
|
20
20
|
* - If multiple executions triggered at the same time:
|
|
21
21
|
* - Only the latest execution is allowed to update the state.
|
|
22
22
|
* - Results from previous executions are ignored if a newer one exists.
|
|
23
|
+
*
|
|
24
|
+
* @see https://floppy-disk.vercel.app/docs/async/mutation
|
|
23
25
|
*/
|
|
24
26
|
export declare const useMutation: <TData, TVariable = undefined, TError = Error>(
|
|
25
27
|
/**
|
package/react.js
CHANGED
|
@@ -210,7 +210,7 @@ const createQuery = (queryFn, options = {}) => {
|
|
|
210
210
|
});
|
|
211
211
|
const internals = /* @__PURE__ */ new WeakMap();
|
|
212
212
|
const configureInternals = (store, variable, variableHash) => ({
|
|
213
|
-
metadata: {},
|
|
213
|
+
metadata: { variableHash },
|
|
214
214
|
setInitialData: (data, revalidate2 = false) => {
|
|
215
215
|
const state = store.getState();
|
|
216
216
|
if (state.state === "INITIAL" && state.data === void 0) {
|
|
@@ -294,7 +294,7 @@ const createQuery = (queryFn, options = {}) => {
|
|
|
294
294
|
isRetrying: !!metadata.retryResolver,
|
|
295
295
|
retryCount: metadata.retryResolver ? stateBeforeExecute.retryCount + 1 : 0
|
|
296
296
|
});
|
|
297
|
-
queryFn(variable, stateBeforeExecute).then((data) => {
|
|
297
|
+
queryFn(variable, stateBeforeExecute, metadata.variableHash).then((data) => {
|
|
298
298
|
var _a;
|
|
299
299
|
if (data === void 0) {
|
|
300
300
|
console.error(
|