This library can be used without TypeScript, but all the sample code in here is written in TypeScript. And why not? It's 2023.
npm i unglitch@latest
v2.5.0
What unglitch is
unglitch was developed with its own vision before the author knew react-query. And that is a good thing because it allowed the mind to be uninfluenced by existing code. It was actually inspired by zustand with the goal to make it even simpler and smaller and being able to avoid side-effects in web applications using the Power of the new Store API in React v18+.
Core Principles
- Deeply nested states are bad and can always be flattened.
- Be small. Be fast. Be simple. Be predictable.
- Prevent unnecessary fetches (Lock'n'Release).
- You know your App, you know the Shape of your State
- Your store is your cache
- Do not blow up the memory
- Updating (foreign) data must be easy
Why isn't unglitch caching every request?
Read the Core Principles again.
Caching unnecessary data only to
throw it away later is a waste of memory.
Getting started
-
Step 1: Create a Store
your_app/utils/store.tsimport { create } from "unglitch"; type MyStore = { todos: string[]; }; const store = create({ todos: [] }); export const { useStore, useFetchData, getSnapshot, update } = store;
-
Step 2: Use the Store
your_app/SomeComponent.tsximport { useStore } from "./utils/store"; export default function Todos() { const [ todos ] = useStore( state => state.todos ); return ( <ul> {todos.map((todo) => ( <li>{todo}</li> ))} </ul> ); }
-
Updating the Store
your_app/SomeComponent.tsximport { update } from "./utils/store"; export default function TodoAdd() { return ( <Button onClick={() => update.merge(() => ({ todos: ['My new todo'] })) }>Add Todo</Button> ); }
-
Updating the Store the harder way
your_app/SomeComponent.tsxDoes the same as the merge sample above. In the given use-case you probably wouldn't want that. But it's good to know the difference.import { update } from "./utils/store"; export default function TodoAdd() { return ( <Button onClick={() => update((state) => ({ todos: [...state.todos, 'My new todo'] })) }>Add Todo</Button> ); }
Fetching Data
-
useFetchData Basic
your_app/AnyComponent.tsximport { useFetchData } from "./utils/store"; import fetchTodosFromApi from "./api-todos"; export default function Todos() { const { isFetching, data: todos } = useFetchData(async (update) => { return fetchTodosFromApi(); }, { onSuccess: (update, dataFromApi) => { // assuming dataFromApi is {status, error, data} update.merge(() => ({ todos: dataFromApi.data })); }, data: (state, isFetching) => state.todos, // the token makes sure that the data is only // fetched in one place with the same token token: 'fetch_todos' }); return ( <> {isFetching && <p>Loading fresh data...</p>} <ul> {todos.map((todo) => ( <li>{todo}</li> ))} </ul> </> ); }
Refreshing Data
-
useFetchData / refresh
your_app/AnyComponent.tsximport { useFetchData } from "./utils/store"; import fetchTodosFromApi from "./api-todos"; export default function Todos() { const { isFetching, data: todos, refresh } = useFetchData( /** ... */ ); return ( <> {isFetching && <p>Loading fresh data...</p>} <Button onClick={refresh}>Refresh Todos</Button> <p> You can DDOS-click this button. As long as a request is running no other request will be fired </p> <ul> {todos.map((todo) => ( <li>{todo}</li> ))} </ul> </> ); }
-
useFetchData / refreshInterval
your_app/AnyComponent.tsximport { useFetchData } from "./utils/store"; import fetchTodosFromApi from "./api-todos"; export default function Todos() { const { isFetching, data: todos, refresh } = useFetchData( /** ... */, { refreshInterval: 5000 // every 5 seconds } ); return ( <> {/** ... */} </> ); }
Controlling the Fetch
-
waitFor: Waiting for dependencies
your_app/AnyComponent.tsxwaitFor is a nice mechanism to remove all boilerplate around if-ing. Pass the must-have variables that are required to run the fetch and it will only fetch when all of them are available. The underlying mechanism iswaitForDeps.current.every(nonNullOrUndefined)
import { useFetchData } from "./utils/store"; import fetchUserCommentsFromApi from "./api-user"; export default function UserComments() { // useAuthUser represents a hook in this sample // which returns a user object when logged in, else null const user = useAuthUser(); const { isFetching, data: userComments, waitFor } = useFetchData( async (update, user) => { // update is the store update function // user is the user object from useAuthUser // you dont have to check if "user" is truthy because // waitFor will wait until the user object is truthy // if never: it will never execute return fetchUserCommentsFromApi(user.id); }, { waitFor: [user] } ); return ( <> {isFetching && <p>Loading your comments...</p>} {userComments.map((comment) => ( <p>{comment}</p> ))} </> ); }
-
allow: Finer control when to execute the fetch
your_app/AnyComponent.tsxallow
is a complementary mechanism towaitFor
. WhilewaitFor
very explicitly checks for nullish/undefinedallow
is a function that when returning false will prevent the fetch, no matter how it is triggered.This might be especially useful if you wanna e.g. prevent the "initial fetch" because you only want to fetch explicitly via the refresh function.
import { useFetchData } from "./utils/store"; export default function ConditionalFetch() { const user = useAuthUser(); const fetchAdminInfo = useFetchData( async (update, user) => { // ... your fetch }, { waitFor: [user], allow: (isInitialFetch: boolean) => user.isAdmin } ); // ... }
⚡️ Realtime Data / getSnapshot
There is only rare / specific cases where you need realtime data. So don't make this your go-to solution.
Data inside of a React component is never a realtime snapshot. It is always a snapshot of the current rendering cycle. That's how React works.
-
getSnapshot
your_app/utils/backup.tsThis function is probably the least you want to use within your React components. A use-case for this function is the connection to other services / libraries.import { getSnapshot } from "./utils/store"; export const backupTheStateForWhateverReason = () => { // we want to make backup every 10 seconds setInterval(() => { const snapshot = getSnapshot(); sendToServer(snapshot); // send the snapshot to your server // or save it in local storage or whatever // you can also use the snapshot to restore the state // so snapshot really is just a plain object // giving you the realtime state of your store }, 10000); }
-
getRealtime
your_app/utils/YourComponent.tsxAlso use this one carefully. If you use it too often you might simply be having an architectural problem. But it's more likely to need this thangetSnapshot
import { useStore } from "./utils/store"; /** * Assume initial data is: * todos = ['My first todo'] */ export default function Todos() { const [ todos, realtimeTodos ] = useStore( state => state.todos ); useEffect(() => { update.merge({ todos: ['My second todo'] }); }, []); useEffect(() => { // your function closure has a todo variable // with ONE entry ( ["My first todo"] ) // if, for whatever reason, you need the state // in realtime, so ignoring the current function closure / lifecycle // you can use const _todos = realtimeTodos(); // again: normally you don't need this. // because react will re-render with the new state }, []); return ( // ... ); }