floppy-disk 3.0.0-alpha.5 → 3.0.0-alpha.6
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 +373 -1
- package/esm/react/create-mutation.d.mts +37 -21
- package/esm/react/create-query.d.mts +24 -22
- package/esm/react/use-mutation.d.mts +82 -0
- package/esm/react.d.mts +2 -1
- package/esm/react.mjs +135 -14
- package/package.json +1 -1
- package/react/create-mutation.d.ts +37 -21
- package/react/create-query.d.ts +24 -22
- package/react/use-mutation.d.ts +82 -0
- package/react.d.ts +2 -1
- package/react.js +134 -12
package/README.md
CHANGED
|
@@ -1,3 +1,375 @@
|
|
|
1
|
-
#
|
|
1
|
+
# FloppyDisk.ts 💾
|
|
2
2
|
|
|
3
3
|
A lightweight, simple, and powerful state management library.
|
|
4
|
+
|
|
5
|
+
This library was highly-inspired by [Zustand](https://www.npmjs.com/package/zustand) and [TanStack-Query](https://tanstack.com/query), they're awesome state manager.
|
|
6
|
+
FloppyDisk provides a very similar developer experience (DX), while introducing additional features and a smaller bundle size.
|
|
7
|
+
|
|
8
|
+
Comparison: https://github.com/afiiif/floppy-disk/tree/beta/comparison
|
|
9
|
+
|
|
10
|
+
Demo: https://afiiif.github.io/floppy-disk/
|
|
11
|
+
|
|
12
|
+
**Installation:**
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
npm install floppy-disk
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Global Store
|
|
19
|
+
|
|
20
|
+
Here's how to create and use a store:
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { createStore } from 'floppy-disk/react';
|
|
24
|
+
|
|
25
|
+
const useDigimon = createStore({
|
|
26
|
+
age: 3,
|
|
27
|
+
level: 'Rookie' as 'In-Training' | 'Rookie' | 'Champion' | 'Ultimate',
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
You can use the store both inside and outside of React components.
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
function MyDigimon() {
|
|
35
|
+
const { age } = useDigimon();
|
|
36
|
+
return <div>Digimon age: {age}</div>;
|
|
37
|
+
// This component will only re-render when `age` changes.
|
|
38
|
+
// Changes to `level` will NOT trigger a re-render.
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function Control() {
|
|
42
|
+
return (
|
|
43
|
+
<>
|
|
44
|
+
<button onClick={() => {
|
|
45
|
+
// You can setState directly
|
|
46
|
+
useDigimon.setState(prev => ({ age: prev.age + 1 }));
|
|
47
|
+
}}>
|
|
48
|
+
Increase digimon's age
|
|
49
|
+
</button>
|
|
50
|
+
|
|
51
|
+
<button onClick={evolve}>Evolve</button>
|
|
52
|
+
</>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// You can create a custom actions
|
|
57
|
+
const evolve = (nextLevel: 'Rookie' | 'Champion' | 'Ultimate') => {
|
|
58
|
+
const { age, level } = useDigimon.getState();
|
|
59
|
+
|
|
60
|
+
const minAge = {
|
|
61
|
+
Rookie: 3,
|
|
62
|
+
Champion: 6,
|
|
63
|
+
Ultimate: 11,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (age < minAge[nextLevel]) {
|
|
67
|
+
return console.warn('Not enough age');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
useDigimon.setState({ level: nextLevel });
|
|
71
|
+
};
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Differences from Zustand
|
|
75
|
+
|
|
76
|
+
If you're coming from Zustand, this should feel very familiar.\
|
|
77
|
+
Key differences:
|
|
78
|
+
|
|
79
|
+
1. **No Selectors Needed**\
|
|
80
|
+
You don't need selectors when using hooks.
|
|
81
|
+
FloppyDisk automatically tracks which parts of the state are used and optimizes re-renders accordingly.
|
|
82
|
+
2. **Object-Only Store Initialization**\
|
|
83
|
+
In FloppyDisk, stores **must** be initialized with an object. Primitive values or function initializers are not allowed.
|
|
84
|
+
|
|
85
|
+
Zustand examples:
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
const useExample1 = create(123);
|
|
89
|
+
|
|
90
|
+
const useExample2 = create(set => ({
|
|
91
|
+
value: 1,
|
|
92
|
+
inc: () => set(prev => ({ value: prev.value + 1 })),
|
|
93
|
+
}));
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
FloppyDisk equivalents:
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
const useExample1 = createStore({ value: 123 });
|
|
100
|
+
|
|
101
|
+
// Unlike Zustand, defining actions inside the store is **discouraged** in FloppyDisk.
|
|
102
|
+
// This improves tree-shakeability and keeps your store minimal.
|
|
103
|
+
const useExample2 = createStore({ value: 1 });
|
|
104
|
+
const inc = () => useExample2.setState(prev => ({ value: prev.value + 1 }));
|
|
105
|
+
|
|
106
|
+
// However, it's still possible if you understand how closures work:
|
|
107
|
+
const useExample2Alt = createStore({
|
|
108
|
+
value: 1,
|
|
109
|
+
inc: () => useExample2Alt.setState(prev => ({ value: prev.value + 1 })),
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Async State (Query & Mutation)
|
|
114
|
+
|
|
115
|
+
FloppyDisk also provides a powerful async state layer, inspired by [TanStack-Query](https://tanstack.com/query) but with a simpler API.
|
|
116
|
+
|
|
117
|
+
It is agnostic to the type of async operation,
|
|
118
|
+
it works with any Promise-based operation—whether it's a network request, local computation, storage access, or something else.
|
|
119
|
+
|
|
120
|
+
Because of that, we intentionally avoid terms like "fetch" or "refetch".\
|
|
121
|
+
Instead, we use:
|
|
122
|
+
|
|
123
|
+
- **execute** → run the async operation (same as "fetch" in TanStack-Query)
|
|
124
|
+
- **revalidate** → re-run while keeping existing data (same as "refetch" in TanStack-Query)
|
|
125
|
+
|
|
126
|
+
### Query vs Mutation
|
|
127
|
+
|
|
128
|
+
<details>
|
|
129
|
+
|
|
130
|
+
<summary>Query → Read Operations</summary>
|
|
131
|
+
|
|
132
|
+
Queries are designed for reading data.\
|
|
133
|
+
They assume:
|
|
134
|
+
|
|
135
|
+
- no side effects
|
|
136
|
+
- no data mutation
|
|
137
|
+
- safe to run multiple times
|
|
138
|
+
|
|
139
|
+
Because of this, queries come with helpful defaults:
|
|
140
|
+
|
|
141
|
+
- ✅ Retry mechanism (for transient failures)
|
|
142
|
+
- ✅ Revalidation (keep data fresh automatically)
|
|
143
|
+
- ✅ Caching & staleness control
|
|
144
|
+
|
|
145
|
+
Use queries when:
|
|
146
|
+
|
|
147
|
+
- fetching data
|
|
148
|
+
- reading from storage
|
|
149
|
+
- running idempotent async logic
|
|
150
|
+
|
|
151
|
+
</details>
|
|
152
|
+
|
|
153
|
+
<details>
|
|
154
|
+
|
|
155
|
+
<summary>Mutation → Write Operations</summary>
|
|
156
|
+
|
|
157
|
+
Mutations are designed for changing data.\
|
|
158
|
+
Examples:
|
|
159
|
+
|
|
160
|
+
- insert
|
|
161
|
+
- update
|
|
162
|
+
- delete
|
|
163
|
+
- triggering side effects
|
|
164
|
+
|
|
165
|
+
Because mutations are **not safe to repeat blindly**, FloppyDisk does **not** include:
|
|
166
|
+
|
|
167
|
+
- ❌ automatic retry
|
|
168
|
+
- ❌ automatic revalidation
|
|
169
|
+
- ❌ implicit re-execution
|
|
170
|
+
|
|
171
|
+
This is intentional.\
|
|
172
|
+
Mutations should be explicit and controlled, not automatic.
|
|
173
|
+
|
|
174
|
+
If you need retry mechanism, then you can always add it manually.
|
|
175
|
+
|
|
176
|
+
</details>
|
|
177
|
+
|
|
178
|
+
### Single Query
|
|
179
|
+
|
|
180
|
+
Create a query using `createQuery`:
|
|
181
|
+
|
|
182
|
+
```tsx
|
|
183
|
+
import { createQuery } from 'floppy-disk/react';
|
|
184
|
+
|
|
185
|
+
const myCoolQuery = createQuery(
|
|
186
|
+
myAsyncFn,
|
|
187
|
+
// { staleTime: 5000, revalidateOnFocus: false } <-- optional options
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const useMyCoolQuery = myCoolQuery();
|
|
191
|
+
|
|
192
|
+
// Use it inside your component:
|
|
193
|
+
|
|
194
|
+
function MyComponent() {
|
|
195
|
+
const query = useMyCoolQuery();
|
|
196
|
+
if (query.state === 'INITIAL') return <div>Loading...</div>;
|
|
197
|
+
if (query.error) return <div>Error: {query.error.message}</div>;
|
|
198
|
+
return <div>{JSON.stringify(query.data)}</div>;
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Query State: Two Independent Dimensions
|
|
203
|
+
|
|
204
|
+
FloppyDisk tracks two things separately:
|
|
205
|
+
|
|
206
|
+
- Is it running? → `isPending`\
|
|
207
|
+
(value: `boolean`)
|
|
208
|
+
- What's the result? → `state`\
|
|
209
|
+
(value: `INITIAL | 'SUCCESS' | 'ERROR' | 'SUCCESS_BUT_REVALIDATION_ERROR'`)
|
|
210
|
+
|
|
211
|
+
They are **independent**.
|
|
212
|
+
|
|
213
|
+
### Automatic Re-render Optimization
|
|
214
|
+
|
|
215
|
+
Just like the global store, FloppyDisk tracks usage automatically:
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
const { data } = useMyQuery();
|
|
219
|
+
// ^Only data changes will trigger a re-render
|
|
220
|
+
|
|
221
|
+
const value = useMyQuery().data?.foo.bar.baz;
|
|
222
|
+
// ^Only data.foo.bar.baz changes will trigger a re-render
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Keyed Query (Dynamic Params)
|
|
226
|
+
|
|
227
|
+
You can create parameterized queries:
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
import { getUserById, type GetUserByIdResponse } from '../utils';
|
|
231
|
+
|
|
232
|
+
type MyQueryParam = { id: string };
|
|
233
|
+
|
|
234
|
+
const userQuery = createQuery<GetUserByIdResponse, MyQueryParam>(
|
|
235
|
+
getUserById,
|
|
236
|
+
// { staleTime: 5000, revalidateOnFocus: false } <-- optional options
|
|
237
|
+
);
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Use it with parameters:
|
|
241
|
+
|
|
242
|
+
```tsx
|
|
243
|
+
function UserDetail({ id }) {
|
|
244
|
+
const useUserQuery = userQuery({ id: 1 });
|
|
245
|
+
const query = useUserQuery();
|
|
246
|
+
if (query.state === 'INITIAL') return <div>Loading...</div>;
|
|
247
|
+
if (query.error) return <div>Error: {query.error.message}</div>;
|
|
248
|
+
return <div>{JSON.stringify(query.data)}</div>;
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Each unique parameter creates its own cache entry.
|
|
253
|
+
|
|
254
|
+
### Infinite Query
|
|
255
|
+
|
|
256
|
+
FloppyDisk does **not provide** a dedicated "infinite query" API.\
|
|
257
|
+
Instead, it embraces a simpler and more flexible approach:
|
|
258
|
+
|
|
259
|
+
> _Infinite queries are just **composition** + **recursion**._
|
|
260
|
+
|
|
261
|
+
Why? Because async state is already powerful enough:
|
|
262
|
+
|
|
263
|
+
- keyed queries handle parameters
|
|
264
|
+
- components handle composition
|
|
265
|
+
- recursion handles pagination
|
|
266
|
+
|
|
267
|
+
No special abstraction needed.
|
|
268
|
+
|
|
269
|
+
Here is the example on how to implement infinite query properly:
|
|
270
|
+
|
|
271
|
+
```tsx
|
|
272
|
+
type GetPostParams = {
|
|
273
|
+
cursor?: string; // For pagination
|
|
274
|
+
};
|
|
275
|
+
type GetPostsResponse = {
|
|
276
|
+
posts: Post[];
|
|
277
|
+
meta: { nextCursor: string };
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const postsQuery = createQuery<GetPostsResponse, GetPostParams>(
|
|
281
|
+
getPosts,
|
|
282
|
+
{
|
|
283
|
+
staleTime: Infinity,
|
|
284
|
+
revalidateOnFocus: false,
|
|
285
|
+
revalidateOnReconnect: false,
|
|
286
|
+
},
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
function Main() {
|
|
290
|
+
return <Page cursor={undefined} />;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function Page({ cursor }: { cursor?: string }) {
|
|
294
|
+
const usePostsQuery = postsQuery({ cursor });
|
|
295
|
+
const { state, data, error } = usePostsQuery();
|
|
296
|
+
|
|
297
|
+
if (state === 'INITIAL') return <div>Loading...</div>;
|
|
298
|
+
if (error) return <div>Error</div>;
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<>
|
|
302
|
+
{data.posts.map(post => (
|
|
303
|
+
<PostCard key={post.id} post={post} />
|
|
304
|
+
))}
|
|
305
|
+
{data.meta.nextCursor && (
|
|
306
|
+
<LoadMore nextCursor={data.meta.nextCursor} />
|
|
307
|
+
)}
|
|
308
|
+
</>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function LoadMore({ nextCursor }: { nextCursor?: string }) {
|
|
313
|
+
const [isNextPageRequested, setIsNextPageRequested] = useState(() => {
|
|
314
|
+
const stateOfNextPageQuery = postsQuery({ cursor: nextCursor }).getState();
|
|
315
|
+
return stateOfNextPageQuery.isPending || stateOfNextPageQuery.isSuccess;
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
if (isNextPageRequested) {
|
|
319
|
+
return <Page cursor={nextCursor} />;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return (
|
|
323
|
+
<ReachingBottomObserver
|
|
324
|
+
onReachBottom={() => setIsNextPageRequested(true)}
|
|
325
|
+
/>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
When implementing infinite queries, it is **highly recommended to disable automatic revalidation**.
|
|
331
|
+
|
|
332
|
+
Why?\
|
|
333
|
+
In an infinite list, users may scroll through many pages ("_doom-scrolling_").\
|
|
334
|
+
If revalidation is triggered:
|
|
335
|
+
|
|
336
|
+
- All previously loaded pages may re-execute
|
|
337
|
+
- Content at the top may change without the user noticing
|
|
338
|
+
- Layout shifts can occur unexpectedly
|
|
339
|
+
|
|
340
|
+
This leads to a **confusing and unstable user experience**.\
|
|
341
|
+
Revalidating dozens of previously viewed pages rarely provides value to the user.
|
|
342
|
+
|
|
343
|
+
## SSR Guidance
|
|
344
|
+
|
|
345
|
+
FloppyDisk is designed primarily for **client-side [sync/async] state**.
|
|
346
|
+
|
|
347
|
+
If your data is already fetched on the server (e.g. via SSR/ISR, Server Components, or Server Actions), then:
|
|
348
|
+
|
|
349
|
+
> **You most likely don't need this library.**
|
|
350
|
+
|
|
351
|
+
This is the same philosophy as TanStack Query. 💡
|
|
352
|
+
|
|
353
|
+
In many cases, developers mix SSR/ISR with client-side state because they want:
|
|
354
|
+
|
|
355
|
+
1. Data to be rendered into HTML on the server
|
|
356
|
+
2. The ability to **revalidate it on the client**
|
|
357
|
+
|
|
358
|
+
A common (but inefficient) approach is:
|
|
359
|
+
|
|
360
|
+
- fetch on the server
|
|
361
|
+
- hydrate it into a client-side cache
|
|
362
|
+
- then revalidate using a query library
|
|
363
|
+
|
|
364
|
+
While this works, it introduces additional complexity.
|
|
365
|
+
|
|
366
|
+
Instead, we encourage a simpler approach:
|
|
367
|
+
|
|
368
|
+
> If your data is fetched on the server, revalidate it using **your framework's built-in mechanism** (e.g. Next.js route revalidation).
|
|
369
|
+
|
|
370
|
+
Because of this philosophy, FloppyDisk **does not support** hydrating server-fetched data into the client store.
|
|
371
|
+
|
|
372
|
+
This keeps the mental model clean:
|
|
373
|
+
|
|
374
|
+
- server data → handled by the framework
|
|
375
|
+
- client async state → handled by FloppyDisk
|
|
@@ -14,7 +14,7 @@ import { type InitStoreOptions, type SetState } from 'floppy-disk/vanilla';
|
|
|
14
14
|
* - No retry mechanism
|
|
15
15
|
* - No caching across executions
|
|
16
16
|
*/
|
|
17
|
-
export type MutationState<TData, TVariable> = {
|
|
17
|
+
export type MutationState<TData, TVariable, TError> = {
|
|
18
18
|
isPending: boolean;
|
|
19
19
|
} & ({
|
|
20
20
|
state: 'INITIAL';
|
|
@@ -41,28 +41,42 @@ export type MutationState<TData, TVariable> = {
|
|
|
41
41
|
variable: TVariable;
|
|
42
42
|
data: undefined;
|
|
43
43
|
dataUpdatedAt: undefined;
|
|
44
|
-
error:
|
|
44
|
+
error: TError;
|
|
45
45
|
errorUpdatedAt: number;
|
|
46
46
|
});
|
|
47
|
+
export declare const INITIAL_STATE: {
|
|
48
|
+
state: string;
|
|
49
|
+
isPending: boolean;
|
|
50
|
+
isSuccess: boolean;
|
|
51
|
+
isError: boolean;
|
|
52
|
+
variable: undefined;
|
|
53
|
+
data: undefined;
|
|
54
|
+
dataUpdatedAt: undefined;
|
|
55
|
+
error: undefined;
|
|
56
|
+
errorUpdatedAt: undefined;
|
|
57
|
+
};
|
|
47
58
|
/**
|
|
48
59
|
* Configuration options for a mutation.
|
|
49
60
|
*
|
|
50
61
|
* @remarks
|
|
51
62
|
* Lifecycle callbacks are triggered for each execution.
|
|
52
63
|
*/
|
|
53
|
-
export type MutationOptions<TData, TVariable> = InitStoreOptions<MutationState<TData, TVariable>> & {
|
|
64
|
+
export type MutationOptions<TData, TVariable, TError = Error> = InitStoreOptions<MutationState<TData, TVariable, TError>> & {
|
|
54
65
|
/**
|
|
55
|
-
* Called when the mutation succeeds
|
|
66
|
+
* Called when the mutation succeeds.\
|
|
67
|
+
* If multiple concurrent executions happened, only the latest execution triggers this callback.
|
|
56
68
|
*/
|
|
57
|
-
onSuccess?: (data: TData, variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable>) => void;
|
|
69
|
+
onSuccess?: (data: TData, variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable, TError>) => void;
|
|
58
70
|
/**
|
|
59
|
-
* Called when the mutation fails
|
|
71
|
+
* Called when the mutation fails.\
|
|
72
|
+
* If multiple concurrent executions happened, only the latest execution triggers this callback.
|
|
60
73
|
*/
|
|
61
|
-
onError?: (error:
|
|
74
|
+
onError?: (error: TError, variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable, TError>) => void;
|
|
62
75
|
/**
|
|
63
|
-
* Called after the mutation settles (either success or error)
|
|
76
|
+
* Called after the mutation settles (either success or error).\
|
|
77
|
+
* If multiple concurrent executions happened, only the latest execution triggers this callback.
|
|
64
78
|
*/
|
|
65
|
-
onSettled?: (variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable>) => void;
|
|
79
|
+
onSettled?: (variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable, TError>) => void;
|
|
66
80
|
};
|
|
67
81
|
/**
|
|
68
82
|
* Creates a mutation store for handling async operations that modify data.
|
|
@@ -78,8 +92,10 @@ export type MutationOptions<TData, TVariable> = InitStoreOptions<MutationState<T
|
|
|
78
92
|
* - Mutations are **not cached** and only track the latest execution.
|
|
79
93
|
* - Designed for operations that change data (e.g. create, update, delete).
|
|
80
94
|
* - No retry mechanism is provided by default.
|
|
81
|
-
* - Each execution overwrites the previous state.
|
|
82
95
|
* - The mutation always resolves (never throws): the result contains either `data` or `error`.
|
|
96
|
+
* - If multiple executions triggered at the same time:
|
|
97
|
+
* - Only the latest execution is allowed to update the state.
|
|
98
|
+
* - Results from previous executions are ignored if a newer one exists.
|
|
83
99
|
*
|
|
84
100
|
* @example
|
|
85
101
|
* const useCreateUser = createMutation(async (input) => {
|
|
@@ -89,10 +105,10 @@ export type MutationOptions<TData, TVariable> = InitStoreOptions<MutationState<T
|
|
|
89
105
|
* const { isPending } = useCreateUser();
|
|
90
106
|
* const result = await useCreateUser.execute({ name: 'John' });
|
|
91
107
|
*/
|
|
92
|
-
export declare const createMutation: <TData, TVariable = undefined>(mutationFn: (variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable>) => Promise<TData>, options?: MutationOptions<TData, TVariable>) => (() => MutationState<TData, TVariable>) & {
|
|
93
|
-
subscribe: (subscriber: import("../vanilla.d.mts").Subscriber<MutationState<TData, TVariable>>) => () => void;
|
|
94
|
-
getSubscribers: () => Set<import("../vanilla.d.mts").Subscriber<MutationState<TData, TVariable>>>;
|
|
95
|
-
getState: () => MutationState<TData, TVariable>;
|
|
108
|
+
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
|
+
subscribe: (subscriber: import("../vanilla.d.mts").Subscriber<MutationState<TData, TVariable, TError>>) => () => void;
|
|
110
|
+
getSubscribers: () => Set<import("../vanilla.d.mts").Subscriber<MutationState<TData, TVariable, TError>>>;
|
|
111
|
+
getState: () => MutationState<TData, TVariable, TError>;
|
|
96
112
|
/**
|
|
97
113
|
* Manually updates the mutation state.
|
|
98
114
|
*
|
|
@@ -100,7 +116,7 @@ export declare const createMutation: <TData, TVariable = undefined>(mutationFn:
|
|
|
100
116
|
* - Intended for advanced use cases.
|
|
101
117
|
* - Prefer using provided mutation actions (`execute`, `reset`) instead.
|
|
102
118
|
*/
|
|
103
|
-
setState: (value: SetState<MutationState<TData, TVariable>>) => void;
|
|
119
|
+
setState: (value: SetState<MutationState<TData, TVariable, TError>>) => void;
|
|
104
120
|
/**
|
|
105
121
|
* Executes the mutation.
|
|
106
122
|
*
|
|
@@ -111,25 +127,25 @@ export declare const createMutation: <TData, TVariable = undefined>(mutationFn:
|
|
|
111
127
|
* - `{ error, variable }` on failure
|
|
112
128
|
*
|
|
113
129
|
* @remarks
|
|
114
|
-
* - If a mutation is already in progress, a warning is logged.
|
|
115
|
-
* - Concurrent executions are allowed but may lead to race conditions.
|
|
116
130
|
* - The promise never rejects to simplify async handling.
|
|
131
|
+
* - If a mutation is already in progress, a warning is logged.
|
|
132
|
+
* - When a new execution starts, all previous pending executions will resolve with the result of the latest execution.
|
|
117
133
|
*/
|
|
118
134
|
execute: TVariable extends undefined ? () => Promise<{
|
|
119
135
|
variable: undefined;
|
|
120
136
|
data?: TData;
|
|
121
|
-
error?:
|
|
137
|
+
error?: TError;
|
|
122
138
|
}> : (variable: TVariable) => Promise<{
|
|
123
139
|
variable: TVariable;
|
|
124
140
|
data?: TData;
|
|
125
|
-
error?:
|
|
141
|
+
error?: TError;
|
|
126
142
|
}>;
|
|
127
143
|
/**
|
|
128
144
|
* Resets the mutation state back to its initial state.
|
|
129
145
|
*
|
|
130
146
|
* @remarks
|
|
131
|
-
* - Does not cancel any ongoing
|
|
132
|
-
* - If
|
|
147
|
+
* - Does not cancel any ongoing execution.
|
|
148
|
+
* - If an execution is still pending, its result may override the reset state.
|
|
133
149
|
*/
|
|
134
150
|
reset: () => void;
|
|
135
151
|
};
|
|
@@ -19,7 +19,7 @@ import { type InitStoreOptions, type SetState } from 'floppy-disk/vanilla';
|
|
|
19
19
|
* @remarks
|
|
20
20
|
* - Data and error are mutually exclusive except in `SUCCESS_BUT_REVALIDATION_ERROR`.
|
|
21
21
|
*/
|
|
22
|
-
export type QueryState<TData> = {
|
|
22
|
+
export type QueryState<TData, TError> = {
|
|
23
23
|
isPending: boolean;
|
|
24
24
|
isRevalidating: boolean;
|
|
25
25
|
isRetrying: boolean;
|
|
@@ -46,7 +46,7 @@ export type QueryState<TData> = {
|
|
|
46
46
|
isError: true;
|
|
47
47
|
data: undefined;
|
|
48
48
|
dataUpdatedAt: undefined;
|
|
49
|
-
error:
|
|
49
|
+
error: TError;
|
|
50
50
|
errorUpdatedAt: number;
|
|
51
51
|
} | {
|
|
52
52
|
state: 'SUCCESS_BUT_REVALIDATION_ERROR';
|
|
@@ -54,7 +54,7 @@ export type QueryState<TData> = {
|
|
|
54
54
|
isError: false;
|
|
55
55
|
data: TData;
|
|
56
56
|
dataUpdatedAt: number;
|
|
57
|
-
error:
|
|
57
|
+
error: TError;
|
|
58
58
|
errorUpdatedAt: number;
|
|
59
59
|
});
|
|
60
60
|
/**
|
|
@@ -63,7 +63,7 @@ export type QueryState<TData> = {
|
|
|
63
63
|
* @remarks
|
|
64
64
|
* Controls caching, retry behavior, lifecycle, and side effects of an async operation.
|
|
65
65
|
*/
|
|
66
|
-
export type QueryOptions<TData, TVariable extends Record<string, any
|
|
66
|
+
export type QueryOptions<TData, TVariable extends Record<string, any>, TError = Error> = InitStoreOptions<QueryState<TData, TError>> & {
|
|
67
67
|
/**
|
|
68
68
|
* Time (in milliseconds) that data is considered fresh.
|
|
69
69
|
*
|
|
@@ -95,15 +95,15 @@ export type QueryOptions<TData, TVariable extends Record<string, any>> = InitSto
|
|
|
95
95
|
/**
|
|
96
96
|
* Called when the query succeeds.
|
|
97
97
|
*/
|
|
98
|
-
onSuccess?: (data: TData, variable: TVariable, stateBeforeExecute: QueryState<TData>) => void;
|
|
98
|
+
onSuccess?: (data: TData, variable: TVariable, stateBeforeExecute: QueryState<TData, TError>) => void;
|
|
99
99
|
/**
|
|
100
100
|
* Called when the query fails and will not retry.
|
|
101
101
|
*/
|
|
102
|
-
onError?: (error:
|
|
102
|
+
onError?: (error: TError, variable: TVariable, stateBeforeExecute: QueryState<TData, TError>) => void;
|
|
103
103
|
/**
|
|
104
104
|
* Called after the query settles (success or final failure).
|
|
105
105
|
*/
|
|
106
|
-
onSettled?: (variable: TVariable, stateBeforeExecute: QueryState<TData>) => void;
|
|
106
|
+
onSettled?: (variable: TVariable, stateBeforeExecute: QueryState<TData, TError>) => void;
|
|
107
107
|
/**
|
|
108
108
|
* Determines whether a failed query should retry.
|
|
109
109
|
*
|
|
@@ -120,7 +120,7 @@ export type QueryOptions<TData, TVariable extends Record<string, any>> = InitSto
|
|
|
120
120
|
* return [false];
|
|
121
121
|
* }
|
|
122
122
|
*/
|
|
123
|
-
shouldRetry?: (error:
|
|
123
|
+
shouldRetry?: (error: TError, currentState: QueryState<TData, TError>) => [true, number] | [false];
|
|
124
124
|
};
|
|
125
125
|
/**
|
|
126
126
|
* Creates a query factory for managing cached async operations.
|
|
@@ -157,13 +157,15 @@ export type QueryOptions<TData, TVariable extends Record<string, any>> = InitSto
|
|
|
157
157
|
* // ...
|
|
158
158
|
* }
|
|
159
159
|
*/
|
|
160
|
-
export declare const createQuery: <TData, TVariable extends Record<string, any> = never>(queryFn: (variable: TVariable, currentState: QueryState<TData>) => Promise<TData>, options?: QueryOptions<TData, TVariable>) => ((variable?: TVariable) => ((options?: {
|
|
160
|
+
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?: {
|
|
161
161
|
/**
|
|
162
|
-
* Whether the query should
|
|
162
|
+
* Whether the query should be ravalidated automatically on mount.
|
|
163
|
+
*
|
|
164
|
+
* Revalidate means execute the queryFn **if stale/invalidated**.
|
|
163
165
|
*
|
|
164
166
|
* @default true
|
|
165
167
|
*/
|
|
166
|
-
|
|
168
|
+
revalidateOnMount?: boolean;
|
|
167
169
|
/**
|
|
168
170
|
* Whether to keep previous successful data while a new variable is loading.
|
|
169
171
|
*
|
|
@@ -180,13 +182,13 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
|
|
|
180
182
|
* // While loading userId=2, still show userId=1 data
|
|
181
183
|
* useQuery({ id: userId }, { keepPreviousData: true });
|
|
182
184
|
*/ keepPreviousData?: boolean;
|
|
183
|
-
}) => QueryState<TData>) & {
|
|
185
|
+
}) => QueryState<TData, TError>) & {
|
|
184
186
|
metadata: {
|
|
185
187
|
isInvalidated?: boolean;
|
|
186
|
-
promise?: Promise<QueryState<TData>> | undefined;
|
|
187
|
-
promiseResolver?: ((value: QueryState<TData> | PromiseLike<QueryState<TData>>) => void) | undefined;
|
|
188
|
+
promise?: Promise<QueryState<TData, TError>> | undefined;
|
|
189
|
+
promiseResolver?: ((value: QueryState<TData, TError> | PromiseLike<QueryState<TData, TError>>) => void) | undefined;
|
|
188
190
|
retryTimeoutId?: number;
|
|
189
|
-
retryResolver?: ((value: QueryState<TData> | PromiseLike<QueryState<TData>>) => void) | undefined;
|
|
191
|
+
retryResolver?: ((value: QueryState<TData, TError> | PromiseLike<QueryState<TData, TError>>) => void) | undefined;
|
|
190
192
|
garbageCollectionTimeoutId?: number;
|
|
191
193
|
rollbackData?: TData | undefined;
|
|
192
194
|
};
|
|
@@ -222,7 +224,7 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
|
|
|
222
224
|
*/
|
|
223
225
|
execute: (options?: {
|
|
224
226
|
overwriteOngoingExecution?: boolean;
|
|
225
|
-
}) => Promise<QueryState<TData>>;
|
|
227
|
+
}) => Promise<QueryState<TData, TError>>;
|
|
226
228
|
/**
|
|
227
229
|
* Re-executes the query if needed based on freshness or invalidation.
|
|
228
230
|
*
|
|
@@ -238,7 +240,7 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
|
|
|
238
240
|
*/
|
|
239
241
|
revalidate: (options?: {
|
|
240
242
|
overwriteOngoingExecution?: boolean;
|
|
241
|
-
}) => Promise<QueryState<TData>>;
|
|
243
|
+
}) => Promise<QueryState<TData, TError>>;
|
|
242
244
|
/**
|
|
243
245
|
* Marks the query as invalidated and optionally triggers re-execution.
|
|
244
246
|
*
|
|
@@ -290,7 +292,7 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
|
|
|
290
292
|
* const { rollback, revalidate } = query.optimisticUpdate(newData);
|
|
291
293
|
*/
|
|
292
294
|
optimisticUpdate: (data: TData) => {
|
|
293
|
-
revalidate: () => Promise<QueryState<TData>>;
|
|
295
|
+
revalidate: () => Promise<QueryState<TData, TError>>;
|
|
294
296
|
rollback: () => TData;
|
|
295
297
|
};
|
|
296
298
|
/**
|
|
@@ -302,10 +304,10 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
|
|
|
302
304
|
* - Should be used if an optimistic update fails.
|
|
303
305
|
*/
|
|
304
306
|
rollbackOptimisticUpdate: () => TData;
|
|
305
|
-
subscribe: (subscriber: import("../vanilla.d.mts").Subscriber<QueryState<TData>>) => () => void;
|
|
306
|
-
getSubscribers: () => Set<import("../vanilla.d.mts").Subscriber<QueryState<TData>>>;
|
|
307
|
-
getState: () => QueryState<TData>;
|
|
308
|
-
setState: (value: SetState<QueryState<TData>>) => void;
|
|
307
|
+
subscribe: (subscriber: import("../vanilla.d.mts").Subscriber<QueryState<TData, TError>>) => () => void;
|
|
308
|
+
getSubscribers: () => Set<import("../vanilla.d.mts").Subscriber<QueryState<TData, TError>>>;
|
|
309
|
+
getState: () => QueryState<TData, TError>;
|
|
310
|
+
setState: (value: SetState<QueryState<TData, TError>>) => void;
|
|
309
311
|
}) & {
|
|
310
312
|
/**
|
|
311
313
|
* Executes all query instances.
|