In a typical React app, data is passed top-down via props, like this:
class App extends React.Component {
state = {
theme: "dark"
}
render() {
return <Toolbar theme={this.state.theme} />
}
}
const Toolbar = ({ theme }) => (
<div>
<ThemedButton theme={props.theme} />
</div>
)
const ThemedButton = ({ theme }) => (
<Button theme={props.theme} />
)
Button
wants to know the theme, so it needs to be passed down from App
, through Toolbar
, and into ThemedButton
for the Button
to finally get access to it. This process, passing components down from a higher parent down through components that don't even necessarily need it, so they can get to components lower down the chain, is called 'prop drilling'.
It's kind of a pain in the ass. Usually the way this is solved is through some kind of 3rd party state management library like Redux or Mobx.
const ThemeContext = React.createContext();
class App extends React.Component {
render() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
)
}
}
const Toolbar = () => (
<div>
<ThemedButton />
</div>
)
const ThemedButton = () => (
<ThemeContext.Consumer>
{theme => <Button theme={theme}> />}
</ThemeContext.Consumer>
)
By taking advantage of the consumer/provider
components offered to us by createContext
, we can 'skip' passing theme
through props and access it directly in Toolbar
!
The Context API can be broken down into three basic pieces:
The createContext
method has this signature:
React.createContext(defaultValue);
It returns an object which holds a { Provider, Consumer }
pair. You can destructure it like this:
const { Provider, Consumer } = React.createContext()
// which gives you access to a <Provider> component
<Provider></Provder>
// and a <Consumer> component
<Consumer></Consumer>
or like this in the 'compound component' style:
const Context = React.createContext()
// now we have
<Context.Provider></Context.Provider>
// and
<Context.Consumer></Context.Consumer>
The Provider
looks like this:
<Provider value={/* some value */}>
It's a React component that you can pass some value in to as a prop. Whenever that value changes, any of the Provider's Consumer
pairs will be notified of the updated value.
A single Provider
can have many consumers.
The Consumer
looks like this:
<Consumer>
{value => <div>{value}</div>}
</Consumer>
The Consumer
receives whatever value was passed into its <Provider>
pair.
Whenever the Provider
's value changes, the Consumer
re-renders and receives the new value (as a render prop
!)
Let's imagine we want different parts of our application to know which language our app should currently be rendering in:
// create our Provider and Consumer pair, store them in LanguageContext
const LanguageContext = React.createContext()
// first, we wrap the highest level component we want to know about our language in our Provider
const App = () => (
<div>
<LanguageContext.Provider value={'english'}>
<Header />
</LanguageContext>
</div>
)
// just a plain ol' react component, nothing to see here
const Header = () => (
<header>
<CurrentLanguage />
</header>
)
// We wrap our child component that wants to know about the parent's value in our Consumer
const CurrentLanguage = () => (
<LanguageContext.Consumer>
{val => <div>Current Language: {val}</div>} {/* english */}
</LanguageContext.Consumer>
)
Context can be accessed in lifecycle methods through a higher order function that returns your consumed component, like this:
class Button extends React.Component {
componentDidMount() {
// ThemeContext value is this.props.theme
}
componentDidUpdate(prevProps, prevState) {
// Previous ThemeContext value is prevProps.theme
// New ThemeContext value is this.props.theme
}
render() {
const {theme, children} = this.props;
return (
<button className={theme ? 'dark' : 'light'}>
{children}
</button>
);
}
}
export default props => (
<ThemeContext.Consumer>
{theme => <Button {...props} theme={theme} />}
</ThemeContext.Consumer>
);
Context uses reference identity to determine when to re-render, which means this:
class App extends React.Component {
render() {
return (
<Provider value={{something: 'something'}}>
<Toolbar />
</Provider>
);
}
}
will cause all child consumers to re-render (since a new object literal gets passed in every time the App component renders, regardless of if the value of value
has changed.
To solve this, simply lift the value into App
's state:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: {something: 'something'},
};
}
render() {
return (
<Provider value={this.state.value}>
<Toolbar />
</Provider>
);
}
}
As Redux maintainer Mark Erikson states in his blog post Redux - Not Dead Yet!:
"Context is great for passing down props without needing intermediary components. If that's all you're using Redux for, then you can safely use context instead.
Context does NOT give you Redux DevTools, Redux middleware, the ability to trace state updates, and all kinds of other goodies Redux offers you"
Here's Dan Abramov's take:
"Context is an advanced feature and is subject to change. In some cases its conveniences outweigh its downsides so some libraries like React Redux and React Router choose to rely on it despite the experimental nature.
The important part here is the word libraries. If context changes its behavior, we as library authors will need to adjust. However, as long as the library doesn’t ask you to directly use the context API, you as the user shouldn’t have to worry about changes to it.
React Redux uses context internally but it doesn’t expose this fact in the public API. So you should feel much safer using context via React Redux than directly because if it changes, the burden of updating the code will be on React Redux and not you."
Probably not. Or if you do, use it for relatively static application wide settings (locale and theme are the two that immediately come to mind).
There are a few uses cases where you might want to use context:
- You have a value you want to share in a lot of different parents of your application and don't want to clutter up your app through prop drilling
- You're relatively new to the app ecosystem and want to take advantage of shared component state, or want to take the first steps towards a flux/redux style approach to state management in your application.
- You're a library maintainer and you want to build a state management solution similar to something like
redux
.
- Official React Context Documentation
- React's new Context API
- Redux - Not Dead Yet!
- Replacing Redux with the new React Context API
- react-waterfall - a minimal state management library built on top of React's Context API