React: ContextAPI as a State solution? [ UPDATED ]
Dewald Els
Posted on May 21, 2021
Updated from Previous Article
Article No Longer Available
⛔️ Problems with Previous Approach
Although the approach taken in the previous article seemed to work fine, the most severe problem was that any component that used the AppContext would re-render. Even if it was using an unrelated state object from the Context. Therefore, I set out to fix this.
✅ Solution
I've updated to solution to use multiple contexts, one for each part of the state. I then created a AppContext which brought together all the Contexts and wrapped that around my my application.
🧑💻 The code
You can get a copy of the code on Github, I've created a new branch which you can find here:
Creating separate Contexts
The first order of business is to create a new Context for each part of my state.
You will see in each of the code snippets that there are two main parts.
- The Provider Component: The Context Provider is used as a Higher Order Component and provides the state value and setter as an Object to the value. This allows the developer to desctructure only the state or setter in a Component.
- Custom Hook: to access the Context's state The custom hook allows easy access to the state and avoids the import of both useContext and MoviesContext in any component that wishes to use the movies state.
The Movies Context
import {createContext, useContext, useState} from "react";
const MoviesContext = createContext([]);
export const useMovies = () => {
return useContext(MoviesContext);
}
export const MoviesProvider = ({children}) => {
const [movies, setMovies] = useState([]);
return (
<MoviesContext.Provider value={{movies, setMovies}}>
{children}
</MoviesContext.Provider>
)
}
The Profile Context
import {createContext, useContext, useState} from "react";
const ProfileContext = createContext(null);
export const useProfile = () => {
return useContext(ProfileContext);
}
export const ProfileProvider = ({children}) => {
const [profile, setProfile] = useState(null);
return (
<ProfileContext.Provider value={{profile, setProfile}}>
{children}
</ProfileContext.Provider>
)
}
The UiLoading Context
import {createContext, useContext, useState} from "react";
const UiLoadingContext = createContext(false);
export const useUiLoading = () => {
return useContext(UiLoadingContext);
}
export const UiLoadingProvider = ({children}) => {
const [uiLoading, setUiLoading] = useState(false);
return (
<UiLoadingContext.Provider value={{uiLoading, setUiLoading}}>
{children}
</UiLoadingContext.Provider>
)
}
The new AppContext
Given that I now have three separate contexts, rather than bloating the index.js
file with multiple providers, I decided to create a AppContext component to group all the Providers together.
As far as I can tell, the order here does not make a difference. Feel free to correct this in the comments, and I will update the article.
import {ProfileProvider} from "./ProfileContext";
import {MoviesProvider} from "./MoviesContext";
import {UiLoadingProvider} from "./UiLoadingContext";
export const AppProvider = ({children}) => {
return (
<ProfileProvider>
<MoviesProvider>
<UiLoadingProvider>
{children}
</UiLoadingProvider>
</MoviesProvider>
</ProfileProvider>
)
}
Using the Context's State
Thanks to the custom hook in each context, it is terrifically easy to gain access to both the state value and/or setter.
If you would want to update the profile, and ONLY have access to the setter, you can write the following code:
const Login = () => {
console.log('Login.render()')
const {setProfile} = useProfile();
const onLoginClick = () => {
setProfile({username: 'birdperson'});
}
... // Login.js
The big "Win" here, is that ONLY components using the profile context will now re-render. This is a stark contrast to the previous article's approach where all components using the AppContext would re-render, even if it wasn't accessing the profile state.
If you need to access both the state and the setter, you can use the custom hook again like this:
...
const {movies, setMovies} = useMovies();
And again, only components using the MoviesContext would re-render when the setMovies setter is invoked, leaving other components untouched.
Conclusion
Using Context is a great way to share state in small applications, but comes with some "Gotchas" if you're not 100% clear on how the ContextAPI works. This is been a great learning experience and thanks again for the messages pointing out the improvements to be made.
Posted on May 21, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.