Introducing Floppy Disk
The Zustand-ReactQuery alternative for local, global, sync, or async state management.
Why?
In the beginning, I made this library just as part of my learning journey. Not trying to re-invent the wheel, but I just want to challange myself whether I can build something like that.
I always thought Zustand (opens in a new tab) & TanStack-Query (opens in a new tab) (a.k.a React-Query) were really cool. I love the simplicity of Zustand. I am truly impressed with the completeness of the features offered by React-Query. So, it will be a really huge challange to replicate both functionalities.
In the end, I managed to replicate more than 90% of their functionalities (see comparison), with the developer experience I desired, and some additional advantages.
How?
- Zustand-Query
In the initial phase, I try to re-create something like React-Query using Zustand. A "Zutand-Query" 😆. But then I encountered difficulties because in Zustand, there is no store "key" that can be used as a query key. I feel like there is no way out unless I create something like Zustand, but with more power.
- Zustand++
As I mentioned previously, finally I create a Zustand-like state manager, but with more features:
- Not only can create a "store", but it also can create "stores" that can be controlled by a store key.
- Added store event handler:
onSubscribe
,onUnsubscribe
,onFirstSubscribe
,onLastUnsubscribe
.
- Go Deeper Into React-Query and SWR
In this phase, I learned about: how to serialize a query key into a string, the different options naming between React-Query & SWR, the behavior of bi-directional query, race-condition prevention, React suspense, and many more.
- The Born of Floppy Disk
Finally I managed to create:
- A global state manager just like Zustand.
- A data fetching state manager just like TanStack Query & SWR, but it tastes like Zustand.
- A smaller bundle size package.
import { create } from 'zustand'; // 3.3 kB (gzipped: 1.5 kB)
import { createStore } from 'floppy-disk'; // 1.4 kB (gzipped: 725 B) 🎉
import { QueryClient, QueryClientProvider, useQuery, useInfiniteQuery, useMutation } from '@tanstack/react-query'; // 41 kB (gzipped: 11 kB)
import { createQuery, createMutation } from 'floppy-disk'; // 9.6 kB (gzipped: 3.2 kB) 🎉
Comparison
In the end, I managed to replicate more than 90% of their functionalities (see comparison), with the developer experience I desired, and some additional advantages.
I want to explain the developer experience part and the additional advantages part.
Query Key & Query Function
I'm not a fan of the experience in React-Query regarding the use of a query key to refer a query. It's a bit difficult to ensure its uniqueness. In React-Query, we can't be 100% sure if there is no duplicate query key, even if we already use Query Key factories (opens in a new tab).
const MY_QUERY = 'my-query';
const MY_OTHER_QUERY = 'my-query'; // 🔴
const myQuery = useQuery([MY_QUERY], myQueryFn);
const myOtherQuery = useQuery([MY_OTHER_QUERY], otherFunction); // 🔴
const state = queryClient.getQueryState({ queryKey: MY_QUERY });
// ^ Is this state comes from myQueryFn or otherFunction?
In Floppy Disk, we refer a query with the query object itself, no ambiguity:
const useQuery = createQuery(myQueryFn);
const myOtherQuery = createQuery(otherFunction);
useQuery.get();
myOtherQuery.get();
useQuery.invalidate();
myOtherQuery.invalidate();
Optimistic Update
Floppy Disk gives a simpler code for optimistic update, which automatically cancel the pending query to prevent race condition, and return a revert & invalidate function.
const { revert, invalidate } = useProfileQuery.optimisticUpdate({
response: payload,
});
// Specific id:
const { revert, invalidate } = useProductDetailQuery.optimisticUpdate({
key: { id: payload.id },
response: payload,
});
Bi-Directional Query
In Floppy Disk, bi-directional query is treated like 2 independent infinite query. So, we can start refetch from the initial page and have a parallel refeching for next & previous pages. Therefore, we can obtain the information in which direction we are fetching/refetching.
React-Query:
Initial:
- Initial fetch (page 5)
- Fetch next (page 6)
- Fetch previous (page 4)
Pages: [4, 5, 6]
Refetch:
- Refetch (page 4)
- Fetch next (page 5)
- Fetch next (page 6)
Floppy Disk:
Initial:
- Initial fetch (page 5)
- Fetch next (page 6)
- Fetch previous (page 4)
Pages: { next: [5, 6], prev: [4] }
Refetch:
- Refetch initial page (page 5)
- Fetch next pages (page 6) and previous pages (page 4) parallelly sequentially
It was inspired from this issue: https://github.com/TanStack/query/issues/1401 (opens in a new tab).
Misc.
There are some other improvement:
- A simplified DX for infinite query
- A better query state explanation
- More ways to handle
onSuccess
/onError
in mutation - Easier to control subscription (re-render)
- And a lot more, see docs
Epilogue
Zustand (opens in a new tab) & React-Query (opens in a new tab) are awesome production-ready library. If you love both libraries, then I think you should try Floppy Disk, but might not need to switch to it, unless it really solved the problem you encounted when using Zustand/React-Query.
Anyway, why is it called "Floppy Disk"?
I put a data in it, and it was a tiny store, then "Floppy Disk" comes to my mind.