FloppyDisk - lightweight, simple, and powerful state management library
npm install floppy-diskA lightweight, simple, and powerful state management library.
This library was highly-inspired by Zustand and TanStack-Query.
Both are awesome state manager. That's why this Floppy Disk library behaves like them, but with small DX improvement, more power, and less bundle size.
Bundle Size Comparison:
``js
import { create } from 'zustand'; // 3.3 kB (gzipped: 1.5 kB)
import { createStore } from 'floppy-disk'; // 1.4 kB (gzipped: 750 B) 🎉
import {
QueryClient,
QueryClientProvider,
useQuery,
useInfiniteQuery,
useMutation,
} from '@tanstack/react-query'; // 31.7 kB kB (gzipped: 9.2 kB)
import { createQuery, createMutation } from 'floppy-disk'; // 9.7 kB (gzipped: 3.3 kB) 🎉
`
- Using Zustand & React-Query: https://demo-zustand-react-query.vercel.app/
👉 Total: 309.21 kB
- Using Floppy Disk: https://demo-floppy-disk.vercel.app/
👉 Total: 272.63 kB 🎉
- Create Store
- Get/set store inside/outside component
- Very simple way to customize the reactivity (state update subscription)
- Support middleware
- Set state interception
- Store event (onSubscribe, onUnsubscribe, etc.)
- Use store as local state manager
- Create Stores
- Same as store, but controlled with a store key
- Create Query & Mutation
- Backend agnostic (support GraphQL & any async function)
- TypeScript ready
- SSR/SSG support
- Custom reactivity (we choose when to re-render)
- Create query
- Dedupe multiple request
- Auto-fetch on mount or manual (lazy query)
- Enable/disable query
- Serve stale data while revalidating
- Retry on error (customizable)
- Optimistic update
- Invalidate query
- Reset query
- Query with param (query key)
- Paginated/infinite query
- Prefetch query
- Fetch from inside/outside component
- Get query state inside/outside component
- Suspense mode
- Create mutation
- Mutate from inside/outside component
- Get mutation state inside/outside component
- ... and a lot more
---
View official documentation on floppy-disk.vercel.app
---
- Key Features
- Table of Contents
- Store
- Basic Concept
- Advanced Concept
- Stores
- Query \& Mutation
- Query State \& Network Fetching State
- Inherited from createStores
- Single Query
- Single Query with Params
- Paginated Query or Infinite Query
- Mutation
- Important Notes
Create a store.
`js
import { createStore } from 'floppy-disk';
const useCatStore = createStore(({ set }) => ({
age: 0,
isSleeping: false,
increaseAge: () => set((state) => ({ age: state.age + 1 })),
reset: () => set({ age: 0, isSleeping: false }),
}));
`
Use the hook anywhere, no providers are needed.
`jsx
function Cat() {
const age = useCatStore('age');
return Cat's age: {age};
}
function Control() {
const increaseAge = useCatStore('increaseAge');
return ;
}
`
> Example: https://codesandbox.io/.../examples/react/basic
Control the reactivity. The concept is same as useEffect dependency array.
`jsx
function YourComponent() {
const { age, isSleeping } = useCatStore();
// Will re-render every state change ^
}
function YourComponent() {
const { age, isSleeping } = useCatStore((state) => [state.isSleeping]);
// Will only re-render when isSleeping is updated ^
// Update on age won't cause re-render this component
}
function YourComponent() {
const { age, isSleeping } = useCatStore((state) => [state.age, state.isSleeping]);
// Will re-render when age or isSleeping is updated ^
}
function YourComponent() {
const { age, isSleeping } = useCatStore((state) => [state.age > 3]);
// Will only re-render when (age>3) is updated
}
`
Even simpler way, after version 2.13.0, we can use store's object key:
`jsx
function YourComponent() {
const age = useCatStore('age');
// Will only re-render when age is updated
}
function YourComponent() {
const age = useCatStore('isSleeping');
// Will only re-render when isSleeping is updated
}
`
> Example: https://codesandbox.io/.../examples/react/custom-reactivity
Reading/writing state and reacting to changes outside of components.
`js
const alertCatAge = () => {
alert(useCatStore.get().age);
};
const toggleIsSleeping = () => {
useCatStore.set((state) => ({ isSleeping: !state.isSleeping }));
};
const unsub = useCatStore.subscribe(
// Action
(state) => {
console.log('The value of age is changed!', state.age);
},
// Reactivity dependency (just like useEffect dependency mentioned above)
(state) => [state.age],
// ^If not set, the action will be triggered on every state change
);
`
Set the state silently (without broadcast the state change to any subscribers).
`jsx`
const decreaseAgeSilently = () => {
useCatStore.set((state) => ({ age: state.age }), true);
// ^silent param
};
// 👇 Will not re-render
function Cat() {
const age = useCatStore('age');
return Cat's age: {age};
}
Store events & interception.
`js`
const useCatStore = createStore(
({ set }) => ({
age: 0,
isSleeping: false,
increaseAge: () => set((state) => ({ age: state.age + 1 })),
reset: () => set({ age: 0, isSleeping: false }),
}),
{
onFirstSubscribe: (state) => {
console.log('onFirstSubscribe', state);
},
onSubscribe: (state) => {
console.log('onSubscribe', state);
},
onUnsubscribe: (state) => {
console.log('onUnsubscribe', state);
},
onLastUnsubscribe: (state) => {
console.log('onLastUnsubscribe', state);
},
intercept: (nextState, prevState) => {
if (nextState.age !== prevState.age) {
return { ...nextState, isSleeping: false };
}
return nextState;
},
},
);
> Example:
> https://codesandbox.io/.../examples/react/store-event
> https://codesandbox.io/.../examples/react/intercept
Let's go wild using IIFE.
`js`
const useCatStore = createStore(
({ set }) => ({
age: 0,
isSleeping: false,
increaseAge: () => set((state) => ({ age: state.age + 1 })),
reset: () => set({ age: 0, isSleeping: false }),
}),
(() => {
const validateCat = () => {
console.info('Window focus event triggered...');
const { age } = useCatStore.get();
if (age > 5) useCatStore.set({ age: 1 });
};
return {
onFirstSubscribe: () => window.addEventListener('focus', validateCat),
onLastUnsubscribe: () => window.removeEventListener('focus', validateCat),
};
})(),
);
Prevent re-render using Watch.
`jsx
function CatPage() {
const age = useCatStore('age');
// If age changed, this component will re-render which will cause
// HeavyComponent1 & HeavyComponent2 to be re-rendered as well.
return (
Cat's age: {age}
);
}
// Optimized
function CatPageOptimized() {
return (
render={(age) => {
return Cat's age: {age};
}}
/>
);
}
`
> Example: https://codesandbox.io/.../examples/react/watch-component
Want a local state instead of global state?
Or, want to set the initial state inside component?
`jsx
const [CatStoreProvider, useCatStoreContext] = withContext(() =>
createStore(({ set }) => ({
age: 0,
isSleeping: false,
increaseAge: () => set((state) => ({ age: state.age + 1 })),
reset: () => set({ age: 0, isSleeping: false }),
})),
);
function Parent() {
return (
<>
>
);
}
function CatAge() {
const { age } = useCatStoreContext()((state) => [state.age]);
// Shorthand after v1.13.0:
// const age = useCatStoreContext()('age');
return
function CatIsSleeping() {
const useCatStore = useCatStoreContext();
const { isSleeping } = useCatStore((state) => [state.isSleeping]);
// Shorthand after v1.13.0:
// const isSleeping = useCatStore('isSleeping');
return (
<>
> Example: https://codesandbox.io/.../examples/react/local-state
Set default reactivity.
`jsx
const useCatStore = createStore(
({ set }) => ({
age: 0,
isSleeping: false,
increaseAge: () => set((state) => ({ age: state.age + 1 })),
reset: () => set({ age: 0, isSleeping: false }),
}),
{
defaultDeps: (state) => [state.age], // 👈
},
);function Cat() {
const { age } = useCatStore();
// ^will only re-render when age changed
return
Cat's age: {age};
}
`Stores
The concept is same as store, but this can be used for multiple stores.
You need to specify the store key (an object) as identifier.
`js
import { createStores } from 'floppy-disk';const useCatStores = createStores(
({ set, get, key }) => ({
// ^store key
age: 0,
isSleeping: false,
increaseAge: () => set((state) => ({ age: state.age + 1 })),
reset: () => set({ age: 0, isSleeping: false }),
}),
{
onBeforeChangeKey: (nextKey, prevKey) => {
console.log('Store key changed', nextKey, prevKey);
},
// ... same as createStore
},
);
function CatPage() {
const [catId, setCatId] = useState(1);
return (
<>
Current cat id: {catId}
>
);
}
function Cat({ catId }) {
const { age } = useCatStores({ catId }, (state) => [state.age]);
return
Cat's age: {age};
}function Control({ catId }) {
const { increaseAge } = useCatStores({ catId }, (state) => [state.increaseAge]);
return ;
}
`> Example: https://codesandbox.io/.../examples/react/stores
— ✨ 💾 ✨ —
Query & Mutation
With the power of
createStores function and a bit creativity, we can easily create a hook just like useQuery and useInfiniteQuery in React-Query using createQuery function.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.$3
There are 2 types of state: query (data) state & network fetching state.
status, isLoading, isSuccess, isError is a query data state.
It has no relation with network fetching state. ⚠️
Here is the flow of the query data state:- Initial state when there is no data fetched.
{ status: 'loading', isLoading: true, isSuccess: false, isError: false }
- After data fetching:
- If success
{ status: 'success', isLoading: false, isSuccess: true, isError: false }
- If error
{ status: 'error', isLoading: false, isSuccess: false, isError: true }
- After data fetched successfully, you will always get this state:
{ status: 'success', isLoading: false, isSuccess: true, isError: false }
- If a refetch is fired and got error, the state would be:
{ status: 'success', isLoading: false, isSuccess: true, isError: false, isRefetchError: true }
The previouse success response will be kept.For network fetching state, we use
isWaiting.
The value will be true if the query is called and still waiting for the response.$3
The
createQuery function inherits functionality from the createStores function, allowing us to perform the same result and actions available in createStores.`tsx
const useMyQuery = createQuery(myQueryFn, {
// 👇 Same as createStores options
defaultDeps: undefined,
onFirstSubscribe: (state) => console.log('onFirstSubscribe', state),
onSubscribe: (state) => console.log('onSubscribe', state),
onUnsubscribe: (state) => console.log('onUnsubscribe', state),
onLastUnsubscribe: (state) => console.log('onLastUnsubscribe', state),
onBeforeChangeKey: (nextKey, prevKey) => console.log('Store key changed', nextKey, prevKey), // ... other createQuery options
});
`Custom reactivity (dependency array) also works:
`tsx
function QueryLoader() {
// This component doesn't care whether the query is success or error.
// It just listening to network fetching state. 👇
const { isWaiting } = useMyQuery((state) => [state.isWaiting]);
return Is network fetching? {String(isWaiting)};
}
`$3
`jsx
const useGitHubQuery = createQuery(async () => {
const res = await fetch('https://api.github.com/repos/afiiif/floppy-disk');
if (res.ok) return res.json();
throw res;
});function SingleQuery() {
const { isLoading, data } = useGitHubQuery();
if (isLoading) return
Loading...; return (
{data.name}
{data.description}
⭐️ {data.stargazers_count}
🍴 {data.forks_count}
);
}
`> Example: https://codesandbox.io/.../examples/react/query
Actions:
Normally, we don't need reactivity for the actions.
Therefore, using
get method will be better, since it will not re-render the component when a query state changed.`jsx
function Actions() {
const { fetch, forceFetch, reset } = useGitHubQuery.get(); // Or like this:
// const { isLoading, data, error, fetch, forceFetch, reset } = useGitHubQuery();
return (
<>
>
);
}
`Options:
`jsx
const useGitHubQuery = createQuery(
async () => {
const res = await fetch('https://api.github.com/repos/afiiif/floppy-disk');
if (res.ok) return res.json();
throw res;
},
{
fetchOnMount: false,
enabled: () => !!useUserQuery.get().data?.user,
select: (response) => response.name,
staleTime: Infinity, // Never stale
retry: 0, // No retry
onSuccess: (response) => {},
onError: (error) => {},
onSettled: () => {},
},
);function MyComponent() {
const { data, response } = useGitHubQuery();
/**
* Since in option we select the data like this:
* select: (response) => response.name
*
* The return will be:
* {
* response: { id: 677863376, name: "floppy-disk", ... },
* data: "floppy-disk",
* ...
* }
*/
}
`Get data or do something outside component:
`jsx
const getData = () => console.log(useGitHubQuery.get().data);
const resetQuery = () => useGitHubQuery.get().reset();// Works just like createStores
useMyQuery.get(/ ... /);
useMyQuery.set(/ ... /);
useMyQuery.subscribe(/ ... /);
useMyQuery.getSubscribers(/ ... /);
`$3
`jsx
const usePokemonQuery = createQuery(async ({ pokemon }) => {
const res = await fetch(https://pokeapi.co/api/v2/pokemon/${pokemon});
if (res.ok) return res.json();
throw res;
});function PokemonPage() {
const [currentPokemon, setCurrentPokemon] = useState();
const { isLoading, data } = usePokemonQuery({ pokemon: currentPokemon });
if (isLoading) return
Loading...; return (
{data.name}
Weight: {data.weight}
);
}
`> Example: https://codesandbox.io/.../examples/react/query-with-param
Get data or do something outside component:
`jsx
const getDitto = () => {
console.log(usePokemonQuery.get({ pokemon: 'ditto' }).data);
};const resetDitto = () => {
usePokemonQuery.get({ pokemon: 'ditto' }).reset();
};
function Actions() {
return (
<>
>
);
}
`$3
`jsx
const usePokemonsInfQuery = createQuery(
async (_, { pageParam = 0 }) => {
const res = await fetch(https://pokeapi.co/api/v2/pokemon?limit=10&offset=${pageParam});
if (res.ok) return res.json();
throw res;
},
{
select: (response, { data = [] }) => [...data, ...response.results],
getNextPageParam: (lastPageResponse, i) => {
if (i > 5) return undefined; // Return undefined means you have reached the end of the pages
return i * 10;
},
},
);function PokemonListPage() {
const { data = [], fetchNextPage, hasNextPage, isWaitingNextPage } = usePokemonsInfQuery();
return (
{data.map((pokemon) => (
{pokemon.name}
))}
{isWaitingNextPage ? (
Loading more...
) : (
hasNextPage &&
)}
);
}
`> Example: https://codesandbox.io/.../examples/react/infinite-query
Note:
- The default stale time is 3 seconds.
- The default error retry attempt is 1 time, and retry delay is 2 seconds.
- The default reactivity of a query is:
(s) => [s.data, s.error, s.isWaitingNextPage, s.hasNextPage]
- Note that by default, subscribers don't listen to isWaiting state.
- You can change the defaultDeps on createQuery options.$3
`jsx
const useLoginMutation = createMutation(
async (variables) => {
const res = await axios.post('/auth/login', {
email: variables.email,
password: variables.password,
});
return res.data;
},
{
onSuccess: (response, variables) => {
console.log(Logged in as ${variables.email});
console.log(Access token: ${response.data.accessToken});
},
},
);function Login() {
const { mutate, isWaiting } = useLoginMutation();
const showToast = useToast();
return (
disabled={isWaiting}
onClick={() => {
mutate({ email: 'foo@bar.baz', password: 's3cREt' }).then(({ response, error }) => {
if (error) {
showToast('Login failed');
} else {
showToast('Login success');
}
});
}}
>
Login
);
}
`Optimistic update:
`jsx
function SaveProduct() {
const { mutate, isWaiting } = useEditProductMutation();
const { getValues } = useFormContext(); return (
disabled={isWaiting}
onClick={() => {
const payload = getValues();
const { revert, invalidate } = useProductQuery.optimisticUpdate({
key: { id: payload.id },
response: payload,
});
mutate(payload).then(({ response, error }) => {
if (error) {
revert();
}
invalidate();
});
}}
>
Save
);
}
`> Example: https://codesandbox.io/.../examples/react/mutation
— ✨ 💾 ✨ —
Important Notes
Don't mutate. (unless you use Immer JS library or something similar)
`js
import { createStore } from 'floppy-disk';const useCartStore = createStore(({ set, get }) => ({
products: [],
addProduct: (newProduct) => {
const currentProducts = get().products;
product.push(newProduct); // ❌ Don't mutate
set({ product });
},
}));
`Don't use conditional reactivity selector.
`jsx
function Cat({ isSomething }) {
const value = useCatStore(isSomething ? 'age' : 'isSleeping'); // ❌
const { age } = useCatStore(isSomething ? (state) => [state.age] : null); // ❌
const { age } = useCatStore((state) => (isSomething ? [state.age] : [state.isSleeping])); // ❌
return Cat's age: {age};
}
`No need to memoize the reactivity selector.
`jsx
function Cat() {
const selectAge = useCallback((state) => [state.age], []); // ❌
const { age } = useCatStore(selectAge);
return Cat's age: {age};
}
`No need to memoize the store key / query key.
`jsx
function PokemonsPage() {
const queryKey = useMemo(() => ({ generation: 'ii', sort: 'asc' }), []); // ❌
const { isLoading, data } = usePokemonsQuery(queryKey);
return ...;
}
``---
View official documentation on floppy-disk.vercel.app