Mastering State Management in React: A Guide to useContext and useReducer Hooks
Emmanuel Fordjour Kumah
Posted on September 30, 2024
In this article, you'll learn how to manage the state in a React app using the Context API and the reducer function. The context API enables access to the state globally without prop drilling. Reducers provides a structured approach to update state logic.
At the end of the tutorial, you will build a contact address book that uses the context API, reducer functions, and React hooks. Here is the code
Prerequisite
To begin, you should be familiar with:
Building a React app
Using
useState
hooks in the React app
Let’s get started!
Overview of state management?
When building complex web applications, you should learn how to manage and synchronize the application's data effectively. This means:
Keeping state organized
Structuring how the state is updated
And how the data flows within the components of your application.
You can use state management libraries like Redux, Mobx, and Zustand to manage. This tutorial focuses on managing the state using the contextAPI
and the reducer
function.
Let’s understand these concepts.
The Context API: Passing data from parent to child components without prop drilling
Generally, we use props to pass data from parent to child components. The challenge is that, if the element requesting data is located further away from the parent, we will pass that data via props through many components in the middle. With this approach, components that do not need access to the data will still have to receive it, and then pass it on to the required component. This is generally referred to as prop drilling. Passing data via props can be cumbersome for certain types of props (e.g User authentication, theme, locale preference) that are required by many components within an application
To avoid prop drilling, you can use the Context API. Context enables a parent component to share values between components without explicitly passing props. This approach, allows the parent to make the data available to any element in the tree below it.
Inserting the state logic into a Reducer
The more complex your components become, the more difficult it becomes to manage the state logic. Reducers combine all the state update logic (e.g. Add contact, Edit contact, Delete contact) in a single function. This means, that when an event is fired within a component, instead of the state update occurring in that component, all the updates will happen in a reducer function.
Combining all the state updates into a single function (reducer) provides a window for you to view all the state updates at a glance.
You have learned what the Context API and reducer do. Next, you will learn how to add the Context API in a React app.
Steps to add context in the React app
Follow these steps, to add the Context API in React:
-
Create the context: First, you create the context by importing the
createContext
method from React. This method provides a context that components can provide or read. It returns anobject
with aSomeContext.Provider
andSomeContext.Consumer
properties which are also components. TheSomeContext.Provider
component lets you give some values to components.The code below indicates how to create a context
//Context.js import { createContext } from 'react'; //create a SomeContext object and export export const SomeContext = createContext(); //properties in the SomeContext object are: // SomeContext.Provider , SomeContext.Consumer which are also components
-
Wrap your components into a Context Provider and pass it some values: Every context you create has a Provider component. The provider component is wrapped around the component tree. It accepts the
value
prop.The code below illustrates a provider component with a
value
prop.
//ceate a context object called SomeContext export const SomeContext = createContext(); // create a provider component for the SomeContext object and pass it a value <SomeContext.Provider value="some context value accessible by all components"> <App/> </SomeContext.Provider>
Defining a component that returns the Provider and using that component to wrap up the main is recommended.
//MyProvider.jsx //Define a component called MyProvider that return the Context Provider component function MyProvider(props) { return ( <SomeContext.Provider value="some context value accessible by all components"> {props.children} </SomeContext.Provider> ); } export default MyProvider // App.js //wrap the MyProvider component around the App component //This ensures the value provided is accessbile by all components. function App(){ return ( <MyProvider> <App/> </MyProvider> ) }
-
Use the context value: To use the context value in any component
- Import the
useContext
hook from React. - Import the created context (e.g.
SomeContext
) - Call the
useContext
hook. It accepts the created context as an argument and returns the context value
- Import the
//MyComponent.jsx
import { useContext } from 'react';
import {SomeContext} from "./Context"
//MyComponent is child component that need access to the context value
const MyComponent = ()=>{
//return the context value
const value = useContext(SomeContext);
return (
<div>The child component has the context value displayed here:{value}</div>
)
}
Congratulations, you know how to use the context API to pass data deeply in the component tree without prop drilling.
Next, let’s learn how the reducer function combines state updates.
Steps to add reducers in your React app
Follow these steps to define the reducer function in your React app:
-
Create a reducer function: The reducer function contains all the logic for the state update. It takes the current state and action as parameters and returns the new state based on the type of action sent by the event handler function in the component
//reducer.js //define the reducer function. It accepts the state and action object function yourReducer(state, action) { // all logic to upate the state will be here if(action.type === "added"){ //perform this state update logic }else if (action.type === "deleted"){ // perform this state update logic } ... // return next state for React to set }
Use the state logic in your component: Import the
useReducer
hook from React to use the reducer function in any component. This hook accepts the reducer function, and the initial state as arguments and returns the current state and a dispatch function. The current state and dispatch will be passed to other components using context.Call the dispatch in the event handler function to update the state: Don’t manage the state in your components. Instead, when an event happens (e.g when you submit user details), call the event handler function, which then dispatches an action (send an action) to the reducer function. The reducer function examines the type of action sent. If the action type sent matches any action type in the reducer it updates the state and returns the latest state.
//insider your function component eg. MyComponent.jsx
//import the useReducer hook and call it inside the component
import {useReducer} from "react"
import yourReducer from "./reducer"
const MyApp = ()=> {
const [state, dispatch] = useReducer(yourReducer, initialState)
//state is the current state of the app
//dispatch allows you to send action to update the state
const handleAddContact(){
//dispatch is a function that will trigger the state changes in the reducer
dispatch({
type: 'added',
id: nextId++,
text: name,
});
}
return (
<div>
<h2>Dispatching action </h2>
<button onClick={handleAddContact}>Click here to dispatch action </button>
</div>
)
}
Here is a summary of the reducer function usage:
Instead of defining the state update logic in the required component, the logic is in the reducer function.
Call the dispatch function in your component and within an event handler function. The dispatch function contains the occurred action.
Based on the action type, the reducer function will update the state and return the latest state.
Combining the Reducer and Context API
Let’s learn how to combine the reducer and context API in our React app.
Use the context API to pass data deeply to child components without prop drilling
Use the reducer function to handle the state update logic
The state is updated based on the action type
The latest state can be accessed with the
useReducer
hook.The state is passed down to the other component using the Context Provider component
The state value is read using the
useContext
hook.
Let’s combine all this knowledge to build our contact address book app.
Project: Building Contact address app
In this section, we will build a contact address app using the Context API and reducer knowledge. The app will enable users to:
Add Contact
Edit contact
Delete contact
Setting up the React app
Set up your React environment using any preferred library and run the app. These are the steps to follow:
Create a
component
folder in yoursrc
directory. All you need in the component folder areForm.jsx
,ContactList.jsx
,ContactDetails.jsx
components. TheForm.jsx
contains the input elements for adding contacts. TheContactList
maps over the array of contacts, and display each contact detail in theContactDetails
component.Create a
context
folder. Define aProvider.jsx
component inside that folder. In theProvider
component create the context and pass your state to thevalue
prop in theContactContext.Provider
component.Create a
reducer
folder. Create acontactReducer.jsx
file inside the folder. Define all the logic to update the state in the reducer functionIn the
App.jsx
component, import theForm
, ContactList
. To make the context value accessible by all components, wrap theProvider
components around the component tree.
Let’s begin with adding context to our app.
Adding Context to our app
First, we will create the ContactContext
in the Provider
component. Follow these steps:
Import
createContext
API from ReactCreate and export the
ContactsContext
//component/context/Provider.jsx
import { createContext } from "react";
export const ContactsContext = createContext(null); // context with a default value of null
The ContactsContext
provide access to the ContactsContext.Provider
component. In the value
prop, you will pass the data to access in the component tree.
//Here the provider component accepts a string as value though it can accept object
<ContactsContext.Provider value="some value">
{children}
</ContactsContext.Provider>
Let’s declare the Provider
component which accepts children
as props and returns the ContactsContext.Provider
. Go ahead and add the code below in the Provider.jsx
file
// context/Provider.jsx file
//code above remains the same
import { createContext} from "react";
import {useState} from "react"
//create the context
export const ContactsContext = createContext(null);
//declare the Provider component which returns the ContactsContext.Provider component
const Provider = ({ children }) => {
//The data to pass to the component tree is in a state variable
const [state, setState] = useState('some value')
return (
<>
<ContactsContext.Provider value={state}>
{children}
</ContactsContext.Provider>
</>
);
};
export default Provider;
Wrap the Provider
component around your components.
Next, we will wrap the Provider
component around the Form
and ContactList
components. This ensures the values assigned to ContactsContext.Provider
is accessed by all child components inside the App.jsx
//App.jsx
import "./styles.css";
import Provider from "./context/Provider";
import Form from "./component/Form";
import ContactList from "./component/ContactList";
export default function App() {
return (
<div className="App">
<Provider>
<Form />
<ContactList />
</Provider>
</div>
);
}
Access the context value with the useContext
hook
Now, we want to access the context value in the ContactList
component. But first, let’s modify the Provider.jsx
component and pass the initialContact
as the state variable
//Provider.jsx
import { createContext } from "react";
export const ContactsContext = createContext(null);
//define an initial state for the app
const initialContact= {
contacts: [
{
id: Math.floor(Math.random() * 10),
name: "Emmanuel Kumah",
phone: "0244234123",
},
],
editingContact: null,
};
const Provider = ({ children }) => {
// pass the initial contact as the state variable
const [state, setState] = useState(initialState)
//pass the state as the context value
return (
<>
<ContactsContext.Provider value={state}>
{children}
</ContactsContext.Provider>
</>
);
};
export default Provider;
To use the context value in the ContactList
component:
Import
ContactsContext
. This is the context you created withcreateContext
API in theProvider.jsx
fileImport the
useContext
hook from React.Call the
useContext
hook and pass theContactsContext
as an argument.
This returns the state
. We loop through the array and display details in the SingleContact
component.
//components/ContactList.jsx
//import the useContact hook
import { useContext} from "react";
import SingleContact from "./SingleContact";
//import the created context
import { ContactsContext } from "../context/Provider";
const ContactList = () => {
//call the useContext hook and pass the ContactsContext
const state = useContext(ContactsContext);
return (
<div>
<section className="contacts">
<h3>Your Address book </h3>
{state.length === 0 ? (
<p>Start Add contacts</p>
) : (
<ul>
{state.map((contact) => (
<div key={contact.id}>
<SingleContact key={contact.id} contact={contact} />
</div>
))}
</ul>
)}
</section>
</div>
);
};
export default ContactList;
The app should look something like this:
Now, it doesn't matter how many layers of components you have in your app. When any component calls the useContext(ContactsContext)
, it will receive the context value ( the state)
Add the reducer logic
In the previous section, you learned how to add and access the context value in the ContactList
component. In this section, we will learn how to manage state logic using the reducer and pass the updated state to the components using the context.
Let’s create a contactReducer.jsx
file in our reducer folder. We will define a reducer function in this file. Previously, we learned that a reducer function accepts the current state and action as arguments and returns the updated state based on the action type.
Let’s define the reducer function
export const contactReducer = (state, action )=>{
//return the next state for React to set
switch(action.type){
}
}
We will dispatch three action types to the reducer function. These are
Add_Contact
Delete_Contact
Edit_Contact
Because we want to handle all the state logic in a single function, we will use a switch
statement to handle each action
object. Dispatched actions have a type
property to indicate the type of action dispatched.
Let’s see how to handle each action.type
If an
action.type
matches theAdd_Contact
, we will define the logic to add a new contact to the state and return the updated stateIf an
action.type
matches theDelete_Contact
, we will define the logic to remove that contact from the state and return the updated stateIf an
action.type
matches theEdit_Contact
, we will edit the contact details and return the updated stateIf the dispatched
action.type
does not match any of the cases, we will return the state.
Here is the complete code for the contactReducer
function and
//contactReducer.jsx
export const contactReducer = (state, action) => {
switch (action.type) {
case "Add_Contact": {
// handle the logic for adding a contact
return {
...state,
contacts: [...state.contacts, { id: Date.now(), ...action.payload }],
editingContact: null,
};
}
case "Delete_Contact": {
//handle the logic for deleting a contact
return {
...state,
contacts: state.contacts.filter(
(contact) => contact.id !== action.payload
),
editingContact: null,
};
}
case "Edit_Contact": {
//handle the logic for editing a contact
return {
...state,
contacts: state.contacts.map(
(contact) => {
if (contact.id === action.payload.id) {
return action.payload;
} else {
return contact;
}
}
),
editingContact: null,
};
}
case "SetEdit_Contact": {
return {
...state,
editingContact: action.payload,
};
}
default: {
throw Error("Unknown action", action.type);
}
}
};
Use the reducer from your component
We defined the logic to update the state, now we want to manage this state in our app. We will use the useReducer
hook. It is similar to useState
, but it is designed for managing a more complex state.
The useReducer
hook accepts two arguments:
A reducer function
An initial state
And it returns:
A stateful value ( a state)
A dispatch function. To assist in dispatching the actions to the reducer.
We will use the state
in the Context.Provider
component. This is to allow all the components access to the state.
Let’s see how to accomplish our task.
In the
Provider
component, import theuseReducer
hook andcontactReducer
function.Call the
useReducer
hook and pass thecontactReducer
andinitialState
This will return the stateful value and the dispatch function
Here is how to achieve that:
//Provider.jsx
import { createContext, useReducer } from "react";
import { contactReducer } from "../reducer/contactReducer";
//define the initial state
const initialState = {
contacts: [
{
id: Math.floor(Math.random() * 10),
name: "Emmanuel Kumah",
phone: "0244234123",
},
],
ed
const Provider = ({ children }) => {
//use the useReducer hook
const [state, dispatch] = useReducer(contactReducer, initialState);
//state contains the initial state
//dispatch enables you to update state
return (
...
)
};
Pass the state
and dispatch
function as context value
Lastly, we will pass the state
and dispatch
as an object to the value
prop of the Contacts.Provider
component. This will enable all the children
component access these values
Here is the code below
//Provider.jsx
const Provider = ({ children }) => {
//manage state with reducers
const [state, dispatch] = useReducer(contactReducer, initialState);
//pass state and dispatch to the provider component
return (
<>
<ContactsContext.Provider value={{ state, dispatch }}>
{children}
</ContactsContext.Provider>
</>
);
};
Here is the complete code:
import { createContext, useReducer } from "react";
import { contactReducer } from "../reducer/contactReducer";
export const ContactsContext = createContext(null);
const initialState = {
contacts: [
{
id: Math.floor(Math.random() * 10),
name: "Emmanuel Kumah",
phone: "0244234123",
},
],
editingContact: null,
};
const Provider = ({ children }) => {
//manage state with reducers
const [state, dispatch] = useReducer(contactReducer, initialState);
return (
<>
<ContactsContext.Provider value={{ state, dispatch }}>
{children}
</ContactsContext.Provider>
</>
);
};
export default Provider;
Dispatching actions in your components
Finally, let’s dispatch actions to the reducer to update the state.
We have a reducer function (contactReducer
) which contains all the state update logic. This means our component would be free of state update logic. It will mainly contain event handlers which dispatch an action object anytime an action occurs.
The Form.jsx
component holds the form elements. In this component, we will listen for a change in the input fields and update the name
and phone
state. On submission, we will dispatch the Add_Contact
action to the reducer function. The reducer will check if the action type( Add_Contact) matches any case. If it does, it updates the state by adding the new contact.
In the Form.jsx
, we access the dispatch
function using the useContext
hook. Thedispatch
accepts an object with type
and payload
as properties.
The
type
key enables you to specify the type of action that occurredThe
payload
holds any data to pass to the reducer.
Here is the code for dispatching an action on form submission
const onFormSubmit = (e) => {
e.preventDefault();
if (state.editingContact) {
dispatch({
type: "Edit_Contact",
payload: { id: state.editingContact.id, name, phone },
});
} else {
dispatch({
type: "Add_Contact",
payload: {
id: Date.now(),
name,
phone,
},
});
}
//clear input fields
setName("");
setPhone("");
};
In the code above, If the
state.editingContact
is true, it indicates we want to edit a contact. Hence we dispatch aEdit_Contact
action type, and pass the edited contact details to thepayload
property.Else we dispatch a
Add_Contact
action type, and pass the details of the contact to thepayload
property.
Here is the complete code for the Form.jsx
component
//Form.jsx
import { useState, useEffect, useContext } from "react";
import { ContactsContext } from "../context/Provider";
const Form = () => {
const [name, setName] = useState("");
const [phone, setPhone] = useState("");
const { state, dispatch } = useContext(ContactsContext);
useEffect(() => {
if (state.editingContact) {
setName(state.editingContact.name);
setPhone(state.editingContact.phone);
}
}, [state.editingContact]);
const handleInputChange = (e) => {
const { name, value } = e.target;
setContactDetails((prevState) => ({ ...prevState, [name]: value }));
};
const onFormSubmit = (e) => {
e.preventDefault();
if (state.editingContact) {
// dispatch Edit_Contact if you are editing
dispatch({
type: "Edit_Contact",
payload: { id: state.editingContact.id, name, phone },
});
} else {
// dispatch Add_Contact if we want to add a new contact
dispatch({
type: "Add_Contact",
payload: {
id: Date.now(),
name,
phone,
},
});
}
//clear input fields on form submission
setName("");
setPhone("");
};
return (
<>
<section className="formSection">
<h2 className="heading">Create your buddy list</h2>
<form onSubmit={onFormSubmit}>
<input
type="text"
name="fullname"
placeholder="Enter name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<input
type="text"
name="phone"
id=""
placeholder="mobile"
value={phone}
onChange={(e) => setPhone(e.target.value)}
/>
<button className="btnAdd" type="submit" disabled={name === ""}>
{`${state.editingContact ? "Update" : "Save"}`}
</button>
</form>
</section>
</>
);
};
export default Form;
Finally, in the ContactDetails
component, we will dispatch the Delete_Contact
type to delete a contact, and dispatch the Set_EditContact
if we are editing a contact.
Take a look at the code below:
import { useContext } from "react";
import { ContactsContext } from "../context/Provider";
const ContactDetails = ({ contact }) => {
const { dispatch } = useContext(ContactsContext);
const handleEdit = () => {
dispatch({
type: "SetEdit_Contact",
payload: contact,
});
};
const handleDelete = () => {
dispatch({
type: "Delete_Contact",
payload: contact.id,
});
};
return (
<>
<div className="contact">
<h3 className="contactName">Name: {contact.name}</h3>
<p>Contact:{contact.phone}</p>
<div className="actionBtn">
<button className="btnContact" onClick={handleEdit}>
Edit
</button>
<button className="btnContact" onClick={handleDelete}>
Delete Contact
</button>
</div>
</div>
</>
);
};
export default ContactDetails;
Conclusion
Congratulations, you have learned how to use context API and the reducer hook. You have learned that:
Context enables a parent component to share values between components without explicitly passing props
Reducers combine all the state update logic (e.g. Add contact, Edit contact, Delete contact) in a single function. This means, that when an event is fired within a component, instead of the state update occurring in that component, all the updates will happen in a reducer function.
useContext
is a React Hook that lets you read and subscribe to context from your componentuseReducer
is a React Hook that lets you add a reducer to your component
Posted on September 30, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
September 30, 2024