With React 16.8 came the release of Hooks, which, along with other great functions, provides a way for function components to have a state!
In my article Managing State with React Hooks I go over how to use the hooks
useState
anduseReducer
to manage state within function components. Give this a read if you need a refresher on how hooks can help manage state.
In this article we’ll take a look at a way to use hooks along with React’s Context API
to provide a shared state between function components!
Create and Provide a Context
While the Context API itself is not a part of hooks, using it along with the useReducer
and useContext
hooks, we can provide a reducer and state globally (or to specific component trees, depending on how you implement it) in our application.
Not familiar with React’s Context API? Check out their documentation explaining what a Context is and what it can accomplish.
Let’s look at a simple example of how this would work!
First off, we’ll create a Context that will eventually provide the state
and dispatch function
of a reducer to the entire application. That way the entire application will be provided a way to access the reducer and state.
import React, { createContext, useReducer } from 'react'
export const StateContext = createContext()
Build a component that provides the Context value to its children
Okay, so we have a Context created that will eventually hold the state
and dispatch
. How do we provide this to the entire application? We’ll need to wrap the entire application in the Provider
we received from the Context. Let’s create a wrapper we can stick our app into that will provide the context value (the state and dispatcher) to its children.
import React, { createContext, useReducer } from 'react'
export const StateContext = createContext()
// This is a component that wraps all of its children in the Provider
export const StateProvider = ({ reducer, initialState, children }) => (
<StateContext.Provider value={useReducer(reducer, initialState)}>
{ children }
</StateContext.Provider>
)
The code above is doing a few things:
- It exports a new component named
StateProvider
that will take in a reducer, an initial state, and children as props. TheStateProvider
component wraps all of its children in theProvider
we grabbed from the StateContext we created. - We pass the reducer and initial state from this component’s props to the
useReducer
hook. - The Provider accepts a prop named
value
. This value is the data that is provided to all of thechildren
. TheuseReducer
hook returns an array with two items: the current state, and a dispatch function. These are the values we are passing into thevalue
prop, thus providing those to all ofStateProvider
‘s child components.
Wrap the whole App in the StateProvider
component
Awesome! We now have a component that will wrap all the children in a Provider that supplies them with the state
and dispatch
we require to share state across the entire application. Let’s wrap an app in this component so the children can consume the Context it provides.
import React from 'react';
import { StateProvider } from './State'
function StateApp() {
// Initialize the state
const initialState = {
points: 0
}
// Defines how to update the state based on actions
const reducer = (state, action) => {
switch (action.type) {
case 'add':
return { ...state, points: state.points + 1 }
default:
return state;
}
};
return (
<StateProvider initialState={initialState} reducer={reducer}>
// Application
</StateProvider>
)
}
export default StateApp
Here we create an initial state and a reducer to manage that state. We then wrap the app in our StateProvider
component, passing in the initialState
and reducer
. As we saw above, StateProvider
uses those to provide all of the children with state
and dispatch
. The result is global access to state!
Consume the Context in a Child Component
Now let’s see how a child component would access and update the global state!
In the past, if you wanted to access a Context you would need to wrap your component in that Context’s
Consumer
. Using the newuseContext
hook allows us to access the context without using the Consumer.
import React, { useContext } from 'react'
import { StateContext } from '../State'
function Points() {
// Here, we get the state and dispatch from the context
const [ { points }, dispatch ] = useContext( StateContext )
return (
<>
{/* Clicking this button runs the global dispatch we got from the Context */}
<button onClick={() => dispatch({type: 'add'})}>Add</button>
<p>{ points }</p>
</>
);
}
export default Points
In order to access the global state we need to import the useContext
hook and the StateContext
we created.
We use the useContext
hook to get access to the data provided by a context. This hook takes one argument, a Context, and returns the current context value. In our case, the context value is the return value of our useReducer
function which contains an array of two items. The first being the current state and the second a dispatch function.
NOTE: I am using Destructuring to pull out only the
points
key from the state and to pull the items out of the array.
When the Add button is clicked, the global dispatch
provided by the Context is fired, resulting in an increment of the global state’s points
key value.
Neat! This component is now consuming and updating a global state that can be shared with other components within the StateProvider
component’s component tree.
Putting it all together
Here is a full example of all of this at work. Below is the App.js
file which will wrap its children in the StateProvider
.
import React from 'react';
import { StateProvider } from './contexts/State'
import GlobalStateCounter from './components/GlobalStateCounter'
import './App.scss'
function App() {
// Initialize the state
const initialState = {
points: 0
}
// Defines how to update the state based on actions
const reducer = (state, action) => {
switch (action.type) {
case 'add':
return { ...state, points: state.points + 1 }
case 'subtract':
return { ...state, points: state.points - 1 }
case 'reset':
return { ...state, points: 0 }
default:
return state;
}
};
return (
<div className="App">
<StateProvider initialState={initialState} reducer={reducer}>
{
// Creates two instances of the component so we can see both update when state changes
[...Array(2)].map((e, i) => <GlobalStateCounter index={i + 1}/> )
}
</StateProvider>
</div>
)
}
export default App
And here is the child component that uses the useContext
hook to access the StateContext
‘s value, which is an array containing the state
and a dispatch
function.
import React, { useContext } from 'react'
import { StateContext } from '../State'
import '../assets/scss/Points.scss'
function Points({index}) {
const [ { points }, dispatch ] = useContext( StateContext )
return (
<div id="Points">
<p className="title">Points (globalState {index})</p>
<hr className="divider"/>
<div className="pointsContainer">
<div className="buttons">
{/* These buttons use the dispatch to update the state */}
<button className="button add" onClick={() => dispatch({type: 'add'})}>Add</button>
<button className="button subtract" onClick={() => dispatch({type: 'subtract'})}>Subtract</button>
<button className="button reset" onClick={() => dispatch({type: 'reset'})}>Reset</button>
</div>
<div className="outputBox">
{/* Output the points variable */}
<p>{ points }</p>
</div>
</div>
</div>
);
}
export default Points
Using React’s Context API and hooks we have implemented global state management! This is a basic example, but a strong foundation for a more complex and organized state management system.
Conclusion
Using this structure, you can provide access to a global state across your entire application! This can act as a replacement for other forms of state management like Redux
. More complexity can be added to this to make it more versatile and organized, but the general idea provides a great base to start managing shared states in your function components!
Thanks for reading!