React Context, All in One
Javier Murillo
Posted on August 12, 2021
All you need to know about the React Context API: basics, optimization, good practices, testing, and future. All the pieces together. All in One.
What is React Context for?
✔️ Simple dependency injection mechanism, avoiding the infamous prop drilling.
✔️ No third-party libraries, React Context is integrated with React and for sure this API will be updated in the future with many improvements.
✔️ Ideal when you can split your states in order to make them accesible to your React component tree (e.g. Theme, Authentication, i18n, ...)
❌ It is not a global state management tool. You manage your state via useState
or useReducer
.
❌ If you app state is frequently updated Context is not the best solution.
❌ Not suitable if you need complex capabilities such as side effects, persistence and data serialization.
❌ Worse debugging since you don't have "Redux DevTools" including the actions history for example.
❌ You have to implement it right in order to avoid optimization leaks. React does not help you there. This post does.
React Context usage example
Let's start straight with some code in order to know:
- How to create a Context.
- How to create a Provider which will provide the context value.
- How to create Consumer components which will use the context value.
// index.jsx
ReactDOM.render(
<MyProvider>
<MyEntireApp/>
</MyProvider>,
document.getElementById('root'),
)
// myContext.js
import { createContext } from 'react'
// Creating the Context
const MyContext = createContext()
export default MyContext
// MyProvider.jsx
const MyProvider = ({ children }) => {
const [state, setState] = useState({})
const fetch = async () => {
// Fetching some data
setState({ ... })
}
useEffect(() => {
fetch()
}, [])
// Providing a value
return (
<MyContext.Provider value={{state, setState}}>
{children}
</MyContext.Provider>
)
}
// FunctionalComponent.jsx
const Consumer = () => {
// Consuming the Context
const myContext = useContext(MyContext)
return (
// Here we can access to the context state
)
}
// ClassComponent.jsx
class Consumer {
constructor () { ... }
render () {
// Consuming the Context
<MyContext.Consumer>
{(myContext) => (
// Here we can access to the context state
)}
</MyContext.Consumer>
}
}
⚠️ When the nearest
<MyContext.Provider>
above the component updates,React.useContext(...)
will trigger a rerender with the latest context value passed to that MyContext provider. Even if an ancestor usesReact.memo
orshouldComponentUpdate
, a rerender will still happen starting at the component itself usinguseContext
.A component calling useContext will always re-render when the context value changes. If re-rendering the component is expensive, you can optimize it by using memoization.
https://reactjs.org/docs/hooks-reference.html#usecontext
What happens with the initial value passed to React.createContext(...)
?
In our example above we are passing undefined
as a our initial context value, but at the same time we are overriding it in our Provider:
const MyContext = createContext()
<MyContext.Provider value={{state, setState}}>
{children}
</MyContext.Provider>
The value that createContext
is receiving as default (undefined) will be the one a Consumer will receive if it does not have any Provider above itself in the component tree.
const Root = () => {
// ⚠️ Here we will get an error since we cannot
// destructure `state` from `undefined`.
const { state } = useContext(MyContext)
return <div>{state}</div>
}
ReactDOM.render(<Root />, document.getElementById('root'))
In our case, our Consumers will always have a Provider above them, since our Provider wraps the entire application (see index.js
). The implementation of a custom hook to use our Context could be a cool idea in order to improve code legibility, abstract the use of useContext
, and throw an error if our Context is used incorrectly (remember, failing fast).
// MyProvider.jsx
const MyProvider = ({ children }) => {
const [state, setState] = useState([])
// Provider stuff...
<MyContext.Provider value={{state, setState}}>
{children}
</MyContext.Provider>
}
// For Hooks
const useMyCtx = () => {
const context = useContext(MyContext)
if (context === undefined) {
throw new Error('useMyCtx must be used withing a Provider')
}
return context
}
// For Classes
const ContextConsumer = ({ children }) => {
return (
<MyContext.Consumer>
{context => {
if (context === undefined) {
throw new Error('ContextConsumer must be used
within a Provider')
}
return children(context)
}}
</MyContext.Consumer>
)
}
export { MyProvider, useMyCtx, ContextConsumer }
With Hooks
// FunctionalComponent.jsx
const Consumer = () => {
const context = useMyCtx()
}
With Classes
// ClassComponent.jsx
class Consumer extends Component {
constructor() { ... }
render() {
return <ContextConsumer>
{context => // Here we can access to the context state }
</ContextConsumer>
}
}
Does my entire app re-render if the Provider state changes?
Depends on how you implemented your provider:
// ❌ Bad
// When the provider's state changes, React translates the rendering
// of <MyEntireApp/> as follows:
// React.creatElement(MyEntireApp, ...),
// rendering it as a new reference.
// ⚠️ No two values of the provider’s children will ever be equal,
// so the children will be re-rendered on each state change.
const Root = () => {
const [state, setState] = useState()
<MyContext.Provider value={{state, setState}>
<MyEntireApp />
</MyContext.Provider>
}
// ✔️ Good
// When the provider's state changes, the children prop
// stays the same so <MyEntireApp/> is not re-rendering.
// `children` prop share reference equality with its previous
// `children` prop.
const MyProvider = ({ children }) => {
const [state, setState] = useState()
<MyContext.Provider value={{state, setState}}>
{children}
</MyContext.Provider>
}
const Root = () => {
<MyProvider>
<MyEntireApp />
</MyProvider>
}
Can I store my global state in just one Context?
No. Well, yes, but you shouldn't. The reason is simple, consider the following global state:
{
auth: {...}
translations: {...}
theme: {...}
}
⚠️ If a component only consumes the theme
, it still will be re-rendered even if another state property changes.
// FunctionalComponent.jsx
// This component will be re-rendered when `MyContext`'s
// value changes, even if it is not the `theme`.
const Consumer = () => {
const { theme } = useContext(MyContext)
render <ExpensiveTree theme={theme} />
}
You should instead split that state in some Contexts. Something like this:
// index.jsx
// ❌ Bad
ReactDOM.render(
<GlobalProvider>
<MyEntireApp/>
</GlobalProvider>,
document.getElementById('root'),
)
// ✔️ Good
ReactDOM.render(
<AuthProvider>
<TranslationsProvider>
<ThemeProvider>
<MyEntireApp/>
</ThemeProvider>
</TranslationsProvider>
</AuthProvider>,
document.getElementById('root'),
)
As you can see this can end in an endless arrowhead component, so a good practice could be splitting this in two files:
// ProvidersWrapper.jsx
// This `ProvidersWrapper.jsx` can help you implementing testing
// at the same time.
const ProvidersWrapper = ({ children }) => (
<AuthProvider>
<TranslationsProvider>
<ThemeProvider>
{children}
</ThemeProvider>
</TranslationsProvider>
</AuthProvider>
)
// index.jsx
ReactDOM.render(
<ProvidersWrapper>
<MyEntireApp/>
</ProvidersWrapper>,
document.getElementById('root'),
)
By doing this, each Consumer should use just what it needs.
Alternatives to splitting Contexts
Instead of splitting contexts, we could apply the following techniques in order to <ExpensiveTree />
don't re-render if a property he is not consuming changes:
1. Spliting the Consumer in two with memo
in between.
// FunctionalComponent.jsx
const Consumer = () => {
const { theme } = useContext(MyContext)
return <ThemeConsumer theme={theme} />
}
const ThemeConsumer = memo(({ theme }) => {
// The rest of your rendering logic
return <ExpensiveTree theme={theme} />
})
An advanced implmentation would be the creation of a HOC with a custom connect(...)
function as follows:
const connect = (MyComponent, select) => {
return function (props) {
const selectors = select();
return <WrappedComponent {...selectors} {...props}/>
}
}
import connect from 'path/to/connect'
const MyComponent = React.memo(({
somePropFromContext,
otherPropFromContext,
someRegularPropNotFromContext
}) => {
... // regular component logic
return(
... // regular component return
)
});
const select = () => {
const { someSelector, otherSelector } = useContext(MyContext);
return {
somePropFromContext: someSelector,
otherPropFromContext: otherSelector,
}
}
export default connect(MyComponent, select)
Source: https://github.com/reactjs/rfcs/pull/119#issuecomment-547608494
However this is against the nature of React Context and does not solve the main issue: the HOC what wraps the Component still tries to re-render, there may be multiple HOCs for just one updated resulting in an expensive operation.
2. One component with useMemo
inside
const Consumer = () => {
const { theme } = useContext(MyContext)
return useMemo(() => {
// The rest of your rendering logic
return <ExpensiveTree theme={theme} />
}, [theme])
}
3. Third-party React Tracked
Prior to v1.6.0, React Tracked is a library to replace React Context use cases for global state. React hook useContext triggers re-renders whenever a small part of state object is changed, and it would cause performance issues pretty easily. React Tracked provides an API that is very similar to useContext-style global state.
const useValue = () => useState({
count: 0,
text: 'hello',
})
const { Provider, useTracked } = createContainer(useValue)
const Consumer = () => {
const [state, setState] = useTracked()
const increment = () => {
setState((prev) => ({
...prev,
count: prev.count + 1,
})
}
return (
<div>
<span>Count: {state.count}</span>
<button type="button" onClick={increment}>+1</button>
</div>
)
}
The useTracked hook returns a tuple that useValue returns, except that the first is the state wrapped by proxies and the second part is a wrapped function for a reason.
Thanks to proxies, the property access in render is tracked and this component will re-render only if state.count is changed.
https://github.com/dai-shi/react-tracked
Do I need to memoize my Provider value or my component?
It depends. Apart from the cases we just saw... Do you have a parent above your Provider which can be updated forcing a natural children re-rendering by React?
// ⚠️ If Parent can be updated (via setState() or even via
// a grandparent) we must be careful since everything
// will be re-rendered.
const Parent = () => {
const [state, setState] = useState()
// Stuff that forces a re-rendering...
return (
<Parent>
<MyProvider>
<MyEntireApp/>
</MyProvider>
</Parent>
)
}
If so, yes. You will have to memoize both the Provider and your Component as follows:
// MyProvider.jsx
const MyProvider = ({ children }) => {
const [state, setState] = useState({})
// With `useMemo` we avoid the creation of a new object reference
const value = useMemo(
() => ({
state,
setState,
}),
[state]
)
<MyContext.Provider value={value}>
{children}
</MyContext.Provider>
}
// FunctionalComponent.jsx
// With `memo` we avoid the re-rendering if props didn't change
// Context value didn't change neither thanks to the previous
// `useMemo`.
const Consumer = memo((props) => {
const myContext = useContext(MyContext)
})
But this is unlikely, you want always to wrap your entire application with your Providers as we saw previously.
ReactDOM.render(
<MyProvider>
<MyEntireApp/>
</MyProvider>,
document.getElementById('root'),
)
Splitting Context in two: stateContext
and setStateContext
For the same reasons we already talked about previously:
⚠️ A Consumer that just changes the state of a Context (by using setState
or dispatch
) will be re-rendered once the update is performed and the value changes.
That's why it is a good idea to split that context in two as follows:
const CountStateContext = createContext()
const CountUpdaterContext = createContext()
const Provider = () => {
const [count, setCount] = usetState(0)
// We memoize the setCount in order to do not create a new
// reference once `count` changes. An alternative would be
// passing directly the setCount function (without
// implementation) via the provider's value or implementing its
// behaviour in our custom hook.
const memoSetCount = useCallback(() => setCount((c) => c + 1), [
setCount,
])
return (
<CountStateContext.Provider value={count}>
<CountUpdaterContext.Provider value={memoSetCount}>
{props.children}
</CountUpdaterContext.Provider>
</CountStateContext.Provider>
)
}
const useCountState() {
const countStateCtx = useContext(StateContext)
if (typeof countStateCtx === 'undefined') {
throw new Error('useCountState must be used within a Provider')
}
return countStateCtx
}
function useCountUpdater() {
const countUpdaterCtx = useContext(CountUpdaterContext)
if (typeof countUpdaterCtx === 'undefined') {
throw new Error('useCountUpdater must be used within a Provider')
}
// We could here implement setCount to avoid the previous useCallback
// const setCount = () => countUpdaterCtx((c) => c + 1)
// return setCount
return countUpdaterCtx
}
// CountConsumer.jsx
// This component will re-render if count changes.
const CountDisplay = () => {
const count = useCountState()
return (
<>
{`The current count is ${count}. `}
</>
)
})
// CountDispatcher.jsx
// This component will not re-render if count changes.
const CounterDispatcher = () => {
const countUpdater = useCountUpdater()
return (
<button onClick={countUpdater}>Increment count</button>
)
}
Components that use both the state and the updater will have to import them like this:
const state = useCountState()
const dispatch = useCountDispatch()
You can export both of them in a single function useCount
doing this:
const useCount = () => {
return [useCountState(), useCountDispatch()]
}
What about using useReducer
? Do I need to take in count everything we talked about?
Yes, of course. The unique difference about using the useReducer
hook is that now you are not using setState
in order to handle the state.
⚠️ Remember, React Context does not manage the state, you do it via useState
or useReducer
.
The possible optimization leaks remains the same we talked about this article.
React Context vs Redux
Let me link you an awesome article for this, authored by Mark "acemarke" Erikson, Redux mantainer:
Are Context and Redux the same thing?
No. They are different tools that do different things, and you use them for different purposes.Is Context a "state management" tool?
No. Context is a form of Dependency Injection. It is a transport mechanism - it doesn't "manage" anything. Any "state management" is done by you and your own code, typically via useState/useReducer.Are Context and useReducer a replacement for Redux?
No. They have some similarities and overlap, but there are major differences in their capabilities.When should I use Context?
Any time you have some value that you want to make accessible to a portion of your React component tree, without passing that value down as props through each level of components.When should I use Context and useReducer?
When you have moderately complex React component state management needs within a specific section of your application.When should I use Redux instead?
Redux is most useful in cases when:
- You have larger amounts of application state that are needed in many places in the app.
- The app state is updated frequently over time.
- The logic to update that state may be complex.
- The app has a medium or large-sized codebase, and might be worked on by many people.
- You want to be able to understand when, why, and how the state in your application has updated, and visualize the changes to your state over time.
- You need more powerful capabilities for managing side effects, persistence, and data serialization.
https://blog.isquaredsoftware.com/2021/01/context-redux-differences/#context-and-usereducer
Testing
Let's test the following case: we have a Provider which fetchs asynchronously some Articles in order to make them available to our fellow Consumers.
We will work with the following mock:
[
{
"id": 1,
"title": "Article1",
"description": "Description1"
},
{
"id": 2,
"title": "Article2",
"description": "Description2"
}
]
// ArticlesProvider.jsx
const ArticlesProvider = ({ children }) => {
const [articles, setArticles] = useState([])
const fetchArticles = async () => {
const articles = await ArticlesService.get('/api/articles')
setArticles(articles)
}
useEffect(() => {
fetchArticles()
}, [])
return (
<ArticlesContext.Provider value={{ articles, setArticles }}>
{children}
</ArticlesContext.Provider>
)
}
const useArticles = () => {
const articlesCtx = useContext(ArticlesContext)
if (typeof articlesCtx === "undefined") {
throw new Error("articlesCtx must be used within a Provider")
}
return articlesCtx
}
export { ArticlesProvider, useArticles }
// ArticlesProvider.spec.jsx
describe("ArticlesProvider", () => {
const noContextAvailable = "No context available."
const contextAvailable = "Articles context available."
const articlesPromise = new Promise((resolve) => resolve(articlesMock))
ArticlesService.get = jest.fn(() => articlesPromise)
// ❌ This code fragment is extracted directly from Testing Library
// documentation but I don't really like it, since here we are
// testing the `<ArticlesContext.Provider>` functionality, not
// our `ArticlesProvider`.
const renderWithProvider = (ui, { providerProps, ...renderOptions }) => {
return render(
<ArticlesContext.Provider {...providerProps}>
{ui}
</ArticlesContext.Provider>,
renderOptions
)
}
// ✔️ Now we are good to go, we test what our Consumers will actually use.
const renderWithProvider = (ui, { ...renderOptions }) => {
return render(<ArticlesProvider>{ui}</ArticlesProvider>, renderOptions)
}
// ⚠️ We mock a Consumer in order to test our Provider.
const ArticlesComsumerMock = (
<ArticlesContext.Consumer>
{(articlesCtx) => articlesCtx ? (
articlesCtx.articles.length > 0 &&
articlesCtx.setArticles instanceof Function && (
<span>{contextAvailable}</span>
)
) : (
<span>{noContextAvailable}</span>
)
}
</ArticlesContext.Consumer>
)
it("should no render any articles if no provider is found", () => {
render(ArticlesComsumerMock)
expect(screen.getByText(noContextAvailable)).toBeInTheDocument()
})
it("should render the articles are available", async () => {
renderWithProvider(ArticlesComsumerMock)
await waitFor(() => {
expect(screen.getByText(contextAvailable)).toBeInTheDocument()
})
})
})
Time to test our Consumer:
// Articles.jsx
const Articles = () => {
const { articles } = useArticles()
return (
<>
<h2>List of Articles</h2>
{articles.map((article) => (
<p>{article.title}</p>
))}
</>
)
}
// Articles.spec.jsx
describe("Articles", () => {
const articlesPromise = new Promise((resolve) => resolve(articlesMock))
ArticlesService.get = jest.fn(() => articlesPromise)
const renderWithProvider = (ui, { ...renderOptions }) => {
return render(<ArticlesProvider>{ui}</ArticlesProvider>, renderOptions)
}
it("should render the articles list", async () => {
renderWithProvider(<Articles />)
await waitFor(() => {
expect(screen.getByText("List of Articles")).toBeInTheDocument()
})
articlesMock.forEach((article) => {
expect(screen.getByText(article.title)).toBeInTheDocument()
})
})
})
Unstable feature: observed bits
// react/index.d.ts
function useContext<T>(context: Context<T>/*, (not public API) observedBits?: number|boolean */): T;
observedBits
is a hidden experimental feature that represent what context values did change.
We can prevent unnecessary re-renders in a Global state by calculating what bits changed and telling our components to observe the bits we are using.
// globalContext.js
import { createContext } from 'react';
const store = {
// The bit we want to observe
observedBits: {
theme: 0b001,
authentified: 0b010,
translations: 0b100
},
initialState: {
theme: 'dark',
authentified: false,
translations: {}
}
};
const getChangedBits = (prev, next) => {
let result = 0;
// ⚠️ With `result OR bits[key]` we calculate the total bits
// that changed, if only `theme` changed we will get 0b001,
// if the three values changed we will get: 0b111.
Object.entries(prev.state).forEach(([key, value]) => {
if (value !== next.state[key]) {
result = result | store.observedBits[key];
}
});
return result;
};
const GlobalContext = createContext(undefined, getChangedBits);
export { GlobalContext, store };
// Theme.jsx
const Theme = () => {
console.log('Re-render <Theme />');
// ⚠️ No matter if the state changes, this component will only
// re-render if the theme is updated
const { state } = useContext(GlobalContext, store.observedBits.theme);
return <p>Current theme: {state.theme}</p>;
};
Keep in mind this is an unstable feature, you are limited to observe 30 values (MaxInt.js) and you will be warned in console :P. I would prefer splitting contexts to pass the necessary props to your application tree, following the initial nature of React Context, while waiting for updates.
A complete demo with a functional playground of this can be found here: https://stackblitz.com/edit/react-jtb3lv
The Future
There are already some proposals to implement the selector
concept, in order to let React manage these optimizations if we are just observing one value in a global state:
const context = useContextSelector(Context, c => c.selectedField)
https://github.com/facebook/react/pull/20646
Bibliography
Interesting articles/comments I have been reading so far that helped me to put all the pieces together, including some stackblitz to play with the re-renders:
- Avoiding unnecessary renders with React context - James K Nelson
- useMemo inside Context API - React - Agney Menon
- 4 options to prevent extra rerenders with React context - Daishi Kato
- How to use React Context effectively - Kent C. Dodds
- How to optimize your context value - Kent C. Dodds
- React Context: a Hidden Power - Alex Khismatulin
- Why React Context is Not a "State Management" Tool (and Why It Doesn't Replace Redux) - Mark Erikson
- Preventing rerenders with React.memo and useContext hook - Dan Abramov
- RFC: Context selectors - Pedro Bern
Key Points
- When the nearest Provider above the component is updated, this component will trigger a re-render even if an ancestor uses
React.memo
orshouldComponentUpdate
. - The value that
React.createContext(...)
is receiving as default will be the one a Consumer will receive if it does not have any Provider above itself in the component tree. - In order to avoid the re-rendering of the entire app (or the use of
memo
), the Provider must receivechildren
as a prop to keep the references equal. - If you implement a Global Provider, no matter what property will be update a Consumer will always trigger a re-render.
- If Parent can be updated (via setState() or even via a grandparent) we must be careful since everything will be re-rendered. We will have to memo both the Provider and the Consumers.
- A Consumer that just changes the state of a Context (by using
setState
ordispatch
) will be re-rendered once the update is performed and the value changes, so it is recommend to split that Context in two: StateContext and DispatchContext. - Remember, React Context does not manage the state, you do it via
useState
oruseReducer
. - Implement a custom mock in order to properly test your Provider,
<Context.Provider {...props} />
is not what your components will directly consume. -
observedBits
is an hidden experimental feature which can helps us to implement a global state avoiding unnecessary re-renders.
That was it, hope you like it!
Posted on August 12, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.