Mastering React Hooks: useContext
React is a popular frontend library (or framework, depending on your perspective) and an in-demand skill if you want to advance your career as a Frontend Developer. Apart from understanding the core of React, one must have a good understanding of various React hooks.
React currently has 15 hooks, of which we mostly use two: useState
and useEffect
. However, there are other hooks that are extremely useful in a variety of situations.
One such hook, the useContext
hook, is one of the most important hooks to understand. The hook provides a native state management solution that can potentially replace any type of state management library.
Interesting, isn’t it?
Let’s get right into it and learn more about this amazing React hook 🚀
What is a Context?
Assume you’re in a room with some candies. There is now a rule that only people who are inside a specific room can eat candies from that room.
Now that you’re inside the room, you can enjoy those candies. A person who is outside your room or inside another room is not allowed to eat candies from your room.
Assume there is a room A that contains another room B within it. Both rooms are filled with candies. Varun and Pooja are the only two people present, where Varun is located in room A, and Pooja is located in room B (See the figure below).
Now, answer the following question – Can Varun eat candies from room B?
No, as the rule states, you cannot eat candies from a room that you are not in.
But, can Pooja eat candies from room A?
Yes, she can. It’s because room B is in room A. Therefore, Pooja is technically inside Room A, where she can eat the candies 🍬
But, what is a “Context”?
Contexts are nothing more than these rooms, and the candies represent the values that the context possesses. These values could be state variables, functions, or something else. Varun and Pooja are child components within the Context, so they (or the child components) have access to the values of their respective components.
Hmm, that was simple! Let’s go a step further and learn how to create and use contexts within our React apps.
Creating A Context In React
To create a new context, you have to use the React.createContext()
method. It creates and returns the new context object.
const Context = React.createContext();
Code language: JavaScript (javascript)
The new context includes a Provider
component, which wraps all code that requires information inside the context. The Provider
component accepts a value
prop containing the information to which we want to give access.
const ContextProvider = () => {
return (
<Context.Provider value={{ counter: 10 }}>
<Counter />
</Context.Provider>
);
};
Code language: JavaScript (javascript)
The amazing thing about context is that everything inside the Provider
component, including the child components, their children, and so on, has access to the variables contained within the value prop.
We can even create state variables, functions, and other types of objects within our ContextProvider component and pass them to the value prop.
const ContextProvider = () => {
const [counter, setCounter] = useState(0);
const increase = () => {
setCounter(counter + 1);
};
const decrease = () => {
setCounter(counter - 1);
};
return (
<Context.Provider value={{ counter, increase, decrease }}>
<Counter />
</Context.Provider>
);
};
Code language: JavaScript (javascript)
Now, let’s look at how to access these values within the context’s child component.
Accessing The Values Inside Components
Now comes the part you’ve been waiting for, i.e., the useContext
hook in action.
The useContext
hook accepts a context object (returned by the React.createContext
method) as an argument and returns the variables (inside the Provider
component’s value prop).
We can use variables within our components, and whenever these variables change, all components that use these variables will rerender to display the updated UI.
It’s as simple as that!
Let’s just use it to see things in action. In our previous example, we had a Counter
component as a child of the Provider
, which means we can access the value of the counter
state, as well as the increase
and decrease
functions, from the Counter
component. Here’s how it’s done:
// Context.jsx
import React, { useState } from 'react';
export const Context = React.createContext();
export const Counter = () => {
const { counter, increase, decrease } = useContext(Context);
return (
<div>
<h2>Counter: {counter}</h2>
<button onClick={increase}>Increase</button>
<button onClick={decrease}>Decrease</button>
</div>
);
};
export const ContextProvider = () => {
const [counter, setCounter] = useState(0);
const increase = () => {
setCounter(counter + 1);
};
const decrease = () => {
setCounter(counter - 1);
};
return (
<Context.Provider value={{ counter, increase, decrease }}>
<Counter />
</Context.Provider>
);
};
Code language: JavaScript (javascript)
// App.jsx
import React from 'react';
import { ContextProvider } from './Context.jsx';
function App() {
return <ContextProvider />;
}
export default App;
Code language: JavaScript (javascript)
Now, by clicking the “Increase” and “Decrease”, our counter state updates, and so does our Counter
component.
Not only the Counter
component but all components within the Provider
can access all the variables in a similar way.
Why use Context when I can just use the useState hook?
That’s a valid question because, in the end, we’re just using the useState
hook inside the context, so why not use it directly inside the main component and pass it down to the children?
There are two main reasons for not using the useState
hook:
- You must manually pass down all state variables and functions to each child, and then to their child, and so on. This can quickly become tedious; you must also pass down the states when creating a new component.
- It causes unnecessary rerenders when the state changes. When any of the state variables change, the parent component rerenders, which rerenders all of their children. This could result in a significant performance bottleneck.
Therefore, using the useState
hook directly on top of any component and passing it down to children is not a good idea. Context is only intended for such situations.
The Context Magic
As we just discussed, using a useState
hook on top of the parent component results in unwanted renders of the children, even if the child is not using the state.
But, how does context solves this problem?
Context solves this problem by rendering only the children who are accessing the context’s values. Consider the following example, which contains two child components within a context.
function App() {
return (
<ContextProvider>
<SiteHeader />
<Counter />
</ContextProvider>
);
}
Code language: JavaScript (javascript)
The context value is used by the child component Counter
, but not by the SiteHeader
component.
const SiteHeader = () => {
console.log('SiteHeader component rendered');
return (
<header>
<h1>This is the demo of Contexts and the useContext hook</h1>
</header>
);
};
const Counter = () => {
const { counter, increase, decrease } = useContext(Context);
console.log('Counter component rendered');
return (
<div>
<h2>Counter: {counter}</h2>
<button onClick={increase}>Increase</button>
<button onClick={decrease}>Decrease</button>
</div>
);
};
Code language: JavaScript (javascript)
Therefore, when we increment the counter, only the Counter
component renders, not the SiteHeader
component.
You can try it out using the Codedamn Playground embedded below:
Let’s go a little deeper into the component tree now. Let’s add another component CounterChild
inside the Counter
component and use the counter
state value there.
const CounterChild = () => {
const { counter, increase, decrease } = useContext(Context);
console.log('CounterChild component rerendered');
return (
<div>
<h3>Counter: {counter}</h3>
<button onClick={increase}>Increase</button>
<button onClick={decrease}>Decrease</button>
</div>
);
};
const Counter = () => {
console.log('Counter component rendered');
return (
<div>
<h2>This is a Counter</h2>
<CounterChild />
</div>
);
};
Code language: JavaScript (javascript)
Try increasing the counter to see what all components are rendering. You’ll notice that only the component that uses the context value, i.e., the CounterChild
, renders now.
You can try it out it using the Codedamn Playground embedded below:
As you can see, only the components that use the context values render when those values change. It is the CounterValue
component in this case.
So, this is the most important advantage of contexts that they prevent unnecessary re-rendering.
Applications of Contexts
Contexts are used to allow components to share a common state. As a result, it can be used for state management within React applications.
Let’s go over some of the scenarios where it comes in handy.
Authentication: Storing Details of LoggedIn User
It is very useful during authentication because it allows you to store the details of the currently logged-in user inside a context and wrap the entire application around that context.
// UserContext.jsx
import React, { useState } from 'react';
const UserContext = React.createContext();
const UserContextProvider = ({ children }) => {
const [user, setUser] = useState();
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
};
Code language: JavaScript (javascript)
// index.jsx
import React from 'react';
import ReactDOMClient from 'react-dom/client';
import { UserContextProvider } from './UserContext';
import App from './App';
const root = ReactDOMClient.createRoot(document.getElementById('root'));
root.render(
<UserContextProvider>
<App />
</UserContextProvider>
);
Code language: JavaScript (javascript)
Because the entire app is contained within the provider, you can access the user details from any component. It enables you to change the UI depending on whether the user is logged in or not.
For example, if there is a user, display the username; otherwise, display a login button in the navbar.
// Navbar.jsx
const Navbar = () => {
const { user } = useContext(UserContext);
return (
<nav>
{user ? <h1>{user.name}</h1> : <button>Login</button>}
</nav>
);
};
Code language: JavaScript (javascript)
Theming: Toggling Between Light/Dark Mode
Toggling between different theme modes requires you to keep track of the current theme mode. To update the UI, your entire application requires access to the theme variable. Context is the best option here.
Here is how you can implement it:
// ThemeContext.jsx
import React, { useState } from 'react';
const ThemeContext = React.createContext();
const ThemeContextProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
};
Code language: JavaScript (javascript)
// index.jsx
import React from 'react';
import ReactDOMClient from 'react-dom/client';
import { ThemeContextProvider } from './ThemeContext';
import App from './App';
const root = ReactDOMClient.createRoot(document.getElementById('root'));
root.render(
<ThemeContextProvider>
<App />
</ThemeContextProvider>
);
Code language: JavaScript (javascript)
You can wrap the entire application within a context so that you can access the theme state from anywhere and make necessary changes to the UI when it changes.
For example, you may modify the background and font color by checking whether the theme is light or dark.
// About.jsx
import React from 'react';
import { ThemeContext } from './ThemeContext';
export default function About() {
const { theme } = React.useContext(ThemeContext);
return (
<div
style={{
backgroundColor: theme === 'dark' ? 'black' : 'white',
}}
>
<p
style={{
color: theme === 'dark' ? 'white' : 'black',
}}
>
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Eos neque
reprehenderit tempore possimus. Unde quasi iure aliquid alias aspernatur
dolorem?
</p>
</div>
);
}
Code language: JavaScript (javascript)
Complete State Management Solution
It is not necessary to use heavy-weight state management libraries like Redux to manage your application state when using contexts.
You can create multiple contexts for each type of data and nest them based on the access level.
In an e-commerce application, for example, you’ll need authentication, product data, order data, etc.
You can now create three different contexts for each category, namely authentication, products, and orders.
Now, all components, including products (to show recommendations to the user) and order contexts (to retrieve the specific user’s orders), require user details. Therefore, it’s kept on top.
The order component requires product data to show which products you’ve previously purchased, on the products page. Therefore, the product context is kept next. At last, we place the order context.
You can now access context values based on access levels, which means that all child components have access to the user details, product list, and orders list. The order context can use values from the product context but not vice-versa. Similarly, both components can access user information but not vice versa.
We just build a complete state management solution using just contexts. You imagine how powerful these are and what amazing things you can do with them.
Summary
Hooks are very powerful additions to React. Having a solid understanding of different React hooks is very beneficial when building a React application.
useContext
is one of the most important hooks that everyone should be aware of. It offers a very powerful API for sharing states among different components.
It offers some amazing benefits such as having state access for any child component, avoiding unnecessary rerenders, not having to explicitly pass the state as props to any component, etc.
Contexts can be used for a variety of purposes, including authentication, switching between light and dark themes, and even as a complete state management solution.
If you’re still using prop drilling or bulky frameworks like Redux, it’s time to try contexts, and trust me, you’ll fall in love with it 😉
I’ll be back with more interesting React hooks in this special series – Master React Hooks, so stay tuned 🚀
This article hopefully provided you with some new information. Share it with your friends if you enjoy it. Also, please provide your feedback in the comments section.
Thank you so much for reading 😄
Sharing is caring
Did you like what Varun Tiwari wrote? Thank them for their work by sharing it on social media.
No comments so far
Curious about this topic? Continue your journey with these coding courses: