TL;DR
// models/IAuthDatum.ts
export interface IAuthDatum {
token?: string;
}
// components/AuthProvider/index.tsx
const defaultAuthDatum: IAuthDatum = { token: undefined };
export const AuthContext = createContext<{
authState: IAuthDatum;
setAuthState: Dispatch<SetStateAction<IAuthDatum>>;
}>({
authState: defaultAuthDatum,
setAuthState: () => {},
});
export const AuthProvider: React.FunctionComponent = ({ children }) => {
const [authState, setAuthState] = useState(defaultAuthDatum);
return <AuthContext.Provider value={{ authState, setAuthState }}>{children}</AuthContext.Provider>;
};
// index.tsx
ReactDOM.render(
<DecksProvider>
<AppEntry />
</DecksProvider>
document.getElementById('root'));
// pages/home/index.tsx
export const Home: NextPage = () => {
const { authState, setAuthState } = useContext(AuthContext);
console.log(authState);
return (
<div onClick={() => setAuthState({ token: "token set!" })}>
{"Token value: "+authState.token}
</div>
);
};
Intro
I tried context once and didn’t especially like it. Part of the supposed appeal is that it is built into React and therefore ostensibly easier to set up than redux. However, I found the setup to involve lots of parts, such that it felt negligibly less complex than redux.
Anyhow, I’ve decided to give it another go only this time I will try to actually understand what is going on — and spell it out here — rather than just copy/paste boilerplate code.
Focus on Hooks
The official docs introduce you to context but only, it seems, with older “class-centric” (or “no-hooks”) react patterns. I have no interest in class-centric react at this point, so I had to track down a separate tutorial that focuses entirely on hooks. The first one I found from Google by Dave Ceddia was great! The rest of this article is very much me rehashing what Dave wrote there for my own long-term memory benefits; if you’re here to learn about context, you might well want to you go there.
I quickly realized that the issues I had had with context, like so many things in life, is that I started with something complex, whereas you need to start with something simple to really get what is going on.
So What’s Going On?
In the original class-centric way of doing things, and starting off super simple, you create and use a react Context like so:
import React from "react";
import ReactDOM from "react-dom";
// Create a Context
const NumberContext = React.createContext(42);
// It returns an object with 2 values:
// { Provider, Consumer }
function App() {
// Use the Provider to make a value available to all
// children and descendants
return (
<NumberContext.Provider value={42}>
<div>
<Display />
</div>
</NumberContext.Provider>
);
}
function Display() {
// Use the Consumer to grab the value from context
// Notice this component didn't get any props!
return (
<NumberContext.Consumer>
{value => <div>The answer is {value}.</div>}
</NumberContext.Consumer>
);
}
ReactDOM.render(<App />, document.querySelector("#root"));
The key thing here is that the Context gives you two higher-order components: the Provider and the Consumer. In its simplest usage, you feed a value to the Provider, and then that value is made available to your Consumer as illustrated in the above code. The Consumer will trigger a re-render of its children whenever the value of the context changes. (How to sensibly update the value of the Context is discussed below.)
It’s also important to understand the difference between the two places where the value of the Context is being set: the argument passed to React.createContext(), and the prop labelled “value” passed to the Provider. According to the official documentation:
The
– ReactJs.orgdefaultValueargument [passed to React.createContext] is only used when a component does not have a matching Provider above it in the tree. This default value can be helpful for testing components in isolation without wrapping them.
In other words, you can use the Consumer of a context without its Provider, but my understanding is that this will only let you access the original “default” value. If you want to be able to update the value of the Context then you need to use the Provider.
To summarize so far:
- Think of the thing that gets created by
React.createContext(value)as being the external “store” of data that you export to your app in order to equip any given component with either aProvideror aConsumerof that value. - The
Consumerwill trigger a re-render of its children whenever the value of its context changes. - In practice, you always need to use the
Providerof theContextin order to update the value of theContext; this makes the default value passed to theReact.createContext()function essentially redundant/obsolete, and you will therefore often seen this default value left out or given a bogus/placeholder data structure.
useContext
The above “class-centric” pattern is ugly. The Consumer does not have any props, and the variable “value” has to be understood as defined within it. Thankfully, we don’t need to use this pattern thanks to the useContext hook.
// import useContext (or we could write React.useContext)
import React, { useContext } from 'react';
// ...
function Display() {
const value = useContext(NumberContext);
return <div>The answer is {value}.</div>;
}
This is much nicer: now we don’t need to wrap components with the Consumer component, and the variable value is declared explicitly and we can therefore call it whatever we like.
Updating Context Within Nested Components
As we just saw, one sets/updates the value of the Context via the prop named “value” passed to the Provider component. This fact is key to understanding how we can update the value of the Context from a component nested within a Consumer of that Context (viz. a React.FC using the useContext hook).
The official documentation gives an example of how to achieve this by storing the latest value of the Context within the state of the top-level component that renders the Provider, as well as a callback function within that top-level component that will update that state. The state and callback are then passed within a single object to the prop labelled “value” of the Provider (thus setting the value of the Context).
The nested component then extracts the object with the state and callback from the Context using the useContext hook. The callback can be triggered from the nested component, causing the state of the top-level component to update, causing the Provider to re-render, causing the value of the Context to change, causing the nested component to re-render.
This is all well and good, except that it would be much nicer to abstract the management of state out of the top-level component and into one or more files that not only define the Context, but also the manner in which its value can be updated.
We can achieve this by extracting the Provider into a component dedicated to this very purpose, so that our top-level component appears to wrap the rest of the app more neatly.
const MyContext = React.createContext({
state: defaultState,
setState: () => {}
});
const { Provider } = MyContext;
const MyContextProvider = ({ children }) => {
const [state, setState] = useState(0);
return (
<Provider value={{state, setState}}>
{children}
</Provider>
);
};
const MyContextConsumer = () => {
const {state, setState} = useContext(MyContext);
return (
<>
<h1> {"Count: " + state} </h1>
<button onClick={()=>setState(prev => prev+1)}>
Click to Increase
</button>
</>
);
};
const App = () => {
return (
<MyContextProvider>
<MyContextConsumer />
</MyContextProvider>
);
}
An important note to stress about this code is that you have in effect two “stores” of information. The information is first stored in the state of a component, and then it is fed to the Context via its Provider. The Consumer component will then get the state combined with a callback as a single object (the ‘value’) from the Context, and use that value as a dependency in its (re-)rendering. Once you understand this fact — that for Context to really be effective you need to couple it with useState (or its alternatives like useReducer) — you will understand why it is often said that Context is not a state-management system, rather, it is a mechanism to inject data into your component tree.
In summary, in practice, you need to keep conceptual track of the “state” as stored in a near-top-level component that wraps the Provider versus the “state” passed to/from the Context, and onto the Consumer.
That’s it — if you can follow these concepts as illustrated in the above code, then you have the essential concepts of React Context. Hurray!
The remainder of this article discusses further important patterns that build off of this knowledge.
Context with useReducer
Since Context is often seen as a replacement for redux, one will likely encounter useReducer instead of useState. Like useState, useReducer returns a state and a function to update the state.
const [state, setState] = useReducer(reducer, initState);
Unlike useState, the useReducer function takes two arguments. The second argument is the initial state that you wish to keep track of. The first argument is a reducer function that maps a previous state and an action to a new state. The action, as with redux, is an object of the form:
{
type: "ACTION_NAME", // Required string or enum entry
payload: ... // Optional data structure
}
By common convention, a reducer function is almost always a switch that returns a new state for different values of action.type. E.g.:
export const myReducer = (prevState, action) => {
switch (action.type) {
case "SET_STH":
return {
...prevState,
sth: [...action.payload]
};
case "ADD_STH_ELSE":
return {
...state,
sthElse: state.sthElse + action.payload
};
default:
throw new Error('Unknown action: ' + JSON.stringify(action));
}
};
Notice that, as with redux, we need to always return a new object in our reducer function when we update the state in order for useReducer to trigger re-renders.
The items returned by useReducer acting on your reducer function and initial state are then placed in an object that is used to set the value of the Context Provider. A wrapped-provider component can thereby be take the following form:
const { Provider } = MyContext;
export const MyContextProvider = ({ children }) => {
const [state, setState] = useReducer(myReducer, initState);
return (
<Provider value={{state, setState}}>
{children}
</Provider>
);
};
By convention, the function returned by useReducer (setState above) is often called ‘dispatch’.
Context and Memoization
Another important concept in redux is that of the ‘selector’. Suppose your app needs to track state of the form state: {A:IA, B:IB, C:IC}. Suppose that state gets updated frequently, that you have a component that only depends on state.C, and that you do not want it to re-render when only state.A and/or state.B get updated. As described in this answer, there are three ways that you can improve performance in such a case:
- Split your Context so that e.g.
state.Cis its own separate state - Split the component that depends on C into two components: the first uses
useContextto get the new value of C and then passes that as a prop to a separate component wrapped inReact.memo - Take the jsx to be returned by the component and wrap it in a function that is itself wrapped in
useMemowith C in the array of dependencies.
You might also consider creating two different Contexts for a single reducer: one to pass through the state, the other to pass through the dispatch function. (This way, any time the state is updated, components that only use the dispatch function — but not the state — will not re-render since the value of their Context never changes.)
Another pattern I have encountered is to wrap the useReducer with a separate hook that executes more nuanced logic, such as also reading/writing to localstorage, and then using this hook within the Provider component.
In short, think hard about what state each Context is responsible for, and consider splitting or memoizing to avoid expensive re-renderings.
Context and Side Effects
No conversation on state management is complete without considering API calls and ‘side effects’. Often we might want to trigger a series of actions such as fetching data from an API, updating the state, then fetching data from another API, etc.
One normally supplements redux with a library like redux-sage, redux-thunk or redux-observable. These allow you to set up the triggering of actions or side effects that can trigger yet more actions or side-effects in a potentially endless cascade of events. These “redux middleware” solutions are also nice in that they help keep your centralized state-management logic separate from your component-state logic.
As far as I can tell, such separation of concerns is not readily accommodated with Context. Instead, you need to intwine the logic to control such cascades of events within your components using useEffect (ultimately).
For example, suppose that upon loading the site you want to check if a user is logged in and, if so, fetch some data and then, based on that data, decide whether to display a message with data fetched from another API.
One way to do this is to create a component that will show a login form if the user is not logged in (based on a boolean from the Context value), or an image if the user is logged in. On clicking the submit button for the login form the component executes a fetch to the api and then updates the Context with the returned data. This triggers a rerender of the component, which uses a useEffect listening for changes to the login boolean that issues another fetch to an API, and uses that data to update the Context again. This final update to the Context triggers another rerendering of components that can easily control the display of a message.
This interplay between components and Context is straightforward enough to understand, though one might be wary of having all of the cascading logic “scattered” around the components with useEffect calls.
One could imagine trying to foist all of the side-effect logic within one or more custom hooks within the Provider component. I have not tried that yet in any serious way, so may revisit this article in the future after experimenting further.
My feeling for now though is that trying to cram the equivalent of redux-observable-like logic within hooks within the provider component will result in something rather convoluted. For this reason, I think I understand why it is said that Context is best-suited for small to medium-size apps here one can indeed manage such “scattering” of the logic to maintain the app’s centralized state within the Consumer components. If your app is very large and complicated, then redux-observable might well be the way to go.





