Simple way to manage state in React with Context
August 02, 2020
What weâll learn
We will learn how to use Reactâs context API to manage state. Also, weâll see
how to use useSWR
hook from swr to manage async data from an API.
Our Requirements
- Data can come from synchronous or asynchronous calls. An API endpoint or a
simple
setState
. - Allow to update state data from the components that use it.
- No extra steps like actions, thunks.
Small introduction to swr
SWR (stale-while-revalidate)
is a caching strategy where data is returned from
a cache immediately and send fetch request to server. Finally, when the server
response is available, get the new data with changes from the server as well as
updating the cache.
Here we are talking about the swr library from
vercel. It provides a hook useSWR
which we will use to fetch
data from GitHub API.
Head over to swr
âs docs to learn more. The API is small and easy.
Store
We need a top-level component where will maintain this global state. Letâs call
this component GlobalStateComponent
. If youâve used Redux
, this can be your
store.
Weâll test with 2 types of data for better a understanding.
- Users data coming from an API like GitHub which might not change pretty quickly.
- A simple counter which increments count by 1 every second.
// global-store.jsx
const GlobalStateContext = React.createContext({
users: [],
count: 0,
});
export function GlobalStateProvider(props) {
// we'll update here
return <GlobalStateContext.Provider value={value} {...props} />;
}
// a hook which we are going to use whenever we need data from `GlobalStateProvider`
export function useGlobalState() {
const context = React.useContext(GlobalStateContext);
if (!context) {
throw new Error("You need to wrap GlobalStateProvider.");
}
return context;
}
Now we need to use useSWR
hook to fetch users data. Basic API for useSWR
looks like this.
const { data, error, mutate } = useSWR("url", fetcher, [options]);
// url - an API endpoint url.
// fetcher - a function which takes the dependencies of first argument as parameters
// and returns a promise.
// options - Options for the hook. Configuration for this hook.
// data - response from the API request
// error - Error response from fetcher will be caught here.
// mutate - Update the cache and get new data from server.
We will use browserâs built-in fetch API. You can use Axios or any other library you prefer.
const fetcher = (url) => fetch(url).then((res) => res.json());
With this, our complete useSWR
hook looks like this.
const { data, error, mutate } = useSWR(`https://api.github.com/users`, fetcher);
And, we need a setState
with count and a setInterval
which updates the count
every second.
...
const [count, setCount] = React.useState(0);
const interval = React.useRef();
React.useEffect(() => {
interval.current = setInterval(() => {
setCount(count => count + 1);
}, 1000);
return () => {
interval.current && clearInterval(interval.current);
}
}, []);
...
A context provider takes a value
prop for the data. Our value will be both
user
related data and count
.
If we put all these little things together in a global-store.jsx
file, it
looks like this.
// global-store.jsx
const GlobalStateContext = React.createContext({
users: [],
mutateUsers: () => {},
error: null,
count: 0,
});
export function GlobalStateProvider(props) {
const { data: users, error, mutate: mutateUsers } = useSWR(
`https://api.github.com/users`,
fetcher
);
const [count, setCount] = React.useState(0);
const interval = React.useRef();
React.useEffect(() => {
interval.current = setInterval(() => {
setCount((count) => count + 1);
}, 1000);
return () => {
interval.current && clearInterval(interval.current);
};
}, []);
const value = React.useMemo(() => ({ users, error, mutateUsers, count }), [
users,
error,
mutateUsers,
count,
]);
return <GlobalStateContext.Provider value={value} {...props} />;
}
// a hook to use whenever we need to consume data from `GlobalStateProvider`.
// So, We don't need React.useContext everywhere we need data from GlobalStateContext.
export function useGlobalState() {
const context = React.useContext(GlobalStateContext);
if (!context) {
throw new Error("You need to wrap GlobalStateProvider.");
}
return context;
}
How to use it
Wrap your top-level component with GlobalStateProvider
.
// app.jsx
export default function App() {
return <GlobalStateProvider>//...</GlobalStateProvider>;
}
Letâs have two components, one consumes users data and another one needs counter.
We can use useGlobalState
hook we created in both of them to get users
and
count
.
// users.jsx
export default function Users() {
const { users, error } = useGlobalState();
if (!users && !error) {
return <div>Loading...</div>;
}
return <ul>...use `users` here</ul>;
}
// counter.jsx
export default function Counter() {
const { count } = useGlobalState();
return <div>Count: {count}</div>;
}
// app.jsx
export default function App() {
return (
<GlobalStateProvider>
<Counter />
<Users />
</GlobalStateProvider>
);
}
Thatâs it. Now youâll see both Counter and Users.
The codesandox link: codesandbox
But, Wait
If you put a console.log
in both Users
and Counter
components, youâll see
even if only count
updated, Users
component also renders.
The fix is simple. Extract users
in a component between App
and Users
, and
pass users
as a prop to Users
component, and wrap Users
in React.memo
// app.jsx
export default function App() {
return (
<GlobalStateProvider>
<Counter />
- <Users />
+ <UserWrapper />
</GlobalStateProvider>
)
}
// user-wrapper.jsx
export default function UserWrapper() {
const { users, error } = useGlobalState();
return <Users users={users} error={error} />;
}
// users.jsx
- export default function Users() {
+ const Users = React.memo(function Users({users, error}) {
- const {users, error} = useGlobalState();
if (!users && !error) {
return <div>Loading...</div>;
}
return (
<ul>
...use users here
</ul>
)
});
export default Users;
Now check the console.log
again. You should see only Counter
component
rendered.
The finished codesandbox link: codesandbox
How to force-update users
Our second requirement was to update the state from any component.
In the same above code, if we pass setCounter
and mutateUsers
in the context
providerâs value
prop, you can use those functions to update the state.
setCounter
will update the counter and mutateUsers
will resend the API
request and returns new data.
You can use this method to maintain any synchronous, asynchronous data without third-party state management libraries.
Closing Notes
- Consider using
useReducer
instead ofuseState
if you end up with too manysetState
s in global state. A good use case will be if you are storing a large object instead of a single value likecount
above. Splitting up that object in multiplesetState
means any change in each of them will re-render all the components using your context provider. Itâll get annyoing to keep track and bring inReact.memo
for every little thing. - react-query is another solid library as an alternative to
swr
. - Redux is still doing great for state management. The new redux-toolkit amazingly simplifies Redux usage. Check it out.
- Have an eye on recoil, A new state management library with easy sync, async state support. I didnât use it on a project yet. Iâll definitely try it soon.
Thank you and have a great day. đ đ