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 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 capability:**
22
- - No selector: auto optimize re-render.
23
- - Store events: `onSubscribe`, `onUnSubscribe`, etc.
24
- - Easier to set initial state from server
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
- - Extremely less bundle size → With almost the same capabilities
28
+ - Much smaller bundle than TanStack Query → With nearly the same capabilities
29
29
 
30
- # Store (Global State)
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
- ## Updating State
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
- ```tsx
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
- import { getZombieById, type GetZombieByIdResponse } from "../utils"; // Your own module
305
-
306
- type ZombieQueryParam = { id: string };
307
-
308
- const zombieQuery = createQuery<GetZombieByIdResponse, ZombieQueryParam>(
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 with parameters:
79
+ Use it in your component:
315
80
 
316
81
  ```tsx
317
- function ZombieDetail({ id }) {
318
- const useZombieQuery = zombieQuery({ id });
319
- const { data, error } = useZombieQuery();
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}, hp: {data.hp}</div>;
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
- When implementing infinite queries, it is **highly recommended to disable automatic revalidation**.
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
- 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
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>) => Promise<TData>, options?: QueryOptions<TData, TVariable, TError>) => ((variable?: TVariable) => ((options?: {
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
@@ -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.2",
5
+ "version": "3.3.0",
6
6
  "keywords": [
7
7
  "utilities",
8
8
  "store",
@@ -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;
@@ -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>) => Promise<TData>, options?: QueryOptions<TData, TVariable, TError>) => ((variable?: TVariable) => ((options?: {
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.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
  /**
@@ -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
  /**
@@ -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(