React Query as a persistent state manager

I'm a full-stack developer from South Africa 🇿🇦. I love writing about JavaScript, HTML and CSS.
Search for a command to run...

I'm a full-stack developer from South Africa 🇿🇦. I love writing about JavaScript, HTML and CSS.
Most of you know me for my consistency, a golden arrow in my blog series. I've written 1000 articles in 1008 days! Almost an article a day, and my honeymoon was the only holiday I ever took. I'm super proud of this achievement; it has been a fantasti...

It's not the first time I'll be talking about community. I think it's an essential aspect of any successful tool. This shows in my previous explorations of Astro, Medusa, and now Vendure as well. All these products thrive in a super open, welcoming, ...

The cool part about Vendure is how easy it is to set up and how abstract each layer is. Basically, we get the following elements: External database Server Worker Admin UI Frontend While this is amazing, it also brings a bit of complexity when it co...

The previous article looked at customizing Vendure on a data and process level. In this article, we'll look at customizing emails, as they are often a big part of a webshop system. We'll be looking at two different layers of customization for customi...

Even though Vendure is a pretty significant project out of the box, in some cases, we might want to go in and modify some elements to work to our specific use case. In this article, I'll take a high-level look at some elements we can customize within...

I had to leverage a kind of context-based state a while ago; however, the element needing context was so small that it seemed overkill to make a full context for this.
And that's when I started building this context of a small reusable hook that does just that.
To demonstrate the difference and difficulties with managing a persistent shareable state, I will also demo another option and work our way up to modify it by leveraging React Query.
Below you can see a short video demo to showcase the downsides of the persistent state hook compared to the React Query hook.

Let's start by creating a persistent state hook in React. This will be a hook that we can use to read and write from a specified storage module. I'll use local storage in this example, but you can change this to any storage.
The hook should be able to retrieve the data set in the storage module and return that. In return, it should be able to persist a new value in the storage, and the module should return this.
Let's create a file called usePersistentState.
The hook will look like this:
import {useState, useEffect} from 'react';
export default function usePersistentState(key) {
const [value, setValue] = useState(null);
const setValueAndPersist = (newValue) => {
if (newValue !== value) {
setValue(newValue);
return localStorage.setItem(key, newValue);
}
return value;
};
useEffect(() => {
const item = localStorage.getItem(key);
if (item) {
setValue(item);
}
}, []);
return [value, setValueAndPersist];
}
We leverage a react useState hook to keep track of the value.
And we use the useEffect hook to run once it mounts by using the [] property.
By using
[]will only run on load.
To use this hook we can do something like this:
function SetState() {
const [value, setValue] = usePersistentState('item_state');
return (
<button onClick={() => setValue(value === 'on' ? 'off' : 'on')}>
Click me {value}
</button>
);
}
And this will work perfectly.
Until... We need to introduce another component that also needs to read this value separately.
Since we used useState it does not update across our application, and it will cause really weird side-effects.
You might have remembered that React Query does not have to work with API calls. It can keep track of any variable.
And in our case, we want it to keep track of our storage object.
So let's also create a usePeristentContext hook.
This will be our hook that uses React Query to keep track of our state.
import {useMutation, useQuery, useQueryClient} from 'react-query';
export default function usePersistentContext(key) {
const queryClient = useQueryClient();
const {data} = useQuery(key, () => localStorage.getItem(key));
const {mutateAsync: setValue} = useMutation(
(value) => localStorage.setItem(key, value),
{
onMutate: (mutatedData) => {
const current = data;
queryClient.setQueryData(key, mutatedData);
return current;
},
onError: (_, __, rollback) => {
queryClient.setQueryData(key, rollback);
},
}
);
return [data, setValue];
}
You can see that we define the query to read from the localStorage. This will be able to set our initial value if it exists.
Then we use a React Query mutation as our set value. This can update our storage and, in the meantime, mutate the query data so it will reflect application-wide!
We can use this hook in the following fashion:
function SetContext() {
const [value, setValue] = usePersistentContext('item_context');
return (
<button onClick={() => setValue(value === 'on' ? 'off' : 'on')}>
Click me {value}
</button>
);
}
The benefit of this method is that another component can read it simultaneously, and the updated value will be read!
Viva la React Query!
You can try both methods on this Code Sandbox.
Thank you for reading my blog. Feel free to subscribe to my email newsletter and connect on Facebook or Twitter