floppy-disk 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Muhammad Afifudin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,612 @@
1
+ # Floppy Disk 💾
2
+
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 [React-Query](https://tanstack.com/query). Both are awesome state manager, but I want to have something like that with **more power** and **less bundle size**.
6
+
7
+ **Bundle Size Comparison:**
8
+
9
+ ```js
10
+ import { create } from 'zustand'; // 3.3k (gzipped: 1.5k)
11
+
12
+ import { createStore } from 'floppy-disk'; // 1.1k (gzipped: 622) 🎉
13
+
14
+ import {
15
+ QueryClient,
16
+ QueryClientProvider,
17
+ useQuery,
18
+ useInfiniteQuery,
19
+ useMutation,
20
+ } from '@tanstack/react-query'; // 41k (gzipped: 11k)
21
+
22
+ import { createQuery, createMutation } from 'floppy-disk'; // 6.1k (gzipped: 2.2k) 🎉
23
+ ```
24
+
25
+ ## Table of Contents
26
+
27
+ - [Store](#store)
28
+ - [Basic Concept](#basic-concept)
29
+ - [Advanced Concept](#advanced-concept)
30
+ - [Stores](#stores)
31
+ - [Query \& Mutation](#query--mutation)
32
+ - [Single Query](#single-query)
33
+ - [Single Query with Params](#single-query-with-params)
34
+ - [Paginated Query or Infinite Query](#paginated-query-or-infinite-query)
35
+ - [Mutation](#mutation)
36
+ - [Important Notes](#important-notes)
37
+
38
+ ## Store
39
+
40
+ ### Basic Concept
41
+
42
+ Create a store.
43
+
44
+ ```js
45
+ import { createStore } from 'floppy-disk';
46
+
47
+ const useCatStore = createStore(({ set }) => ({
48
+ age: 0,
49
+ isSleeping: false,
50
+ increaseAge: () => set((state) => ({ age: state.age + 1 })),
51
+ reset: () => set({ age: 0, isSleeping: false }),
52
+ }));
53
+ ```
54
+
55
+ Use the hook anywhere, no providers are needed.
56
+
57
+ ```jsx
58
+ function Cat() {
59
+ const { age } = useCatStore((state) => [state.age]);
60
+ return <div>Cat's age: {age}</div>;
61
+ }
62
+
63
+ function Control() {
64
+ const { increaseAge } = useCatStore((state) => [state.increaseAge]);
65
+ return <button onClick={increaseAge}>Increase cat's age</button>;
66
+ }
67
+ ```
68
+
69
+ Control the reactivity. The concept is same as useEffect dependency array.
70
+
71
+ ```jsx
72
+ function Cat() {
73
+ const { age, isSleeping } = useCatStore();
74
+ // Will re-render every state change ^
75
+ return <div>...</div>;
76
+ }
77
+
78
+ function Cat() {
79
+ const { age, isSleeping } = useCatStore((state) => [state.isSleeping]);
80
+ // Will only re-render when isSleeping is updated ^
81
+ // Update on age won't cause re-render this component
82
+ return <div>...</div>;
83
+ }
84
+
85
+ function Cat() {
86
+ const { age, isSleeping } = useCatStore((state) => [state.age, state.isSleeping]);
87
+ // Will re-render when age or isSleeping is updated ^
88
+ return <div>...</div>;
89
+ }
90
+
91
+ function Cat() {
92
+ const { age, isSleeping } = useCatStore((state) => [state.age > 3]);
93
+ // Will only re-render when (age>3) is updated
94
+ return <div>...</div>;
95
+ }
96
+ ```
97
+
98
+ Reading/writing state and reacting to changes outside of components.
99
+
100
+ ```js
101
+ const alertCatAge = () => {
102
+ alert(useCatStore.get().age);
103
+ };
104
+
105
+ const toggleIsSleeping = () => {
106
+ useCatStore.set((state) => ({ isSleeping: !state.isSleeping }));
107
+ };
108
+
109
+ const unsub = useCatStore.subscribe(
110
+ // Action
111
+ (state) => {
112
+ console.log('The value of age is changed!', state.age);
113
+ },
114
+ // Reactivity dependency (just like useEffect dependency mentioned above)
115
+ (state) => [state.age],
116
+ // ^If not set, the action will be triggered on every state change
117
+ );
118
+ ```
119
+
120
+ ### Advanced Concept
121
+
122
+ Set the state **silently** (without broadcast the state change to **any subscribers**).
123
+
124
+ ```jsx
125
+ const decreaseAgeSilently = () => {
126
+ useCatStore.set((state) => ({ age: state.age }), true);
127
+ // ^silent param
128
+ };
129
+ // 👇 Will not re-render
130
+ function Cat() {
131
+ const { age } = useCatStore((state) => [state.age]);
132
+ return <div>Cat's age: {age}</div>;
133
+ }
134
+ ```
135
+
136
+ Store events & interception.
137
+
138
+ ```js
139
+ const useCatStore = createStore(
140
+ ({ set }) => ({
141
+ age: 0,
142
+ isSleeping: false,
143
+ increaseAge: () => set((state) => ({ age: state.age + 1 })),
144
+ reset: () => set({ age: 0, isSleeping: false }),
145
+ }),
146
+ {
147
+ onFirstSubscribe: (state) => {
148
+ console.log('onFirstSubscribe', state);
149
+ },
150
+ onSubscribe: (state) => {
151
+ console.log('onSubscribe', state);
152
+ },
153
+ onUnsubscribe: (state) => {
154
+ console.log('onUnsubscribe', state);
155
+ },
156
+ onLastUnsubscribe: (state) => {
157
+ console.log('onLastUnsubscribe', state);
158
+ },
159
+ intercept: (nextState, prevState) => {
160
+ if (nextState.age !== prevState.age) {
161
+ return { ...nextState, isSleeping: false };
162
+ }
163
+ return nextState;
164
+ },
165
+ },
166
+ );
167
+ ```
168
+
169
+ Let's go wild using IIFE.
170
+
171
+ ```js
172
+ const useCatStore = createStore(
173
+ ({ set }) => ({
174
+ age: 0,
175
+ isSleeping: false,
176
+ increaseAge: () => set((state) => ({ age: state.age + 1 })),
177
+ reset: () => set({ age: 0, isSleeping: false }),
178
+ }),
179
+ (() => {
180
+ const validateCat = () => {
181
+ console.info('Window focus event triggered...');
182
+ const { age } = useCatStore.get();
183
+ if (age > 5) useCatStore.set({ age: 1 });
184
+ };
185
+ return {
186
+ onFirstSubscribe: () => window.addEventListener('focus', validateCat),
187
+ onLastUnsubscribe: () => window.removeEventListener('focus', validateCat),
188
+ };
189
+ })(),
190
+ );
191
+ ```
192
+
193
+ Prevent re-render using `Watch`.
194
+
195
+ ```jsx
196
+ function CatPage() {
197
+ const { age } = useCatStore((state) => [state.age]);
198
+ // If age changed, this component will re-render which will cause
199
+ // HeavyComponent1 & HeavyComponent2 to be re-rendered as well.
200
+ return (
201
+ <main>
202
+ <HeavyComponent1 />
203
+ <div>Cat's age: {age}</div>
204
+ <HeavyComponent2 />
205
+ </main>
206
+ );
207
+ }
208
+
209
+ // Optimized
210
+ function CatPageOptimized() {
211
+ return (
212
+ <main>
213
+ <HeavyComponent1 />
214
+ <useCatStore.Watch
215
+ selectDeps={(state) => [state.age]}
216
+ render={({ age }) => {
217
+ return <div>Cat's age: {age}</div>;
218
+ }}
219
+ />
220
+ <HeavyComponent2 />
221
+ </main>
222
+ );
223
+ }
224
+ ```
225
+
226
+ Want a local state instead of global state?
227
+ Or, want to set the initial state inside component?
228
+
229
+ ```jsx
230
+ const [CatStoreProvider, useCatStoreContext] = withContext(() =>
231
+ createStore(({ set }) => ({
232
+ age: 0,
233
+ isSleeping: false,
234
+ increaseAge: () => set((state) => ({ age: state.age + 1 })),
235
+ reset: () => set({ age: 0, isSleeping: false }),
236
+ })),
237
+ );
238
+
239
+ function Parent() {
240
+ return (
241
+ <>
242
+ <CatStoreProvider>
243
+ <CatAge />
244
+ <CatIsSleeping />
245
+ <WillNotReRenderAsCatStateChanged />
246
+ </CatStoreProvider>
247
+
248
+ <CatStoreProvider>
249
+ <CatAge />
250
+ <CatIsSleeping />
251
+ <WillNotReRenderAsCatStateChanged />
252
+ </CatStoreProvider>
253
+
254
+ <CatStoreProvider onInitialize={(store) => store.set({ age: 99 })}>
255
+ <CatAge />
256
+ <CatIsSleeping />
257
+ <WillNotReRenderAsCatStateChanged />
258
+ </CatStoreProvider>
259
+ </>
260
+ );
261
+ }
262
+
263
+ function CatAge() {
264
+ const { age } = useCatStoreContext()((state) => [state.age]);
265
+ return <div>Age: {age}</div>;
266
+ }
267
+ function CatIsSleeping() {
268
+ const useCatStore = useCatStoreContext();
269
+ const { isSleeping } = useCatStore((state) => [state.isSleeping]);
270
+ return (
271
+ <>
272
+ <div>Is Sleeping: {String(isSleeping)}</div>
273
+ <button onClick={useCatStore.get().increase}>Increase cat age</button>
274
+ </>
275
+ );
276
+ }
277
+ ```
278
+
279
+ Set default reactivity.
280
+
281
+ ```jsx
282
+ const useCatStore = createStore(
283
+ ({ set }) => ({
284
+ age: 0,
285
+ isSleeping: false,
286
+ increaseAge: () => set((state) => ({ age: state.age + 1 })),
287
+ reset: () => set({ age: 0, isSleeping: false }),
288
+ }),
289
+ {
290
+ defaultDeps: (state) => [state.age], // 👈
291
+ },
292
+ );
293
+
294
+ function Cat() {
295
+ const { age } = useCatStore();
296
+ // ^will only re-render when age changed
297
+ return <div>Cat's age: {age}</div>;
298
+ }
299
+ ```
300
+
301
+ ## Stores
302
+
303
+ The concept is same as [store](#store), but this can be used for multiple stores.
304
+
305
+ You need to specify the store key (an object) as identifier.
306
+
307
+ ```js
308
+ import { createStores } from 'floppy-disk';
309
+
310
+ const useCatStores = createStores(
311
+ ({ set, get, key }) => ({
312
+ // ^store key
313
+ age: 0,
314
+ isSleeping: false,
315
+ increaseAge: () => set((state) => ({ age: state.age + 1 })),
316
+ reset: () => set({ age: 0, isSleeping: false }),
317
+ }),
318
+ {
319
+ onBeforeChangeKey: (nextKey, prevKey) => {
320
+ console.log('Store key changed', nextKey, prevKey);
321
+ },
322
+ },
323
+ );
324
+
325
+ function CatPage() {
326
+ const [catId, setCatId] = useState(1);
327
+
328
+ return (
329
+ <>
330
+ <div>Current cat id: {catId}</div>
331
+ <button onClick={() => setCatId((prev) => prev - 1)}>Prev cat</button>
332
+ <button onClick={() => setCatId((prev) => prev + 1)}>Next cat</button>
333
+
334
+ <Cat catId={catId} />
335
+ <Control catId={catId} />
336
+ </>
337
+ );
338
+ }
339
+
340
+ function Cat({ catId }) {
341
+ const { age } = useCatStores({ catId }, (state) => [state.age]);
342
+ return <div>Cat's age: {age}</div>;
343
+ }
344
+
345
+ function Control({ catId }) {
346
+ const { increaseAge } = useCatStores({ catId }, (state) => [state.increaseAge]);
347
+ return <button onClick={increaseAge}>Increase cat's age</button>;
348
+ }
349
+ ```
350
+
351
+ ## Query & Mutation
352
+
353
+ With the power of `createStores` function and a bit creativity, we can easily create a hook just like `useQuery` and `useInfiniteQuery` in [React-Query](https://tanstack.com/query) using `createQuery` function.
354
+
355
+ It can dedupe multiple request, handle caching, auto-update stale data, handle retry on error, handle infinite query, and many more. With the flexibility given in `createStores`, you can extend its power according to your needs.
356
+
357
+ ### Single Query
358
+
359
+ ```jsx
360
+ const useGitHubQuery = createQuery(async () => {
361
+ const res = await fetch('https://api.github.com/repos/afiiif/floppy-disk');
362
+ if (res.ok) return res.json();
363
+ throw res;
364
+ });
365
+
366
+ function SingleQuery() {
367
+ const { isLoading, data } = useGitHubQuery();
368
+
369
+ if (isLoading) return <div>Loading...</div>;
370
+
371
+ return (
372
+ <div>
373
+ <h1>{data.name}</h1>
374
+ <p>{data.description}</p>
375
+ <strong>⭐️ {data.stargazers_count}</strong>
376
+ <strong>🍴 {data.forks_count}</strong>
377
+ </div>
378
+ );
379
+ }
380
+ ```
381
+
382
+ Custom reactivity:
383
+
384
+ ```jsx
385
+ // This component doesn't care whether the query is success or error.
386
+ // It just listening to network fetching state. 👇
387
+ function SingleQueryLoader() {
388
+ const { isWaiting } = useGitHubQuery((state) => [state.isWaiting]);
389
+ return <div>Is network fetching? {String(isWaiting)}</div>;
390
+ }
391
+ ```
392
+
393
+ Actions:
394
+
395
+ ```jsx
396
+ function Actions() {
397
+ const { fetch, forceFetch, markAsStale, reset } = useGitHubQuery(() => []);
398
+ return (
399
+ <>
400
+ <button onClick={fetch}>Call query if the query data is stale</button>
401
+ <button onClick={forceFetch}>Call query</button>
402
+ <button onClick={markAsStale}>Invalidate query</button>
403
+ <button onClick={reset}>Reset query</button>
404
+ </>
405
+ );
406
+ }
407
+ ```
408
+
409
+ Options:
410
+
411
+ ```jsx
412
+ const useGitHubQuery = createQuery(
413
+ async () => {
414
+ const res = await fetch('https://api.github.com/repos/afiiif/floppy-disk');
415
+ if (res.ok) return res.json();
416
+ throw res;
417
+ },
418
+ {
419
+ fetchOnMount: false,
420
+ enabled: () => !!useUserQuery.get().data?.user,
421
+ select: (response) => response.name
422
+ staleTime: Infinity, // Never stale
423
+ retry: 0, // No retry
424
+ onSuccess: (response) => {},
425
+ onError: (error) => {},
426
+ onSettled: () => {},
427
+ },
428
+ );
429
+ ```
430
+
431
+ Get data or do something outside component:
432
+
433
+ ```jsx
434
+ const getData = () => {
435
+ console.log(useGitHubQuery.get().data);
436
+ };
437
+
438
+ const resetQuery = () => {
439
+ useGitHubQuery.get().reset();
440
+ };
441
+
442
+ function Actions() {
443
+ return (
444
+ <>
445
+ <button onClick={getData}>Get Data</button>
446
+ <button onClick={resetQuery}>Reset query</button>
447
+ </>
448
+ );
449
+ }
450
+ ```
451
+
452
+ ### Single Query with Params
453
+
454
+ ```jsx
455
+ const usePokemonQuery = createQuery(async ({ pokemon }) => {
456
+ const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${pokemon}`);
457
+ if (res.ok) return res.json();
458
+ throw res;
459
+ });
460
+
461
+ function PokemonPage() {
462
+ const [currentPokemon, setCurrentPokemon] = useState();
463
+ const { isLoading, data } = usePokemonQuery({ pokemon: currentPokemon });
464
+
465
+ if (isLoading) return <div>Loading...</div>;
466
+
467
+ return (
468
+ <div>
469
+ <h1>{data.name}</h1>
470
+ <div>Weight: {data.weight}</div>
471
+ </div>
472
+ );
473
+ }
474
+ ```
475
+
476
+ Get data or do something outside component:
477
+
478
+ ```jsx
479
+ const getDitto = () => {
480
+ console.log(usePokemonQuery.get({ pokemon: 'ditto' }).data);
481
+ };
482
+
483
+ const resetDitto = () => {
484
+ usePokemonQuery.get({ pokemon: 'ditto' }).reset();
485
+ };
486
+
487
+ function Actions() {
488
+ return (
489
+ <>
490
+ <button onClick={getDitto}>Get Ditto Data</button>
491
+ <button onClick={resetDitto}>Reset Ditto</button>
492
+ </>
493
+ );
494
+ }
495
+ ```
496
+
497
+ ### Paginated Query or Infinite Query
498
+
499
+ ```jsx
500
+ const usePokemonsInfQuery = createQuery(
501
+ async (_, { pageParam = 0 }) => {
502
+ const res = await fetch(`https://pokeapi.co/api/v2/pokemon?limit=10&offset=${pageParam}`);
503
+ if (res.ok) return res.json();
504
+ throw res;
505
+ },
506
+ {
507
+ select: (response, { data }) => [...(data || []), ...response.results],
508
+ getNextPageParam: (lastPageResponse, i) => {
509
+ if (i > 5) return undefined; // Return undefined means you have reached the end of the pages
510
+ return i * 10;
511
+ },
512
+ },
513
+ );
514
+
515
+ function PokemonListPage() {
516
+ const { data, fetchNextPage, hasNextPage, isWaitingNextPage } = usePokemonsInfQuery();
517
+
518
+ return (
519
+ <div>
520
+ {data?.map((pokemon) => (
521
+ <div key={pokemon.name}>{pokemon.name}</div>
522
+ ))}
523
+ {isWaitingNextPage ? (
524
+ <div>Loading more...</div>
525
+ ) : (
526
+ hasNextPage && <button onClick={fetchNextPage}>Load more</button>
527
+ )}
528
+ </div>
529
+ );
530
+ }
531
+ ```
532
+
533
+ **Note:**
534
+
535
+ - The default stale time is 3 seconds.
536
+ - The default reactivity of a query is `(state) => [state.data, state.error]`.
537
+ (For paginated query `(state) => [state.data, state.error, state.isWaitingNextPage, state.hasNextPage]`)
538
+ - You can change the `defaultDeps` on `createQuery` options.
539
+
540
+ ### Mutation
541
+
542
+ ```jsx
543
+ const useLoginMutation = createMutation(
544
+ async (variables) => {
545
+ const res = await axios.post('/auth/login', {
546
+ email: variables.email,
547
+ password: variables.password,
548
+ });
549
+ return res.data;
550
+ },
551
+ {
552
+ onSuccess: (response, variables) => {
553
+ console.log(`Logged in as ${variables.email}`);
554
+ console.log(`Access token: ${response.data.accessToken}`);
555
+ },
556
+ },
557
+ );
558
+
559
+ function Login() {
560
+ const { mutate, isWaiting } = useLoginMutation();
561
+ return (
562
+ <div>
563
+ <button
564
+ disabled={isWaiting}
565
+ onClick={() =>
566
+ mutate({ email: 'foo@bar.baz', password: 's3cREt' }).then(() => {
567
+ showToast('Login success');
568
+ })
569
+ }
570
+ >
571
+ Login
572
+ </button>
573
+ </div>
574
+ );
575
+ }
576
+ ```
577
+
578
+ ## Important Notes
579
+
580
+ Don't mutate.
581
+
582
+ ```js
583
+ import { createStore } from 'floppy-disk';
584
+
585
+ const useCartStore = createStore(({ set, get }) => ({
586
+ products: [],
587
+ addProduct: (newProduct) => {
588
+ const currentProducts = get().products;
589
+ product.push(newProduct); // ❌ Don't mutate
590
+ set({ product });
591
+ },
592
+ }));
593
+ ```
594
+
595
+ You don't need to memoize the reactivity selector.
596
+
597
+ ```jsx
598
+ function Cat() {
599
+ const selectAge = useCallback((state) => [state.age], []); // ❌
600
+ const { age } = useCatStore(selectAge);
601
+ return <div>Cat's age: {age}</div>;
602
+ }
603
+ ```
604
+
605
+ Don't use conditional reactivity selector.
606
+
607
+ ```jsx
608
+ function Cat({ isSomething }) {
609
+ const { age } = useCatStore(isSomething ? (state) => [state.age] : null); // ❌
610
+ return <div>Cat's age: {age}</div>;
611
+ }
612
+ ```
package/esm/index.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from './vanilla';
2
+ export * from './react/create-store';
3
+ export * from './react/create-stores';
4
+ export * from './react/create-query';
5
+ export * from './react/create-mutation';
6
+ export * from './react/with-context';
package/esm/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from './vanilla';
2
+ export * from './react/create-store';
3
+ export * from './react/create-stores';
4
+ export * from './react/create-query';
5
+ export * from './react/create-mutation';
6
+ export * from './react/with-context';
@@ -0,0 +1,21 @@
1
+ import { InitStoreOptions } from '../vanilla';
2
+ export declare type MutationState<TVar, TResponse = any, TError = unknown> = {
3
+ /**
4
+ * Network fetching status.
5
+ */
6
+ isWaiting: boolean;
7
+ isSuccess: boolean;
8
+ isError: boolean;
9
+ response: TResponse | null;
10
+ responseUpdatedAt: number | null;
11
+ error: TError | null;
12
+ errorUpdatedAt: number | null;
13
+ mutate: (variables?: TVar) => Promise<TResponse>;
14
+ };
15
+ export declare type CreateMutationOptions<TVar, TResponse = any, TError = unknown> = InitStoreOptions<MutationState<TVar, TResponse, TError>> & {
16
+ onMutate?: (variables: TVar | undefined, inputState: MutationState<TVar, TResponse, TError>) => void;
17
+ onSuccess?: (response: TResponse, variables: TVar | undefined, inputState: MutationState<TVar, TResponse, TError>) => void;
18
+ onError?: (error: TError, variables: TVar | undefined, inputState: MutationState<TVar, TResponse, TError>) => void;
19
+ onSettled?: (variables: TVar | undefined, inputState: MutationState<TVar, TResponse, TError>) => void;
20
+ };
21
+ export declare const createMutation: <TVar, TResponse = any, TError = unknown>(mutationFn: (variables: TVar | undefined, state: MutationState<TVar, TResponse, TError>) => Promise<TResponse>, options?: CreateMutationOptions<TVar, TResponse, TError>) => import("./create-store").UseStore<MutationState<TVar, TResponse, TError>>;