How to manage state in React apps with useReducer and useContext hooks
Choosing a state management library to manage and handle a global state in a React app can be tricky and time-consuming. A lot depends on the scope of the React app and there are many options available.
With the adaption of React Hooks API, one such option is to use a combination of useReducer
hook and the Context API. In this post, let's take a look at how to manage global state in a React app using both of them.
Prerequisites
🔗To take full advantage of this tutorial, or run along with the example, please make sure you following installed/access to in your local development environment.
- Node.js version >=
12.x.x
installed - have access to one package manager such as
npm
oryarn
create-react-app
cli installed or usenpx
- basics of React Hooks
If you are not familiar with React Hooks, I recommend you to go through the in-depth post on React hooks here.
State management in React apps with useReducer
🔗There are two types of states to deal with in React apps. The first type is the local state that is used only within a React component. The second type is the global state that can be shared among multiple components within a React application.
With the release of Context API as well as Hooks API, implementing a global state is possible without installing any additional state management library. The useReducer
hook is a great way to manage complex state objects and state transitions. You may have seen or used useState
to manage simple or local state in React apps.
The useReducer
hook is different from useState
. The main advantage it has over useState
is that covers the use case when there is a need of handling complex data structures or a state object that contains multiple values. It updates the state by accepting a reducer function and an initial state. Then, it returns the actual state and a dispatch function. This dispatch function is used to make changes to the state.
Create a new React app & installing dependencies
🔗To get started, create a new React project by executing the following command in a terminal window:
npx create-react-app react-expense-trackercd react-expense-tracker
To focus on the main topic of this tutorial as well as to give the demo app a nice look and feel, let's use pre-defined components from Reactstrap. It provides Bootstrap 4 components that are based on Flexbox and useful to handle the layout of a web app. To get started using Bootstrap in a React app, install the following dependencies:
yarn add bootstrap@4.5.0 reactstrap@8.5.1 uuid@8.2.0
After installing these dependencies, open the React project you created and open the file index.js
. Add an import statement to include the Bootstrap CSS file.
1// after other imports2import 'bootstrap/dist/css/bootstrap.min.css';
That's it to set up Bootstrap in the current React app.
Defining a global state
🔗Start by creating a new file called GlobalState.js
inside the src/
directory.
Let's use React's context API to create a Context provider that can is going to share the state across multiple components. You can think of this example as mimicking Redux' philosophy. Import the required statements.
1import React, { useReducer, createContext } from 'react';2import { v4 as uuid } from 'uuid';
Next, create an empty context for Expense and define an initial state object. This initial state is going to have one expense item present. This also helps to define a schema or data model for all other expense items (but do note that this for demonstration purpose in context to this post).
1export const ExpenseContext = createContext();23const initialState = {4 expenses: [5 {6 id: uuid(),7 name: 'Buy Milk',8 amount: 109 }10 ]11};
Then define a function called reducer
. It is going to take two arguments, the current state and action. This reducer's job is to modify or update the state object whenever there is an action taken within the app by the user. One example of an action is a user adding an expense.
For the following example, this reducer
function is going to have one action type, which is to add the expense. If there are no changes or modifications, this reducer
function is going to return the current state (which is the default case).
1const reducer = (state, action) => {2 switch (action.type) {3 case 'ADD_EXPENSE':4 return {5 expenses: [...state.expenses, action.payload]6 };7 default:8 return {9 state10 };11 }12};
Next, define an ExpenseContextProvider
that is going to behave like a store (as a store in Redux).
1export const ExpenseContextProvider = props => {2 const [state, dispatch] = useReducer(reducer, initialState);34 return (5 <ExpenseContext.Provider value={[state, dispatch]}>6 {props.children}7 </ExpenseContext.Provider>8 );9};
The useReducer
hook allows us to create a reducer using the reducer
function defined previously. The initialState
is passed as the second argument.
Wrap the App with the provider
🔗When the ExpenseContextProvider
is wrapped around any component in the React app, that component and its children will be able to access the current state as well as modify the state object.
In this section, that's what we are going to do. Open, App.js
file, and modify it as below.
1import React from 'react';2import { Container } from 'reactstrap';34import { ExpenseContextProvider } from './GlobalState';56import Header from './components/Header';7import Form from './components/Form';8import List from './components/List';910export default function App() {11 return (12 <ExpenseContextProvider>13 <Container className="text-center">14 <Header />15 <Form />16 <List />17 </Container>18 </ExpenseContextProvider>19 );20}
In the next sections, let us create other components that are children to this App
component. Create a components/
directory and then create three new component files:
Header.js
Form.js
List.js
Add the header of the app
🔗In this section, let us define a presentational component called Header
. It's going to be a simple jumbotron component from Bootstrap displaying the title of the app and the logo.
Open Header.js
and add the following snippet:
1import React from 'react';2import { Jumbotron } from 'reactstrap';3import Logo from '../logo.svg';45export default function Headers() {6 return (7 <Jumbotron fluid>8 <h3 className="display-6">9 Expense Tracker React App10 <img src={Logo} style={{ width: 50, height: 50 }} alt="react-logo" />11 </h3>12 </Jumbotron>13 );14}
Add a form component
🔗Open Form.js
file and import the following statements.
1import React, { useState, useContext } from 'react';2import {3 Form as BTForm,4 FormGroup,5 Input,6 Label,7 Col,8 Button9} from 'reactstrap';10import { v4 as uuid } from 'uuid';1112import { ExpenseContext } from '../GlobalState';
The uuid
module is going to generate a unique id for each expense item in the global state.
Define a Form
component that is going to access values from ExpenseContext
using useContext
hook.
1export default function Form() {2 const [state, dispatch] = useContext(ExpenseContext);34 //...5}
Using the useState
reducer, define two state variables that are going to be local to this component. These state variables are going to help us define controlled input fields. A controlled input field accepts its current value as a prop as well as a callback to change that value.
Add the following initial state for name
and amount
using useState
. Both of them are going to have an empty string as their initial value.
1const [name, setName] = useState('');2const [amount, setAmount] = useState('');
To update their values when a user starts typing, add the following handler methods. Both of these functions are going to retrieve the value from the corresponding field. The console statements are for testing purposes.
1const handleName = event => {2 console.log('Name ', event.target.value);3 setName(event.target.value);4};56const handleAmount = event => {7 console.log('Amount ', event.target.value);8 setAmount(event.target.value);9};
Lastly, to submit the form, there is going to be another handler method called handleSubmitForm
. This method when triggered is going to dispatch the action to add the expense (ADD_EXPENSE
). This is how the reducer
function in the global state updates the state.
1const handleSubmitForm = event => {2 event.preventDefault();3 if (name !== '' && amount > 0) {4 dispatch({5 type: 'ADD_EXPENSE',6 payload: { id: uuid(), name, amount }7 });89 // clean input fields10 setName('');11 setAmount('');12 } else {13 console.log('Invalid expense name or the amount');14 }15};
Lastly, add the following JSX to display the component.
1return (2 <BTForm style={{ margin: 10 }} onSubmit={handleSubmitForm}>3 <FormGroup className="row">4 <Label for="exampleEmail" sm={2}>5 Name of Expense6 </Label>7 <Col sm={4}>8 <Input9 type="text"10 name="name"11 id="expenseName"12 placeholder="Name of expense?"13 value={name}14 onChange={handleName}15 />16 </Col>17 </FormGroup>18 <FormGroup className="row">19 <Label for="exampleEmail" sm={2}>20 Amount21 </Label>22 <Col sm={4}>23 <Input24 type="number"25 name="amount"26 id="expenseAmount"27 placeholder="$ 0"28 value={amount}29 onChange={handleAmount}30 />31 </Col>32 </FormGroup>33 <Button type="submit" color="primary">34 Add35 </Button>36 </BTForm>37);
Display a list of items
🔗In this section, let's add the List.js
component to display a list of items from the current state object provided by the ExpenseContext
. Open the file and add the following import statements:
1import React, { useContext } from 'react';2import { ListGroup, ListGroupItem } from 'reactstrap';34import { ExpenseContext } from '../GlobalState';
Next, map the state
value to display the name of the expense and the amount of the expense as a list item.
1export default function List() {2 const [state] = useContext(ExpenseContext);3 return (4 <ListGroup>5 {state.expenses.map(item => {6 return (7 <ListGroupItem key={item.id}>8 {item.name} - $ {item.amount}9 </ListGroupItem>10 );11 })}12 </ListGroup>13 );14}
Running the app
🔗All the components of the simple Expense Tracker app are complete. Now, let's run the app and see it as an action. On the initial render, the Rect app is going to look like below.
It is going to display one expense item that is defined as the object in the initial state. Try adding a new item in the list and see if the list updates and form gets cleared or not.
Conclusion
🔗Using useReducer
in conjunction with React's Context API is a great way to quickly get started with managing your state. However, some caveats come with React's Context API. Re-rendering of multiple components unnecessarily can become a huge problem and its something you should take care.
More Posts
Browse all posts