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
useStatehooks 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
createContextmethod from React. This method provides a context that components can provide or read. It returns anobjectwith aSomeContext.ProviderandSomeContext.Consumerproperties which are also components. TheSomeContext.Providercomponent 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
valueprop.The code below illustrates a provider component with a
valueprop.
//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
useContexthook from React. - Import the created context (e.g.
SomeContext) - Call the
useContexthook. 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
useReducerhook 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
useReducerhook.The state is passed down to the other component using the Context Provider component
The state value is read using the
useContexthook.
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
componentfolder in yoursrcdirectory. All you need in the component folder areForm.jsx,ContactList.jsx,ContactDetails.jsxcomponents. TheForm.jsxcontains the input elements for adding contacts. TheContactListmaps over the array of contacts, and display each contact detail in theContactDetailscomponent.Create a
contextfolder. Define aProvider.jsxcomponent inside that folder. In theProvidercomponent create the context and pass your state to thevalueprop in theContactContext.Providercomponent.Create a
reducerfolder. Create acontactReducer.jsxfile inside the folder. Define all the logic to update the state in the reducer functionIn the
App.jsxcomponent, import theForm, ContactList. To make the context value accessible by all components, wrap theProvidercomponents 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
createContextAPI 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 withcreateContextAPI in theProvider.jsxfileImport the
useContexthook from React.Call the
useContexthook and pass theContactsContextas 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_ContactDelete_ContactEdit_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.typematches theAdd_Contact, we will define the logic to add a new contact to the state and return the updated stateIf an
action.typematches theDelete_Contact, we will define the logic to remove that contact from the state and return the updated stateIf an
action.typematches theEdit_Contact, we will edit the contact details and return the updated stateIf the dispatched
action.typedoes 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
Providercomponent, import theuseReducerhook andcontactReducerfunction.Call the
useReducerhook and pass thecontactReducerandinitialStateThis 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
typekey enables you to specify the type of action that occurredThe
payloadholds 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.editingContactis true, it indicates we want to edit a contact. Hence we dispatch aEdit_Contactaction type, and pass the edited contact details to thepayloadproperty.Else we dispatch a
Add_Contactaction type, and pass the details of the contact to thepayloadproperty.
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.
useContextis a React Hook that lets you read and subscribe to context from your componentuseReduceris a React Hook that lets you add a reducer to your component





Top comments (0)