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
setUsername
function in the value of our Provider. - We added 2 buttons to call the
setUsername
function in theWhoAmI
component.
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.
Game
Component- Create a new piece of state called
theme
, which will store the current theme used by the app (themes.light
by default). - Create a
ThemeContext
object usingReact.createContext()
- Wrap the component tree in
<ThemeContext.Provider>
and give it a value of thetheme
state. - Create and export a custom hook called
useTheme
. - (Optional) Add in a button which allows the user to switch between
themes.light
andthemes.dark
- Create a new piece of state called
Square
Component- Get the theme from the new
useTheme
hook in theGame
component.
- 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'));