Context Hooks page
Learn how to manage global app state with context hooks.
The Prop Drilling Problem
A single component can take as many props as you want to give it, but just like arguments in functions, it’s a good idea to limit this number, as more props makes for a more confusing and harder to test component. This discussion here, perfectly summarizes why it’s a bad idea and considered a "code smell".
However, this can be difficult to do when you have a lot of data to pass through your component tree. Consider the following hierarchy with a few "drilled props". Lets imagine that the Theme, Domain and RootUrl are decided within the App component, but are only needed within the ButtonText component. That is to say, Dashboard and Button have no business related to any of those props.
──┬ App(Theme, Domain, RootUrl)
└─┬ Dashboard(Theme, Domain, RootUrl)
|─┬ Button(Theme, Domain, RootUrl)
│ └── ButtonText(Theme, Domain, RootUrl)
└─┬ Button(Theme, Domain, RootUrl)
└── ButtonText(Theme, Domain, RootUrl)
Without concerning ourselves with what exactly each of the props does, we can see the problem. Every piece of global state must be propagated through the entire hierarchy. As a result, every component would look something like this:
function Component1({ Theme, Domain, RootUrl }) {
return <Component2 Theme={Theme} Domain={Domain} RootUrl={RootUrl}>
}
It is possible to re-write it using the spread operator, but what you gain in conciseness, you lose in clarity and performance. Avoid writing components like this!
function Component1(props) {
return <Component2 {...props} >
}
In this case, it doesn’t matter if Component1 even needs the Theme prop; it will always require it simply because Component2 might require it. Initially, this was solved using libraries such as Redux. These libraries would work by wrapping each component in a connector Higher-order Component (HoC) which would automatically pass in any required props. Today, we solve this problem using React’s Context Providers and Consumers.
What is Context?
One way to think about Contexts is an additional set of props which are passed transparently through React’s internals instead of arguments. It involves three parts:
- The Context: Think of the context like a box of things. The box needs to be available to all who want to use it.
- The Provider: The provider puts things into the box. Whatever data it handles is only available to its children.
- The Consumer: The consumer takes things out of the box. It can only access the providers which are above it in the component hierarchy.
The Context API
Writing a Context
Creating a Context is as easy as calling createContext() and supplying it a default value.
import React, { createContext } from 'react';
const defaultValue = 'Unknown';
const UsernameContext = createContext(defaultValue);
The default value is what the Consumers will get if they have no available Provider. This is often used more in testing than in production.
Writing a Provider
The provider component is exposed by the context. It is always accessible via ContextName.Provider and requires a single prop named value. This prop will be provided to all of its Consumers.
Any components we render inside of the provider will be able to access the information in the value prop, no matter how deeply nested they are in the component tree. This eliminates the need for prop drilling where a single prop would need to be passed down through multiple components.
import React, { createContext } from 'react';
const defaultValue = 'Unknown';
const UsernameContext = createContext(defaultValue);
function App() {
let [username, setUsername] = React.useState('No-name');
return (
<UsernameContext.Provider value={username}>
<WhoAmI />
</UsernameContext.Provider>
);
}
Writing a Consumer
The consumer is similarly exposed by the context under ContextName.Consumer. It allows us to extract the value supplied to a producer above it using a callback.
import React, { createContext } from 'react';
const defaultValue = 'Unknown';
const UsernameContext = createContext(defaultValue);
function App() {
let [username, setUsername] = React.useState('No-name');
return (
<UsernameContext.Provider value={username}>
<WhoAmI />
</UsernameContext.Provider>
);
}
function WhoAmI() {
return (
<UsernameContext.Consumer>
{(value) => {
return <span>{value}</span>;
}}
</UsernameContext.Consumer>
);
}
Updating the Context
As discussed before, Contexts are a lot like props. Their only difference is the method through which they are passed. To update them, we need to update the value that is passed into the Provider. Consider the following changes to make it possible:
import React, { createContext } from 'react';
const defaultValue = {
username: 'Unknown',
setUsername: () => new Error('Not in provider'),
};
const UsernameContext = createContext(defaultValue);
function App() {
let [username, setUsername] = React.useState('No-name');
return (
<UsernameContext.Provider
value={{ username: username, setUsername: setUsername }}
>
<WhoAmI />
</UsernameContext.Provider>
);
}
function WhoAmI() {
return (
<UsernameContext.Consumer>
{(value) => {
return (
<>
<span>{value.username}</span>
<button onClick={() => value.setUsername('Mike')}>
My Name is Mike
</button>
<button onClick={() => value.setUsername('Kyle')}>
My Name is Kyle
</button>
</>
);
}}
</UsernameContext.Consumer>
);
}
We have made three changes to our code:
- We have changed the shape of our context data from a String, to an Object. This was necessary to pass multiple properties within the same Context.
- We included the
setUsernamefunction in the value of our Provider. - We added 2 buttons to call the
setUsernamefunction in theWhoAmIcomponent.
Context with Hooks
With the introduction of hooks, React also brought us the useContext hook. It allows us to consume Contexts without callbacks. In order to use it, pass the Context object to the useContext hook.
The above example could be re-written to use the useContext hook as follows:
import React, { createContext, useContext } from 'react';
const defaultValue = {
username: 'Unknown',
setUsername: () => new Error('Not in provider'),
};
const UsernameContext = createContext(defaultValue);
function App() {
let [username, setUsername] = React.useState('No-name');
return (
<UsernameContext.Provider
value={{ username: username, setUsername: setUsername }}
>
<WhoAmI />
</UsernameContext.Provider>
);
}
function WhoAmI() {
const value = useContext(UsernameContext);
return (
<>
<span>{value.username}</span>
<button onClick={() => value.setUsername('Mike')}>My Name is Mike</button>
<button onClick={() => value.setUsername('Kyle')}>My Name is Kyle</button>
</>
);
}
The method for consuming contexts has now shifted from callbacks from within JSX to simple, procedural function calls. This has the benefit of making the code flatter, and easier to read. Additionally, using multiple consumers becomes significantly cleaner.
Without Hooks:
return (
<FooContext.Consumer>
{(foo) => (
<BarContext.Consumer>
{(bar) => (
<BazContext.Consumer>
{(baz) => (
<span>
{foo} {bar} {baz}
</span>
)}
</BazContext.Consumer>
)}
</BarContext.Consumer>
)}
</FooContext.Consumer>
);
With Hooks
const foo = useContext(FooContext);
const bar = useContext(BarContext);
const baz = useContext(BazContext);
return (
<span>
{foo} {bar} {baz}
</span>
);
Much better.
Advanced Provider Patterns
The example above demonstrates the simplest use-case for context, but often times developers will organize their providers to abstract away a lot of the boilerplate. Exposing your Context object directly can also result in more complex code and even more complex maintenance, as the consuming code is accessing the data directly. By creating wrappers for the Provider component and useContext hook, you can control exactly what data each component is using, thereby reducing code complexity and simplifying maintenance.
Let us consider a more complex situation: Global styles. take a look at how we might refactor the ThemeContext so that it’s wrapped in it’s own custom component:
import React, { createContext } from 'react';
const THEMES = {
blue: { color: '#0000ff', fontSize: '1.25rem' },
red: { color: '#7ea0ff', fontSize: '2rem' },
};
const ThemeContext = React.createContext();
export default function ThemeProvider({ theme, children }) {
return (
<ThemeContext.Provider value={THEMES[theme]}>
{children}
</ThemeContext.Provider>
);
}
In the example above, we’ve taken away all of the ThemeContext logic and encapsulated it into its own component ThemeProvider. This is a very common technique for organizing contexts in a scalable and reusable way.
We can take this a step further by exporting a custom hook useTheme from this file, which can then be used by nested components like Button to access the theme:
import React, { createContext } from 'react';
const THEMES = {
blue: { color: '#0000ff', fontSize: '1.25rem' },
red: { color: '#7ea0ff', fontSize: '2rem' },
};
const ThemeContext = React.createContext();
export default function ThemeProvider({ theme, children }) {
return (
<ThemeContext.Provider value={THEMES[theme]}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const theme = useContext(ThemeContext);
return theme;
}
Now, we can refactor our Layout component to use this new provider. Notice how much cleaner it looks when we abstract away the context logic into its own component (ThemeProvider)
import React from 'react';
import ThemeProvider from './ThemeProvider';
export default function Layout() {
return (
<ThemeProvider theme="blue">
<Button label="Click Me!" />
</ThemeProvider>
);
}
We can also refactor the way the Button component consumes the theme, by having it use the newly exposed useTheme custom hook.
import React from 'react';
import { useTheme } from './ThemeProvider';
function Button({ label }) {
const theme = useTheme();
return (
<div style={theme}>
<button>{label}</button>
</div>
);
}
Exercise
Let’s use our context hook knowledge to make our Tic-Tac-Toe use a style theme!
The problem
✏️ Let’s add in the ability to use a style theme for our app.
GameComponent- Create a new piece of state called
theme, which will store the current theme used by the app (themes.lightby default). - Create a
ThemeContextobject usingReact.createContext() - Wrap the component tree in
<ThemeContext.Provider>and give it a value of thethemestate. - Create and export a custom hook called
useTheme. - (Optional) Add in a button which allows the user to switch between
themes.lightandthemes.dark
- Create a new piece of state called
SquareComponent- Get the theme from the new
useThemehook in theGamecomponent.
- Get the theme from the new
Here’s the starter code
const squareStyling = {
width: '200px',
height: '200px',
border: '1px solid black',
boxSizing: 'border-box',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
fontSize: '6em',
color: 'black',
};
const boardStyling = {
display: 'flex',
flexWrap: 'wrap',
flexDirection: 'row',
justifyContent: 'flex-start',
width: '600px',
height: '600px',
boxShadow: '0px 3px 8px 0 rgba(0, 0, 0, 0.1)',
boxSizing: 'border-box',
};
function boardHasWinner(board) {
const winningCombos = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
const winningCombo = winningCombos.find((combo) => {
return (
board[combo[0]] !== '' &&
board[combo[0]] === board[combo[1]] &&
board[combo[0]] === board[combo[2]]
);
});
return !!winningCombo;
}
function Square({ onClick, symbol, id }) {
const theme = {}; // Get this from a custom hook
return (
<div
id={id}
style={Object.assign({}, squareStyling, {
color: theme.text,
backgroundColor: theme.background,
})}
onClick={onClick}
>
{symbol}
</div>
);
}
function Board({ board, handleSquareClick }) {
return (
<div style={boardStyling}>
{board.map((symbol, index) => (
<Square
key={index}
symbol={symbol}
onClick={() => handleSquareClick(index)}
/>
))}
</div>
);
}
// Create a context for your theme
// Provide a theme (light or dark) to the children of this component
// Export a custom hook that returns the current theme
// Consume the custom hook in Square.js
// Create a state and button that allows the user to switch between themes
const blankBoard = ['', '', '', '', '', '', '', '', ''];
function Game() {
const [board, setBoard] = React.useState(blankBoard);
const [isXTurn, setIsXTurn] = React.useState(true);
const currentPlayer = isXTurn ? 'X' : 'O';
function handleSquareClick(squareIndex) {
if (!board[squareIndex]) {
const newBoard = [...board];
newBoard[squareIndex] = currentPlayer;
if (boardHasWinner(newBoard)) {
alert(`${currentPlayer} Wins!`);
resetGame();
} else {
setBoard(newBoard);
setIsXTurn(!isXTurn);
}
}
}
function resetGame() {
setIsXTurn(true);
setBoard(blankBoard);
}
return (
<>
<Board board={board} handleSquareClick={handleSquareClick} />
<button onClick={() => {}}>Toggle Theme</button>
current player: {currentPlayer}
</>
);
}
ReactDOM.render(<Game />, document.getElementById('root'));
Solution
Click to see the solution
Game
const squareStyling = {
width: '200px',
height: '200px',
border: '1px solid black',
boxSizing: 'border-box',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
fontSize: '6em',
color: 'black',
};
const boardStyling = {
display: 'flex',
flexWrap: 'wrap',
flexDirection: 'row',
justifyContent: 'flex-start',
width: '600px',
height: '600px',
boxShadow: '0px 3px 8px 0 rgba(0, 0, 0, 0.1)',
boxSizing: 'border-box',
};
function boardHasWinner(board) {
const winningCombos = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
const winningCombo = winningCombos.find((combo) => {
return (
board[combo[0]] !== '' &&
board[combo[0]] === board[combo[1]] &&
board[combo[0]] === board[combo[2]]
);
});
return !!winningCombo;
}
const themes = {
light: {
text: '#4A5568',
background: '#EDF2F7',
},
dark: {
text: '#EDF2F7',
background: '#4A5568',
},
};
const ThemeContext = React.createContext();
function useTheme() {
return React.useContext(ThemeContext);
}
function Square({ onClick, symbol, id }) {
const theme = useTheme();
return (
<div
id={id}
style={Object.assign({}, squareStyling, {
color: theme.text,
backgroundColor: theme.background,
})}
onClick={onClick}
>
{symbol}
</div>
);
}
function Board({ board, handleSquareClick }) {
return (
<div style={boardStyling}>
{board.map((symbol, index) => (
<Square
key={index}
symbol={symbol}
onClick={() => handleSquareClick(index)}
/>
))}
</div>
);
}
const blankBoard = ['', '', '', '', '', '', '', '', ''];
function Game() {
const [board, setBoard] = React.useState(blankBoard);
const [isXTurn, setIsXTurn] = React.useState(true);
const [theme, setTheme] = React.useState(themes.light);
const currentPlayer = isXTurn ? 'X' : 'O';
function handleSquareClick(squareIndex) {
if (!board[squareIndex]) {
const newBoard = [...board];
newBoard[squareIndex] = currentPlayer;
if (boardHasWinner(newBoard)) {
alert(`${currentPlayer} Wins!`);
resetGame();
} else {
setBoard(newBoard);
setIsXTurn(!isXTurn);
}
}
}
function handleToggleTheme() {
setTheme(theme === themes.light ? themes.dark : themes.light);
}
function resetGame() {
setIsXTurn(true);
setBoard(blankBoard);
}
return (
<ThemeContext.Provider value={theme}>
<Board board={board} handleSquareClick={handleSquareClick} />
<button onClick={handleToggleTheme}>Toggle Theme</button>
current player: {currentPlayer}
</ThemeContext.Provider>
);
}
ReactDOM.render(<Game />, document.getElementById('root'));

