Hey there! React is pretty great with a ton of stuff. But sometimes we need global states, such as a UI theme or locale preferences. Now ordinarily, to pass states down to child components what we do is pass down props. But with global states, we have to pass down props several times down the component tree or the roots of a potato if you've seen Women Of React 2020. This creates a cumbersome phenomenon known as "prop drilling". This means that we are passing down the props from grandparent to parent to child and so on.
Now to solve this issue, you can use something like Redux, which is a completely fine solution, but restructures your entire code and necessitates a ton of boilerplate code. This makes it unsuitable for lightweight implementations. Keep in mind though that it doesn't affect performance.
So what do we do?
Enter React Context API.
Context provides a way to pass data through the component tree without having to pass props down manually at every level.
That's the official React docs intro. It was introduced in React 16.3. It solves the global state management problem. Context is often touted as a lightweight alternative to Redux and provides much cleaner and simpler code. So let's get started with it!
So let's make a simple React app. Use create-react-app
to generate one. And write the following in App.js
function App() {
return (
<div className="App">
<AppBar theme="white" />
</div>
);
}
function AppBar({theme}) {
return(
<div className="AppBar">
<ThemedButton theme={theme}/>
</div>
);
}
function ThemedButton({theme}) {
return(
<div>
<button style={{backgroundColor: theme}} />
</div>
)
}
export default App;
Well, as you can see above, we have to thread the theme property through all the components, so that we can apply it to child elements. This is great for three components maybe, but imagine a full dynamic website, where the component tree might be huge and deep.
Let's try the same thing with React Context then. Now before you use Context you should keep in mind that this isn't meant for small number of props for a small number of components. For that, simple prop threading and component composition would be much simpler. So use it wisely.
const ThemeContext = React.createContext('white');
function App() {
return (
<div className="App">
<ThemeContext.Provider value={"black"}>
<AppBar />
</ThemeContext.Provider>
</div>
);
}
function AppBar() {
return(
<div className="AppBar">
<ThemedButton />
</div>
);
}
function ThemedButton() {
return(
<div>
<ThemeContext.Consumer>
{value => <button style={{backgroundColor: value}} />}
</ThemeContext.Consumer>
</div>
)
}
export default App;
Okay, that's a lot of new code and if you look closely you'll see that our props in the AppBar
and ThemedButton
component have disappeared. So what happened? Let's break it all down .
So notice at the top of the code snippet I have the line,
const ThemeContext = React.createContext('white');
This is what creates the React Context object. Now, every Context object comes with a Provider and a consumer. Again if you refer to the above code you'll see them.
Provider and Consumer
//Provider
<ThemeContext.Provider value={"black"}>
</ThemeContext.Provider>
//Consumer
<ThemeContext.Consumer>
</ThemeContext.Consumer>
The Provider component allows consuming components to subscribe to context changes.
It accepts a value
prop to be passed to consuming components that are descendants of this Provider. Thus, one Provider can be connected to many consumers. Providers can even be nested to override values deeper within the component tree.
All consumers that are descendants of a Provider will re-render whenever the Provider’s value
prop changes.
The Consumer component is the component which subscribes to the context changes. The Consumer component however requires a function as a child like render props. The function receives the current context value and returns a React node.
The value argument passed to the function will be equal to the value prop of the closest Provider for this context above in the tree. Thus, in the code above I have used the value to colour the button,
<ThemeContext.Consumer>
{value => <button style={{backgroundColor: value}} />}
</ThemeContext.Consumer>
Get the value of the Context
So you know how to use the Context API now. But if you look at the Provider and think about the use cases, you'll quickly realise that it's a bit difficult to extract the context from our JSX code for implementing other functionality. Sure, there are workarounds but that isn't really ideal. You may see this somewhere, but it is usually legacy.
Now if ThemedButton
was a class component we would be able to extract the context with contextType.
The contextType property
on a class can be assigned a Context object. This lets you consume the nearest current value of that Context type using this.context
. You can reference this in any of the lifecycle methods including the render function. So we could implement it like this.
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
However, we are using lightweight functional components and Hooks! So let's refactor our existing code a bit,
import React, { useContext } from 'react';
const ThemeContext = React.createContext('white');
function App() {
return (
<div className="App">
<ThemeContext.Provider value={"black"}>
<AppBar />
</ThemeContext.Provider>
</div>
);
}
function AppBar() {
return(
<div className="AppBar">
<ThemedButton />
</div>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext)
return(
<div>
<button style={{backgroundColor: theme}} />
</div>
)
}
export default App;
Over here, we've used the useContext
hook which is the functional component equivalent contextType
. With useContext
we can do away with the provider, and get the current context value outside of our JSX code.
Updating Our Context
Updating our Context is as simple as updating a state. With functional components, we can use the useState
hook to achieve this by passing down a function which will update out context,
import React, { useState, useContext } from "react";
const ThemeContext = React.createContext({ theme: "white", toggler: () => {} });
function App() {
const [color, setColor] = useState("white");
const toPass = {
theme: color,
toggler: () => {
return color === "white" ? setColor("black") : setColor("white");
},
};
return (
<div className="App">
<ThemeContext.Provider value={toPass}>
<AppBar />
</ThemeContext.Provider>
</div>
);
}
function AppBar() {
return (
<div className="AppBar">
<ThemedButton />
</div>
);
}
function ThemedButton() {
const context = useContext(ThemeContext);
return (
<div>
<button
style={{ backgroundColor: context.theme }}
onClick={context.toggler}
/>
</div>
);
}
export default App;
As you see above the color
state is manipulated by a toggle function which we passed down via our Context. The toggle function from the global Context is then called by the button in a child component, which thus updates the global Context.
So there you go! You now know how to use Context and Hooks to maintain a global state throughout your component tree.
If you'd like to dig deeper into Context, read the official docs,
Top comments (6)
I've heard context has some performance issue when there are alot of changes, aka the app is very dynamic and In those cases it is better to use redux, is that still the case or there were any changes?
As far as I know, problem with React Context is that if you update the values that you send to Provider, the whole React component tree that is wrapped in that Provider will re-render. So using it for app wide state might present some performance issues, depending on what are you doing.
Maybe using it for theme data won't be a problem since that's something that probably won't change that often. For some more dynamic state, maybe it's better to create multiple React Contexts that only provide data to smaller parts of the app or use a dedicated state library like Redux or MobX or many others that exist.
No. Only components wrapped inside Consumer will re-render or components with React.useContext to consum the data from Context will re-render.
Yup! Only components which subscribe to the Context changes, i.e, Consumers will re-render. However, Context should only be used when you require a few props to be available to a ton of components in a component tree, and prop drilling becomes cumbersome.
Yeah you are both right, my mistake...But still, the component will re-render even if the values that it's using from that Context instance don't change.
Still, it might be better to use multiple Context instances for multiple parts of the app, instead of having single Context provide bunch of values to many many components (eg. wrapping your whole app inside a single Context provider)
Got it,
Thank you, Great article.