Understanding React Context API
May 7th 2020
React’s createContext
, useContext
, useReducer
, Context Provider
, Context Consumer
and creating custom hooks can all seem confusing. Where does one even start?
The Context API was created to solve a specific problem in react, sharing state down a component tree. Similar to the solution that Redux and React-Redux libraries solve, this solution prevents prop drilling. Prop drilling is when you have to continuously pass props down through a component tree in order for a component “way down” in the application to use that prop. If you’ve used react for sometime you have probably experienced this pain point.
Prop Drilling
Usually you manage state at a high level component such as the <App />
component, and if that root component needs to access a state value, as well as a “grand-child” component that needs to access that value, then the state must be passed down as a prop all the way down. For example, say in your root component you need to be aware of a property in your state called theme
. That theme property will determine the background color for your app, if it’s value is light
then the background color of the app will be white, but if the value is dark
the background color will be black. Furthermore, let’s say we have a List
component in our App
and that list component has a ListItem
component and that ListItem
component has a Button
component. The Button
component needs to be aware of the theme
property in the root state as well because it’s color will change depending on the value of the theme
. In a traditional architecture we would have to pass the value down as a prop such as:
import React, { useState } from "react"
const App = (props) => {
const [theme, setTheme] = useState('light')
const bg = theme === 'light' ? 'white' : 'black'
return (
<div style={{
background: bg
}}>
<List theme={theme} />
</div>
)
}
const List = ({ theme }) => (
<ul>
<ListItem theme={theme} text="light" />
<ListItem theme={theme} text="dark" />
</ul>
)
const ListItem = ({ theme, text }) => (
<li>
<Button theme={theme}>
{text}
</Button>
</li>
)
const Button = ({ children, theme }) => {
const bgColor = theme === 'light' ? 'darkgray' : 'lightgray'
return (
<button
style={{
backgroundColor: bgColor
}}>
{ children }
</button>
)
}
export default App
As you can see we had to pass the theme
prop down from App
to List
to ListItem
and finally to Button
. On top of that, List
and ListItem
did not have to (or care to) know about the value of theme
in our state and is irrelevant to them. Wouldn’t it be nice if only App
and the Button
component would have to know about theme
value and we wouldn’t have to prop drill it all the way down? Welcome in the Context API!
Welcome to the Context API
Context provides a way to pass data through the component tree without having to pass props down manually at every level.
https://reactjs.org/docs/context.html
Let’s look at how we would refactor our small application using the Context API and also we will use the reducer architecture with useReducer
hook from React to enhance our example so it could be extendable to other global state properties besides our theme
property.
First, we’ll create a new folder in our project directory called hooks
and inside that folder we can create a file called useTheme.js
. The useTheme.js
file will essentially become our state management for our application as we will abstract our application state from our App
component. First we’ll have to import some helpers from react for us to use the Context API and also the reducer architecture:
import React, { createContext, useContext, useReducer } from 'react'
We will use the createContext
method to create our context object. A context object that is created by createContext
gives us access to a Provider
property that functions as a Higher Order Component in which we will wrap our component tree in that needs access to our state. Remember, our component tree is just the components that make up our App including: App
, List
, ListItem
and Button
. Any component “down stream” in our component tree can become a consumer to access our context value. Let’s look at creating a context in our useTheme.js
file:
import React, { createContext, useContext, useReducer } from 'react'
const ThemeContext = createContext()
Creating a Reducer to Manage State
Next we will create a reducer which will help us set and manage our state. We can create a variable that holds our default state such as:
const DEFAULT_STATE = {
theme: "light"
}
Not complicated but this will demonstrate what our state object will look like. Now we can create our reducer. As convention, for reducers our reducer function will accept two arguments: state object and an action object. For our actions object we will have an action.type
which is the action we want to perform on our state and action.payload
which will be the value of the state property we are setting. Let’s look at our reducer:
const reducer = (state, action) => {
switch(action.type){
case 'theme':
return { ...state, theme: action.payload }
default:
return DEFAULT_STATE
}
}
This is a very simple reducer for now but it demonstrates the fundamentals of using a reducer and checking the action.type
to change (and return) our state. Putting this together so far our useTheme.js
file should look like:
import React, { createContext, useContext, useReducer } from 'react'
const ThemeContext = createContext()
const DEFAULT_STATE = {
theme: "light"
}
const reducer = (state, action) => {
switch(action.type){
case 'theme':
return { ...state, theme: action.payload }
default:
return DEFAULT_STATE
}
}
Defining a Context Provider
Now that we created our Context as ThemeContext
and we have defined our default state and created a reducer to handle the changes in state we can focus on our provider. Since we’ll be exporting our provider to use to wrap our <App />
component we’ll need to export it as a variable:
const ThemeProvider = ({ children }) => {
<ThemeContext.Provider value="light">
{ children }
</ThemeContext>
}
Context Providers accepts a value
prop to be passed to consuming components that are descendants (in our component tree) of this Provider. For now we hard coded this value so now that any child component that needed access to this value could use:
import React, { useContext } from 'react'
import { ThemeContext } from '../hooks/useTheme'
//assuming this component is a child of our <ThemeProvider>
const ChildComponent = (props) => {
const theme = useContext(ThemeContext)
console.log(theme) // would console log "light"
return (
<span>The theme is: {theme}</span>
)
}
But since we are using a reducer example we can change the value
prop in our ThemeProvider
function to use our reducer:
const ThemeProvider = ({ children }) => {
<ThemeContext.Provider value={ useReducer(reducer, DEFAULT_STATE) }>
{ children }
</ThemeContext>
}
The useReducer
hook is similar to useState
in that it returns an array with the first value the state that is returned from our reducer
function and the second value of the array is a dispatcher
function which we can use to dispatch actions and in turn change our state. Since we’re using a reducer as the Provider value we can define our useTheme.js
file further by creating a custom hook and exporting that hook which can be imported into any of our consumer components to get our state context.
Creating a Custom Hook to for Consuming Components
Let’s continue our useTheme.js
and define our custom hook:
const useTheme = () => {
// short version:
// return useContext(ThemeContext)
// long version but more declarative
const [state, dispatcher] = useContext(ThemeContext)
return [state, dispatcher]
}
Let’s wrap up our useTheme.js
file by exporting our ThemeProvider
and useTheme
hook:
import React, { createContext, useContext, useReducer } from 'react'
const ThemeContext = createContext()
const DEFAULT_STATE = {
theme: "light"
}
const reducer = (state, action) => {
switch(action.type){
case 'theme':
return { ...state, theme: action.payload }
default:
return DEFAULT_STATE
}
}
const ThemeProvider = ({ children }) => {
<ThemeContext.Provider value={ useReducer(reducer, DEFAULT_STATE) }>
{ children }
</ThemeContext>
}
const useTheme = () => {
const [state, dispatcher] = useContext(ThemeContext)
return [state, dispatcher]
}
export { ThemeProvider, useTheme }
Now with that wrapped up we can refactor our App so we can utilize our new hook. First, we need to wrap our <App />
in our <ThemeProvider>
higher order component. In our index.js
file with defines ReactDOM.render
we can import our ThemeProvider
and wrap our <App />
. Let’s change index.js
so that:
import React from "react";
import ReactDOM from "react-dom";
import { ThemeProvider } from "./hooks/useTheme";
import App from "./components/App";
ReactDOM.render(
<ThemeProvider>
<App />
</ThemeProvider>,
document.getElementById("root")
);
With our <App />
component wrapped in our <ThemeProvider>
we are able to use our custom hook useTheme
to consume the value
we defined in our ThemeProvider
function which is the returned value of useReducer
which would be [state, dispatcher]
Returning to our App.js
file we can refactor our App to make use of our custom hook and access a “global” state without prop drilling our state down. Let’s look at our App.js
file:
import React from "react";
import { useTheme } from "../hooks/useTheme";
const App = props => {
const [state] = useTheme();
const bg = state.theme === "light" ? "#ffffff" : "#000000";
return (
<div
style={{
background: bg
}}
>
<List />
</div>
);
};
const List = () => (
<ul>
<ListItem value="light" />
<ListItem value="dark" />
</ul>
);
const ListItem = props => (
<li>
<Button {...props} />
</li>
);
const Button = ({ value }) => {
const [state, dispatcher] = useTheme();
const bgColor = state.theme === "light" ? "#333333" : "#eeeeee";
const textColor = state.theme === "light" ? "#ffffff" : "#000000";
return (
<button
style={{
backgroundColor: bgColor,
color: textColor
}}
onClick={() => {
dispatcher({ type: "theme", payload: value });
}}
>
{value}
</button>
);
};
export default App;
The two important changes to highlight is we are using our useTheme
hook to get the state in our parent App
function and conditionally set the background color of our app. Secondly, we were able to remove the theme
prop from our component tree and the Button
component was able to access our state via the useTheme
custom hook that we created. We also made some more changes to our Button
component in order to dispatch an action which updates our state. Let’s review that portion quickly:
const Button = ({ value }) => {
const [state, dispatcher] = useTheme();
const bgColor = state.theme === "light" ? "#333333" : "#eeeeee";
const textColor = state.theme === "light" ? "#ffffff" : "#000000";
return (
<button
style={{
backgroundColor: bgColor,
color: textColor
}}
onClick={() => {
dispatcher({ type: "theme", payload: value });
}}
>
{value}
</button>
);
};
First, we are accepting a prop
called value
that is inherited from our List
component and passed down through our ListItem
component. This value represents a theme option so when a user clicks on a button it will change the theme to the respective value. Second, we are using our custom hook useTheme
to get the state of our application (which is returned by our reducer) and also we’re destructuring the array to get the dispatcher
function which sends an action to our reducer and changes our state. We are setting an onClick
callback handler function on our button and invoking our dispatcher
function inside our click handler, passing in the type
and the payload
which is the value
of the button the user clicked on.
If you want to check out the full results of the application you can use the CodeSandbox link: https://codesandbox.io/s/j1ebq
In this tutorial we looked at how to use the Context API in React to avoid prop drilling and use a “global” state in our component tree. We also were able to utilize a useReducer
hook with a custom reducer we created in order for us to help manage our state. Finally, we created a custom hook that we could reuse in our consuming components that would get the current value of our state, and also provide a dispatcher
function that we could use to update our state. Thanks for taking the time to explore React’s Context API with me and hopefully this simple example helped you understand the power of what can be accomplished with the API. Until next time, stay curious, stay creative.