Motivation
Why use Context to handle React state? The same reason you use Redux to manage your application store. If your components talk to one another and lifting state becomes cumbersome, then Context is a native way to deal with your application state.
Prerequisites
We will be refactoring <App>
—which uses local state—to use Context:
// App.js
import { useState } from 'react';
export default function App() {
const [state, setState] = useState({ count: 0 });
function increment() {
setState({
count: state.count + 1,
});
}
function decrement() {
setState({
count: state.count - 1,
});
}
return (
<>
<p>{state.count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</>
);
}
As you can see, there’s a counter with 2 buttons that increments or decrements the count.
Provider
Create <Provider>
and set prop value
to 0:
// Provider.js
import { createContext } from 'react';
export const Context = createContext();
export default function Provider(props) {
return <Context.Provider value={0}>{props.children}</Context.Provider>;
}
Render <Provider>
as the top-level component in index.js
:
// index.js
import { render } from 'react-dom';
import App from './App';
import Provider from './Provider';
render(
<Provider>
<App />
</Provider>,
document.getElementById('root')
);
Get the Provider value
with useContext
in <App>
:
// App.js
import { useContext } from 'react';
import { Context } from './Provider';
export default function App() {
const value = useContext(Context);
return (
<>
<p>{value}</p>
<button>+</button>
<button>-</button>
</>
);
}
useReducer
Create a reducer function:
// reducer.js
export default function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return {
count: state.count + action.payload,
};
case 'DECREMENT':
return {
count: state.count - action.payload,
};
default:
throw new Error();
}
}
Pass state and dispatch from useReducer
to Provider prop value
:
// Provider.js
import { createContext, useReducer } from 'react';
import reducer from './reducer';
export const Context = createContext();
const initialState = {
count: 0,
};
export default function Provider(props) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<Context.Provider value={[state, dispatch]}>
{props.children}
</Context.Provider>
);
}
Update <App>
with the new context value:
// App.js
import { useContext } from 'react';
import { Context } from './Provider';
export default function App() {
const [state, dispatch] = useContext(Context);
function increment() {
dispatch({
type: 'INCREMENT',
payload: 1,
});
}
function decrement() {
dispatch({
type: 'DECREMENT',
payload: 1,
});
}
return (
<>
<p>{state.count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</>
);
}
Consumer
An alternative to useContext
is Consumer.
First, export Consumer
from Context:
// Provider.js
export const Context = createContext();
export const { Consumer, Provider } = Context;
Then replace useContext
with <Consumer>
:
// App.js
import { Consumer } from './Provider';
export default function App() {
return (
<Consumer>
{([state, dispatch]) => (
<>
<p>{state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT', payload: 1 })}>
+
</button>
<button onClick={() => dispatch({ type: 'DECREMENT', payload: 1 })}>
-
</button>
</>
)}
</Consumer>
);
}
<Consumer>
requires a callback function for prop children
. The 1st argument is the Provider value
.
Class
For class components, the Provider value can be accessed on this.context
:
class App extends Component {
render() {
const [state, dispatch] = this.context;
// ...
}
}
Todo
Ideas for improvements:
- Build a custom hook for
useContext(Context)
. - Refactor action types to constants.
- Create action creators.
- Set
displayName
on Context to improve visibility in React DevTools.