In this series, instead of using a state-management library or proposing a one-size-fits-all solution, we start from the bare minimum and we build up our state management as we need it.
- In the first article we described how we load and display data with hooks.
- In the second article we learned how to change remote data with hooks.
- In the third article we learned how to share data between components with React Context, without using globals, singletons or resorting to state management libraries like MobX or Redux.
- In this fourth article we'll see how to share data between using SWR, which is probably what we should have done from the beginning.
In the previous articles we were storing the loaded data within React, in a useState
hook. But since then SWR was released (Oct 2019).
I first learned about SWR thanks to a video tutorial by Leigh Halliday: "React Data Fetching with Hooks using SWR", and I thought it was interesting enough that I could try it on a small internal project at work.
But a few weeks later a Twitter thread took me to this article; something clicked in my head and I realised I wasn't just looking for an excuse to try SWR.
No. I had been doing it wrong all along!
I was storing my remotely fetched data in useReducer
or useState
and manually mutating (or via a reducer), and then maybe reloading from the server in some cases, but not in others. And I was using React Context to make the data available to unrelated components in my app.
SWR makes this easier and better.
SWR stores the fetched data in a static cache. Therefore there's no need to use React Context to share the data with other components. And all components fetching the same data are updated when the data changes.
I refactored my SPA to use SWR and that resulted in a much simpler application logic. In addition, we now benefit from all the nice features that come with SWR such as "focus revalidation" and "refetch on interval".
Let's refactor our example from the previous three articles to use SWR.
Before SWR
Our demo app before SWR is what we got after our third article. (see repo)
Install SWR
yarn add swr
Refactoring our custom hook
In our demo app we have a custom useFetchedGames
hook that loads the games using the useAsyncFunction
hook, and then stores them using useState
to provida a way to locally mutate the data.
const useFetchedGames = () => {
const [fetchedGames, error, isPending] = useAsyncFunction(getGames, emptyList);
const [games, setGames] = React.useState(emptyList);
React.useEffect(() => {
setGames(fetchedGames);
}, [fetchedGames]);
return { games, setGames, error, isPending };
};
The problem with this approach is:
- The list of games is stored twice: first in a
useState
hook insideuseAsyncFunction
, and then in a newuseState
hook. - If the list of games is updated on the server, we never reload it. Here's where SWR shines.
We're going to refactor useFetchedGames
to use SWR instead of useState
.
const useFetchedGames = () => {
const { data, error, mutate } = useSWR('getGames', getGames);
const games = data || []
const isPending = !data
const setGames = mutate
return { games, setGames, error, isPending };
};
The full diff can be found in this git commit.
Note the "getGames"
string literal, just before the getGames
function. This is a key to help SWR identify our request. In our case it can be anything as long as it is unique for this resource (the list of games). There's a even simpler way. You can find it in the docs.
Removing React Context
Now that we're using useSWR
we don't need a React context, its provider, nor the useContext
hook.
In the demo project we make our components consume the useGames
hook directly, instead of the useGamesContext
one.
For example, GameGrid.tsx:
- import { useGamesContext } from '../GamesContext';
+ import { useGames } from '../useGames';
export const GameGrid = () => {
- const { games, error, isPending, markAsFinished } = useGamesContext();
+ const { games, error, isPending, markAsFinished } = useGames();
return (
<div className="gamegrid">
You can see the complete diff in this git commit.
Conclusion
With this small refactoring, our app has less code to maintain and we benefit from other great SWR features:
- Revalidate on focus.
- Revalidate on interval. (optional)
- Revalidate on reconnect.
- Retry on error.
I think Zeit's SWR (or a similar library) is a much better solution than storing fetched data in a React component using useState
or useReducer
.
I continue to store my application's UI state using custom hooks that use useReducer
and useState
but for remote data, I prefer to store it in a cache.
Please let me know what you think in the comments below.
Top comments (3)
Maybe a little too late, I'm currently working on a project and looking to implement this approach to retrieve data. however, I already implemented the Auth side of the application using custom hooks and a context, is it necessary to try and implement SWR for that too?
You can definitely have authentication handled separately.
You don't need to use SWR for everything. Your can start with 1 query.
I realise this is a two year old post. I have created an account specifically to say THANK YOU for this.
This is the first article I've been able to find with the method of abstracting and then referencing the useSWR call out to it's own file for repeatability across components.
I appreciate it's probably basic stuff but as a beginner this is that next level of complexity from the basics which I have not been able to find any info on.
THANK YOU!!