In this tutorial, you will manage the state of a React app using Redux. Redux helps you track and manage the state of an entire application in a single object instead of having the state and logic in a top-level component.
You will build a to-do app that centralizes the state and logic using Redux.
By the end of the tutorials you will know:
What Redux is and the benefit of using Redux for state management.
Understand and use Redux concepts such as the store, reducer, actions, etc. in a Todo app.
The tutorial will be in two sections. The first section explains key concepts, the Redux architecture, and the basic usage of Redux. In the next section, we will build a Todo app using Redux for state management.
Prerequisite
To get the most out of this tutorial you should be familiar with:
Functions in JavaScript
Knowledge of React terminology: State, JSX, Components, Props, and Hooks
Building a basic React app
Introduction to State Management
React enables developers to build complex user interfaces easily. To add interactivity to the UI, React components need access to data. The data can be a response from an API endpoint or defined within the app. This data will be updated in response to an interaction, such as when a user clicks on a button, or types into an input field.
Inside a React component, the data is stored in an object
called state
. Whenever state
changes, the component will re-render, and React will update the screen to display the new data as part of the UI.
In a React app, multiple components may need access to the state. Hence, it needs to be effectively managed. Effective state management entails being able to store and update data in an application.
What is Redux?
Redux is a pattern and library for managing and updating application state, using events called "actions". It serves as a centralized store for state that needs to be used across your entire application, with rules ensuring that the state can only be updated in a predictable fashion.
With Redux, you have a central store to keep, update, and monitor the state of your application. That means, our components may not have states. The state will be in a central location and can be accessed by multiple components in your application.
What problem does Redux solve?
A basic React app can be segmented into the following:
State: The current condition of the app
View: The UI of the app
Actions: A function that updates the state when an event occurs in your app (generally referred to as Event handlers).
Every component in an app can have a state. However, it becomes a challenge if multiple components need access to the same data. To solve this issue, we "lift the state up". Lifting state up is a process where you move the state from a child component to its parent (top-level) component. With this approach, you can easily share state between multiple child components.
However, there are disadvantages to "lifting state up":
It can complicate your code: Lifting the state up can add huge boilerplate code to your components. The state must be passed down from the parent component to the child components resulting in
prop-drilling
. Additionally, the state is updated in the parent component.It can impact performance: When you lift the state up, you increase the number of components that will re-render when the state changes. This can affect performance, especially on mobile devices.
In a large-scale single-page application, our code will manage more states. This state can include server responses and cached data, as well as locally created data that has not yet been persisted to the server. It will get to the stage where you will lose control over when, why and how the state updates. Making it difficult to reproduce bugs or add new features.
A better approach is to extract the shared states from the parent component and put them into a centralized location outside the component tree.
This is a better approach because:
It eliminates prop drilling.
Regardless of the component position, you can trigger actions from any component inside the parent component, and the state can be modified.
That is the concept behind Redux. It helps developers manage global state (a state that is needed across many parts of our app), and make the state accessible by all components irrespective of how deeply nested they are.
The first rule of Redux is that everything that can be modified in your application should be represented in a single object called the state
or the state tree.
There are key terms to know when using Redux. To make it easy to understand these terms we will first consider an analogy for Redux. After this, we will define the terms in the next sections.
Redux analogy
Imagine you are the Managing Partner of a huge restaurant. To be well versed in managing the restaurant, you decide to keep track of the state of the restaurant.
You might want to track:
The stock of the various ingredients available
Financial status (weekly income and expenditure pattern)
The number of employed chefs
The number of hired waitresses
Weekly customers etc
To keep all this information in your brain will be a hassle. Instead, you keep them in a central location called the store (redux store
).
You hire an attendant (reducer
) who is the only person who can update the store's information.
There are shareholders (components
) who rely on the state(data) of the restaurant to update their portfolio (UI
). These shareholders can only access data and cannot modify it.
Now let us assume the shareholders hire a new chef and the store's data need to be updated. Because they cannot update the data, a shareholder can send (dispatch
) a note with that new information to the attendant(reducer
). He then updates the previous data in the store with the latest information.
Anytime data is updated, the rest of the shareholders are notified (subscribers
), and they can update their portfolio (UI
)
Understanding the Redux Terminologies
Below are the new terms to be familiar with:
Actions
The second rule of Redux is that the state
is read-only. You can only modify the state tree by sending an action
. This ensures that neither the views nor the network callbacks will ever write directly to the state. Instead, they express an intent to transform the state.
In other words, action
is the only recommended way to change the application state
.
An action describes what has occurred in the application. It is a JavaScript object passed as a parameter to the store
and holds the information required for the store
to update the state
.
An action
varies in any given application. For instance, in a counter app, you will only need the following actions:
increased the count
decreased the count
In a todo app, you may have the following actions
:
Added todo item
Deleted todo item
Complete todo item
Filter todos, etc.
With Redux, because we are separating the state
from the components, the components don't know exactly how the state changes. All they care about is that they need to send an action
.
The action object
has a type
property where you specify what event occurred in your component. That means whenever an event occurs, the event handler function will dispatch an action
with what has occurred to help update the state
in the store.
The action
object
also has a payload
property. Any new data about the event will be in the payload
. For instance, when I dispatch an action
of type
"addedTodo", the payload
can contain the new to-do item and the ID.
Below are examples of action
objects:
//action 1
const addTodoAction = {
type: 'todos/todoAdded', //what happened
payload: {todoID, newTodo} //data
}
//action 2
const getOrder = {
type: 'orders/getOrderStatus', //what happened
payload: {orderId, userID} //data
}
Action Creators
The action creators
are functions that return action
objects. Because the action creators
contains the logic that can be used in multiple instances of the application, you can pass it some parameters
that can be accessed in the action
objects.
Below are examples of action creators
:
//example 1
function addTodo(todo){
//return action object
return {
type: 'todos/addTodo',
payload: todo // what happened
}
}
//example 2
function getOrders(orderID, userID){
//return action object
return {
type: 'orders/getOrderStatus',
payload: {orderId, userID} //what happened
}
}
Reducers
A reducer
is a pure function that accepts the current state
and an action
as arguments
and returns the updated state
. It is called a reducer
because similar to the Array.reduce()
method, the Redux reducer reduces a set of actions over time into a single state.
The reducer should be a pure function. A pure function is a function that will return the same output for the same input. It does not change or modify the global state or the state of any other functions.
What this means is :
A reducer function is not allowed to modify the current
state
. Instead, they must make a copy of the currentstate
and update the copied values.A reducer function should not update the state by reading from a database
A reducer function should not make a call to any third-party API
Below is the syntax of a reducer function:
const myReducer = (state, action) => newState
The logic inside the reducer function is as below:
In the body of the function, we check the
action.type
property.If the
type
of action matches something you have defined, you will make a copy of thestate
, and modify that state with the new value from theaction.payload
If the
action.type
does not match anything you have defined, you will return the existingstate
Below is an example of a todoReducer
function:
const intialTodo = [{id:1, todo:""}]
const todoReducer = (state = initialTodo, action)=>{
if(action.type === "todos/AddedTodo"){
return [...state, todo: action.payload]
}else{
return state
}
}
Below is what is happening:
In the
if
statement verify if theaction.type
matches the expression on the right (action.type === "todos/AddedTodo"
)If it does, then we use the spread operator (
...
) to make a copy of thestate
, and update the copy with the new todo data derived fromaction.payload
If not, we return the previous
state
The third principle of Redux is that to describe state changes, you declare a function that accepts the previous state of the app, the action being dispatched, and returns the next state of the app
Store
A store
in an object that holds the entire state
of your application. It is the central location of data and where data is updated.
The store
has three main methods:
getState()
: Returns the currentstate
of the applicationdispatch(action)
: This is how to instruct the component to send an action to the store to change thestate
of the application.-
subscribe(listener)
: Thesubscribe
method will allow the components to listen for a change in data. It accepts alistener
as acallback
function that helps you:- Update the UI to reflect the current state
- Perform side effects or any other task that needs to be done when the state changes.
Below is an example of how to create a store
in redux.
//store.js
//import your root reducer
import rootReducer from "./rootReducer"
//import createStore from redux
import {createStore} from "redux"
//create the store
const store =createStore(rootReducer)
Dispatch
Dispatch is used to send action
to our store
. It is the only way to change the state
of our app.
To update the state
, you will call the store.dispatch()
method. When dispatch()
is called, the store will execute the reducers (the reducers have access to the current state
and an action
as input, and perform some logic). The store then updates its state with the output of the reducers.
The store.dispatch()
method accepts the action
object as an argument.
store.dispatch({ type: 'todo/addedTodo' })
In the snippet above, for instance, whenever a user enters a new to-do, you will dispatch the action to the store
. Because there is a reducer
function inside the store, it will use the dispatched action.type
to determine the logic for the new state.
Selectors
Selectors are functions that help you extract specific data from the state
. It accepts the state
as an argument and returns the data to retrieve from the state
.
You will use the selector
in your component to get specific data from the state.
const selectLatestTodo = state => state.data //selector function
const currentValue = selectLastestTodo(store.getState())
console.log(currentValue)
// Buy milk
Illustration of the Redux architechture
In this section, we will use all the terminologies learned to explain how data flow in our app, and how the UI is re-rendered when the state changes.
Let's take a look at the setup of Redux:
Create a redux
store
Define the
reducer
logic and pass thereducer
function to the store. Thereducer
accepts thestate
andaction
object as arguments.The
store
will run the logic in thereducer
functionThe value returned by the reducer function becomes the initial state of the app.
When the component is mounted, it connects with the store, gets the initial state, and uses the state to display the UI. Because the component is connected to the store, it will have access to any state update.
Updating the state:
An event occurs in the app. For instance, a to-do item has been added
The component dispatches the
action
to the reduxstore
The
store
re-runs thereducer
function. It has access to the previousstate
, theaction
object, and returns the updated stateThe
store
notifies all the connected components of the state change.Each UI component will verify if it needs to use the updated state.
If it does, it re-renders the UI with the new state and updates what is on the screen.
Creating a store
, subscribing
and dispatching actions
In this section, we will learn how to create a store
and dispatch
actions to the store.
The Redux store brings together the state
, reducer
and action
of our application. It is the central location of the application's state.
The functions of the store
are to:
Hold the current state of the application.
Allow access to the current state
Allow the state to be updated
Dispatch actions
Subscribe to changes
Creating a store
Use the createStore()
method from the Redux library to create the store
. This method accepts a reducer
function as an argument.
Below is a code snippet on how to create a store:
//store.js
import { createStore} from 'redux'
const store = createStore(rootReducer)
Next, you will need to pass the "root reducer" function to the createStore()
. The root reducer combines all of the other reducers in your application.
To create a root reducer, you import the combineReducer()
method from the Redux library. The combineReducer
helps you combine all the different reducer functions. It accepts an object
of reducer functions as its argument. The key
of the object will become the keys in your root state object, and the values are the reducer functions.
Below is an example of how to create a rootReducer
:
import { combineReducers } from 'redux';
const reducers = {
user: userReducer,
cart: cartReducer,
orders: ordersReducer,
};
const rootReducer = combineReducers(reducers);
Now, you have learned how to
Create a store
Add a root reducer to the store
Next, you will learn how to get the initial state of the store and dispatch actions to the store
Dispatching actions
to the store
To update the state
of the application, the component needs to dispatch actions.
Below is how to do that:
Import the
store
into your applicationcall the
store.dispatch()
methods and pass it theaction
objects.
//actions object
const addTodo = {
type: 'todos/todoAdded',
payload: "Buy milk"
});
const completeTodo = {
type: 'todos/todoRemoved',
payload: 2 // id of the todo to complete
}
//dispatch the action to update the state
store.dispatch(addTodo)
store.dispatch(completeTodo)
Subscribing to the store
Use the subscribe()
method to subscribe to a store. The subscribe()
method will listen for changes to the state
of your app. This will help you update the UI to reflect the current state
, perform side effects, or any task that needs to be done when the state
changes.
In the code snippet below illustrates how to listen for updates, and log the latest state to the console:
const subscription = store.subscribe(() => {
// Do something when the state changes.
console.log('State after dispatch: ', store.getState())
});
Adding Redux to a React app
In this section, you will use the concept learned to build a to-do app with basic functionalities ( add, delete, and complete a todo) while using redux for state
management.
We will not go in-depth into each implementation as we have covered the concepts earlier.
Here are the steps to follow:
Set up your React project and install the required dependencies
Create your Todo components for the UI
Create a redux
store
to track and manage thestate
of your appDefine the
reducer
logic and connect to the storeDispatch
actions
to update thestate
Read data from the
store
The repository for the project is found here. It has branches for each major step we will implement.
Let's get started
Step 1: Setting up your project
Create a React app in your project directory by following the steps below:
-
Start a project from the basic template using Vite by running the command below in your terminal:
npm create vite@latest my-react-app --template react // Replace "my-react-app" with the name of your project.
-
Install the
redux
andreact-redux
dependencies in yourpackage.json
file. Run the command
npm install redux react-redux --save
This installs the core redux architecture and simplifies connecting the react app with redux.
Run the app with
npm run dev
Step 2: Creating the Todo components
Below is the UI for our app.
Now, let's create the required components
Create a "components" folder in the "src" directory
-
The components needed to create the UI of the app are:
-
<TodoHeading/>
: contains the heading text -
<TodoInput/>
: an input to enter a todo -
<TodoItem/>
: displays a single todo, with a delete and complete button -
<TodoList/>
: display the lists of todos.
-
Step 3: Creating the store
Next, we will need to create a store
to keep track of and manage the entire state
of the application. We do that using the createStore()
method from Redux.
Follow these steps:
Create a "store" folder in the root directory of your app
Create a
store.js
file inside the "store" folderImport the
createStore
method from reduxCall the
createStore()
method, and pass thetodoReducer
as an argument ( we will define this in the next step)Export the
store
to be used inside your React app
//store/store.js
import { createStore } from "redux";
import todoReducer from "../reducer/todoReducer";
const store = createStore(todoReducer);
export default store;
Step 4: Defining the reducer
function
We will define our reducer inside a todoReducer.js
file. Reducers are functions that contain the logic required to update the state and return a new state. The todoReducer.js
will also contain the initial state of our app.
Follow the steps below to define a reducer
function:
- Create a
todoReducer.js
insider a "reducer" folder
Add the code snippet below to the file
//state object
const initialState = {
todos: [
{
id: 1,
item: "Learn redux fundamentals",
completed: false,
},
{
id: 2,
item: "Build a todo-app",
completed: false,
},
],
};
//define the reducer logic
const todoReducer = (state = initialState, action) => {
switch (action.type) {
//logic to add a new todo
case "todos/addedTodo":
return {
...state,
todos: [...state.todos, action.payload],
};
//logic to delete a todo
case "todos/deleteTodo":
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.payload),
};
// logic to complete a todo
case "todos/completeTodo":
return {
...state,
todos: state.todos.map((todo) => {
if (todo.id === action.payload) {
return {
...todo,
completed: !todo.completed,
};
} else {
return todo;
}
}),
};
default:
return state;
}
};
export default todoReducer;
In the code above:
We added the initial
state
of our app. It is prefilled with anarray
of todos to enable us to display some dummy data when the app is rendered.We define the
todoReducer
function. This function accepts thestate
andaction
objects as parameters.In the body of the function, we implemented a
switch
statement. Based on the expression (action.type
) there is a logic in eachcase
on how thestate
will be updated and returned.Finallly, we
export
thetodoReducer
and pass it as an argument to thecreateStore()
method (as previously indicated).
Step 5: Wrapping the Provider
component around your app
The Provider
component enables the Redux store
to be available to any nested components that need to access the store
.
Since any React component in a React Redux app can be connected to the store, most applications will render a Provider
at the top level, with the entire app’s component tree inside of it.
Below is how to wrap our root component inside a Provider
Import the
store
in themain.jsx
Import the
Provider
component from "react-redux"Wrap your root component
<App/>
in theProvider
The
Provider
accepts astore
props with the importedstore
as its value
//main,jsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./index.css";
import { Provider } from "react-redux"; //Provider
import store from "./store/store.js"; //store
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
In the code above, we have wrapped the <Provider/>
around <App/>
component to enable all nested components to access the store
Next, we would read and display data from the store using the useSelector
hook
Step 6: Reading and displaying the todos with useSelector
hook
React-Redux has its custom hooks, which you can use in your components. The useSelector
hook lets the React components read data from the Redux store. It accepts a selector function that takes the entire Redux store state as its argument, reads some value from the state, and returns that result.
Follow the steps below to read and display data:
Import the
useSelector
from "react-redux"Call the
useSelector()
method. It accepts a selector function as a callbackReturn the
todos
from thestate
The code below illustrates how to read the todos
from our store.
//components/TodoList.jsx
import React from "react";
import { useSelector } from "react-redux";
import TodoItem from "./TodoItem";
const TodoList = () => {
//callback function
const selectTodos = (state) => state.todos;
//extract the todos
const returnedTodos = useSelector(selectTodos);
const displayTodos = returnedTodos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
));
return <div>{displayTodos}</div>;
};
export default TodoList;
Let's understand the code above:
The first time the
<TodoList/>
component renders, theuseSelector
hook will execute theselectTodos
callback function.What is returned by the
selectTodos
will be returned by theuseSelector
hook to be used in our component.The
const returnedTodos
will hold the same data as thestate.todos
inside our Redux store state.We use the JavaScript
map()
method to iterate over each todo and display a single todo.The
useSelector
automatically subscribes to the Redux store. Hence, whenever anaction
is dispatched, it will call theselectTodos
function. If the value returned by theselectTodos
has been updated,useSelector
will force theTodoList
component to re-render with the new data.
We know how to read and display data from the store
. Next, we will learn how to dispatch actions from the components to update the store
Step 5: Dispatching actions with useDispatch
hook
The useDispatch
hook provides access to the dispatch
method that is needed to dispatch actions
to update the state
.
We can call const dispatch = useDispatch()
in any component that needs to dispatch actions, and then call dispatch(someAction)
as needed.
In the TodoInput
component, let's dispatch an action to add a new todo:
import
useDispatch
from "react-redux"Call the
useDispatch()
method. It returns thedispatch
functionEnter and submit the new todo
-
Call the
dipatch()
method in theaddTodo
and pass theaction
object
//components/TodoInput.jsx import React, { useState } from "react"; import { useDispatch } from "react-redux"; const TodoInput = () => { const [todo, setTodo] = useState(""); const dispatch = useDispatch(); const onInputTodo = (e) => { setTodo(e.target.value); }; //handle submission of todo const handleTodoSubmit = (e) => { e.preventDefault(); addTodo(); // addTodo setTodo(""); }; //action creators const addTodo = () => { //dispatch action to add a todo return dispatch({ type: "todos/addedTodo", payload: { id: Math.floor(Math.random() * 20) + 1.1, item: todo }, }); }; return ( <div> <form className="todo_form_container" onSubmit={handleTodoSubmit}> <input className="todo_input" type="text" placeholder="Enter your todo" value={todo} onChange={onInputTodo} /> <button className="todo_btn">Add Todo</button> </form> </div> ); }; export default TodoInput;
In the code above, on submitting a new todo:
The
handleTodoSubmit
function executes theaddTodo()
functionThe
addTodo()
dispatches theaction
object to thetodoReducer
function to update thestate
Because we have imported the useSelector
hook, we can easily add a new todo to the store's state, and it will reflect in the UI.
Below is what we have done so far
Create the
store
Wrap the
<Provider store={store}>
around your top-level<App>
component to enable all other components to access and update the store.Call the
useSelector
hook to read data in React componentsCall the
useDispatch
hook to dispatch actions in React components
Dispatching action on clicking the "delete" and "complete" buttons
In the TodoItem
components, we can now click on the "delete" and "complete" button. On clicking these buttons we dispatch actions to delete and complete a todo. These are handled in the onDelete
and onComplete
action creators.
The code snippet is as below:
//components/TodoItem.jsx
import { useDispatch } from "react-redux";
const TodoItem = ({ todo }) => {
const dispatch = useDispatch();
//delete a todo
const onDelete = (id) => {
return dispatch({
type: "todos/deleteTodo",
payload: id,
});
};
//complete Todo
const onComplete = (id) => {
return dispatch({
type: "todos/completeTodo",
payload: id,
});
};
return (
<div>
<h3 className={`todo${todo.completed ? "Completed" : ""}`}>
{todo.item}
</h3>
<div>
<button onClick={() => onComplete(todo.id)}>Complete</button>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</div>
</div>
);
};
export default TodoItem;
- Clicking the "delete" and "complete" button calls the
onDelete()
andonComplete
functions respectively. TheonDelete
function dispatches an action to thetodoReducer
to delete the specified item while theonComplete
function dispatches an action to thetodoReducer
to complete the selected item.
Redux vs Context API
The difference between Redux and Context API is how they manage states. In Redux state is managed in a central store. However, the Context API deals with state updates on a component level, as they happen within each component.
You might ask, can't you use the useContext
hook from the Context API to pass state to multiple components since that eliminates prop drilling
?
In a scenario where a state affects multiple components or is required by all the components in an app, you can use the useContext
hook to manage state
. This avoids props drilling and makes data easily accessible in all components.
However, there are some disadvantages to using useContext
:
-
The
useContext
hook has a complex setup: When building an enterprise-level app where multiple components may need access tostate
, you might have to use thecontext
API to create multiple contexts and provide each context with the data required for the different aspects of your application. For instance, you will create- Authentication Context: to easily authenticate users
- Theming Context: to change the theme. For example, enable dark mode
- Form Context: to pass form data to the form component, etc.
This phenomenon might result in having to create multiple contexts to meet a specific need, leading to deeply nested Context Provider components in your application.
Secondly,
useContext
is not optimized for enterprise-level apps where the state changes frequently. This will decrease the performance of your app.Lastly, when using
useContext
, UI logic and state management will be in the same component.
Below are some scenarios you might use Redux over useContext
:
There are lots of states in your application, and these states are required in many places in the app.
The app state is updated frequently over time
The logic to update that state may be complex
The app has a medium or large-sized codebase and might be worked on by multiple developers
Conclusion
In this tutorial, you managed the state of a React Todo app using Redux. Next, learn how to manage the state using the Redux Toolkit. Redux Toolkit makes it easier to write good Redux applications and speeds up development. Furthermore, learn Redux DevTools to help you trace when, where, why, and how your application's state changed.