Introduction
As a React developer, understanding how to manage states effectively is key to building fully functional applications. How you handle data and execute its logic determines how efficient your applications will be.
Therefore, in this tutorial, I will walk you through how to manage states effectively using various methods, such as the useState hook, the useReducer hook, Context API, Redux toolkit, and URLs.
Just a heads up: This blog was originally posted on my website. If you're interested in exploring more of my content or want to see the original post, here.
What is state management?
A state is the current value within an application at a specific time. For example, when you save, delete, or update a file on your computer, you're changing the state of that file. This concept is similar to React states; you can modify their values at any point within the application.
Therefore, managing states in React means handling the different actions that modify their value within an application. So, how exactly do we manage states in React?
It's important to understand that in React, state management is about efficiently handling the current state of the application, and each state is bonded to a specific component.
How to manage states using the React useState hook
The useState hook in React is the easiest way to manage states. It enables us to create and change state variables within our components.
For example, consider a Cart component.
It needs two states: one for the current item being added to the Cart and another for the Cart itself.
The Cart state can be an array, and the item can be an object containing the product's price, name, and quantity, depending on the application requirements.
Creating and managing states using the React useState hook
The code snippet above shows how to create a state using the useState hook.
const [state, setState] = useState(<default_value>)
The useState hook returns an array with two elements. The first value within the array represents the state name, and it holds the current value of the state. The second value is a function that enables us to update the state's value.
Any state declared within your application must have a default value depending on its data type. It can be a string, number, object, array, or even a null value.
onst [stringState, setState] = useState("");
const [objectState, setState] = useState({});
const [objectState, setState] = useState([]);
const [state, setState] = useState(null);
Updating states with the useState hook
In the previous section, you learned how to declare states using the React useState hook. Here, I will walk you through how to update them.
Consider the code snippet below:
import { useState } from "react";
const App = () => {
const [name, setName] = useState("David");
const changeName = () => setName("Ankur");
return (
<div>
<p>{name}</p>
<button onClick={changeName}> Change Name</button>
</div>
);
};
export default App;
From the code snippet above, I created a name state using the useState hook and set its default value to "David". The function changeName updates the name state to "Ankur" when the user clicks the button. This is a simple illustration of how you can useState hook to manage your states.
Before we proceed, take a look at another example. Suppose we have a page that displays a counter that allows us to increase and decrease its value when we click on the buttons.
import {useState} from 'react'
const App = () => {
const [counter, setCounter] = useState(0)
const increaseCounter = () => {
setCounter(count => count + 1)
}
const decreaseCounter = () => {
setCounter(count => count - 1)
}
return (
<div>
<h2>{counter}</h2>
<div>
<button onClick={decreaseCounter}> Decrease </button>
<button onClick={increaseCounter}> Increase </button>
</div>
</div>
)
}
export default App
Unlike the previous example, the setCounter function takes a parameter and either adds or subtracts its value. But why is this necessary?
In cases where you rely on the previous state's value to determine its current state, it is best to use the functional approach. This is because the setCounter function is asynchronous. Therefore, to prevent issues and ensure the state is updated correctly, the functional approach is recommended.
//👇🏻 increases counter (count is the previous value)
const increaseCounter = () => {
setCounter((count) => count + 1);
};
//👇🏻 decreases counter
const decreaseCounter = () => {
setCounter((count) => count - 1);
};
So far, you've learnt how to create and handle states using the React useState
hook. Next, let's discuss another React hook that is immensely powerful for managing states within complex applications.
How to manage states using the React useReducer hook
The useReducer hook is another powerful tool for managing states within your application. Unlike useState, which is suitable for simpler cases, useReducer is commonly used when handling a large number of states, especially in components with numerous states across multiple event handlers.
The useReducer hook can be divided into four key components: the state, the reducer function, the action, and the dispatch function. Think of it as a machine, where the state represents the current condition of the machine—whether it's on, off, ready for work, or busy.
The reducer function acts as the machine's brain. It interprets actions and instructs the computer on what to do based on various actions.
Actions are similar to the buttons available to you, each triggering a specific action. The action refers to the instructions given to the machine. Actions can be the buttons available for you to press to carry out various actions.
The dispatch function serves as the control panel of the machine. It triggers the reducer function to carry out a particular task, and you can only interact with the dispatch function when processing a job.
In React, the state is an object containing all states declared within the application. The reducer function manipulates the state directly and returns a copy of the result, and the dispatch function triggers the reducer function when various events occur.
The action is an object containing a type and a payload property. The type property specifies the exact action to be executed by the reducer function, and the payload can accept data from the user or other parts of the application.
Managing states using the useReducer hook
In this section, you'll learn how to create and update React states using the useReducer hook. First, let's replicate the counter using the useReducer hook.
import { useReducer } from "react";
const App = () => {
//👇🏻 reducer function
const reducer = (state, action) => {
switch (action.type) {
case "increase":
return { counter: state.counter + 1 };
case "decrease":
return { counter: state.counter - 1 };
default:
return state;
}
};
//👇🏻 declares the useReducer hook
const [state, dispatch] = useReducer(reducer, { counter: 0 });
const increaseCounter = () => {
dispatch({ type: "increase" });
};
const decreaseCounter = () => {
dispatch({ type: "decrease" });
};
return (
<div>
<h2>{state.counter}</h2>
<div>
<button onClick={decreaseCounter}>Decrease</button>
<button onClick={increaseCounter}>Increase</button>
</div>
</div>
);
};
From the code snippet above, you need to declare the useReducer hook, as shown below.The useReducer hook accepts two arguments, the reducer function and the state object, and returns an array containing the state and the dispatch function.
const [state, dispatch] = useReducer(reducer, { counter: 0 });
Next, you need to create the reducer function to execute the increase and decrease actions. The useReducer hook achieves this using a switch statement that checks the type of action to be performed and executes that action.
const reducer = (state, action) => {
switch (action.type) {
case "increase":
return { counter: state.counter + 1 };
case "decrease":
return { counter: state.counter - 1 };
default:
return state;
}
};
Execute the dispatch function to trigger the reducer based on the action you want to perform.
const increaseCounter = () => {
dispatch({ type: "increase" });
};
const decreaseCounter = () => {
dispatch({ type: "decrease" });
};
Finally, you can access the counter value from the state object and execute the dispatch functions when users click the buttons.
return (
<div>
<h2>{state.counter}</h2>
<div>
<button onClick={decreaseCounter}>Decrease</button>
<button onClick={increaseCounter}>Increase</button>
</div>
</div>
);
Apart from executing actions within the reducer function, you can also pass values into it using a payload object within the dispatch function. To demonstrate this, let's try to increase the counter by 5 when the user clicks a button.
Create a function named increaseBy5 that executes the dispatch function, which accepts a payload, as shown below.
const increaseBy5 = () => {
dispatch({type: "increaseBy5", payload: {number: 5}})
}
Finally, update the reducer function to execute the action and add its button to the page. The state updates its value using the number passed into the payload.
const reducer = (state, action) => {
switch (action.type) {
case "increase":
return { counter: state.counter + 1 };
case "decrease":
return { counter: state.counter - 1 };
case "increaseBy5":
return { counter: state.counter + action.payload.number };
default:
return state;
}
};
One major difference between the useReducer and the useState hook is that, unlike the useState hook, you can manage multiple states within the useReducer hook. Let's add another state that toggles its value when we click a button.
Modify the useReducer hook by adding a name state.
const [state, dispatch] = useReducer(reducer, { counter: 0, name: "David" });
Since there is more than one state within the useReducer hook, you need to modify the reducer function to update the states using the rest operator.
const reducer = (state, action) => {
switch (action.type) {
case "increase":
return { ...state, counter: state.counter + 1 }
case "decrease":
return { ...state, counter: state.counter - 1 }
case "increaseBy5":
return { counter: state.counter + action.payload.number }
case "toggleName":
return { ...state, name: state.name === "Ankur" ? "David" : "Ankur" };
default:
return state
}
}
Finally, create its dispatch function that toggles the name state when a user clicks the button.
const toggleName = () => {
dispatch({type: "toggleName" })
}
Additionally, it is advisable to save your action names within an object and access their name via this object to prevent naming errors. For example, we can have an action variable, as shown below.
const ACTIONS = {
increase: "increase",
decrease: "decrease",
increaseBy5: "increaseBy5",
toggleName: "toggleName"
}
const reducer = (state, action) => {
switch (action.type) {
case ACTIONS.increase:
return { ...state, counter: state.counter + 1 }
case ACTIONS.decrease:
return { ...state, counter: state.counter - 1 }
case ACTIONS.increaseBy5:
return { counter: state.counter + action.payload.number }
case ACTIONS.toggleName:
return { ...state, name: state.name === "Ankur" ? "David" : "Ankur" };
default:
return state
}
}
const toggleName = () => {
dispatch({type: ACTIONS.toggleName })
}
useState vs useReducer hook for managing states
So far, you have learned how to manage states using the useState and useReducer hooks. In this section, we will analyze both of them and explore the best cases for their usage.
The useState hook is easy to use and ideal for applications with simple state management. It is recommended in such cases because it requires less code and is easy to implement. On the other hand, the useReducer hook is best for applications involving complex state management.
Declaring states using the useState hook:
const [state, setState] = useState(<initial value>)
Declaring states using the useReducer hook:
const reducer = (state, action) => {
switch(action.type) {
//cases
}
}
const [state, dispatch] = useReducer(reducer, {})
When you have multiple states within a component or states with complex transitions, it is not efficient to use the useState hook in this case. In such cases, it is highly recommended to use the useReducer hook because it provides a centralized logic for your states and handles the state management efficiently to ensure that it works as expected.
To demonstrate this, let's create a todo application using the useReducer hook. The application allows users to create and delete todos.
First, you need to declare the useReducer hook with its default state. The todos state is an array containing each todo object, and the todoInput represents each item added by the user.
import { useReducer } from "react";
const App = () => {
const [state, dispatch] = useReducer(reducer, {
todos: [
{ id: Math.random(), todo: "Friends hangout" },
{ id: Math.random(), todo: "Team meeting" },
],
todoInput: "",
});
return <div>{/**-- App UI--**/}</div>;
};
The code snippet above creates two new states - the todos array and the todoInput for holding the user's current input.
Next, create the reducer function.
//👇🏻 action names
const ACTIONS = {
addTodo: "addTodo",
deleteTodo: "deleteTodo",
createTodo: "createTodo",
};
//👇🏻 reducer functions
const reducer = (state, action) => {
switch (action.type) {
case ACTIONS.addTodo:
return {
...state,
todos: [
...state.todos,
{ id: Math.random(), todo: action.payload.todo },
],
};
case ACTIONS.createTodo:
return { ...state, todoInput: action.payload.input };
case ACTIONS.deleteTodo:
const updatedTodos = state.todos.filter(
(todo) => todo.id !== action.payload.id
);
return { ...state, todos: updatedTodos };
default:
return state;
}
};
The addTodo action adds the newly created todo item to the todos array. The createTodo action updates the state of the todo input, and the deleteTodo action removes a selected todo item from the todos array using its ID.
Return the user interface for the application from the App component.
import { useReducer } from "react";
const App = () => {
const ACTIONS = {
addTodo: "addTodo",
deleteTodo: "deleteTodo",
createTodo: "createTodo",
};
const reducer = (state, action) => {
switch (action.type) {
case ACTIONS.addTodo:
return {
...state,
todos: [
...state.todos,
{ id: Math.random(), todo: action.payload.todo },
],
};
case ACTIONS.createTodo:
return { ...state, todoInput: action.payload.input };
case ACTIONS.deleteTodo:
const updatedTodos = state.todos.filter(
(todo) => todo.id !== action.payload.id
);
return { ...state, todos: updatedTodos };
default:
return state;
}
};
const [state, dispatch] = useReducer(reducer, {
todos: [
{ id: Math.random(), todo: "Hello todo" },
{ id: Math.random(), todo: "People good?" },
],
todoInput: "",
});
return (
<div>
<h2> Todo List</h2>
{state.todos.map((td) => (
<div key={td.id}>
<p>{td.todo}</p>
<button onClick={() => handleDeleteTodo(td.id)}>Delete</button>
</div>
))}
<form onSubmit={handleAddInput}>
<input
type='text'
value={state.todoInput}
onChange={(e) => handleInputChange(e)}
/>
<button type='submit'> Add Todo</button>
</form>
</div>
);
};
export default App;
Finally, create the function that handles input changes and two more for adding and deleting items from the todo list.
//👇🏻 handles input change
const handleInputChange = (e) => {
dispatch({ type: ACTIONS.createTodo, payload: { input: e.target.value } });
};
//👇🏻 handles add todo
const handleAddInput = (e) => {
e.preventDefault();
//👇🏻 ensures the user's input is not empty
if (state.todoInput.trim()) {
dispatch({ type: ACTIONS.addTodo, payload: { todo: state.todoInput } });
dispatch({ type: ACTIONS.createTodo, payload: { input: "" } });
}
};
//👇🏻 handles delete todo
const handleDeleteTodo = (id) => {
dispatch({ type: ACTIONS.deleteTodo, payload: { id } });
};
The handleInputChange function updates the todoInput state with the user's input. The handleAddInput function adds the user's todo to the list and resets the input field to empty after the user enters a new todo. The handleDeleteTodo function accepts a selected todo ID and removes it from the todo list.
How to manage states using the Context API
React Context API is a state management technique that enables us to manage states within our application by passing props from a parent component into its child components.
For instance, when a user signs into your application, you need to customize the user's experience within the application by displaying the user name on a few pages.
Without Context API, you may have to pass the username as a prop into every page or parent component until it gets to the exact component where the username is needed.
The process of passing states as props through multiple parent components until they get to the deeply nested component where it is needed is called Prop drilling.
This process makes your code harder to maintain and understand because parent components that do not need the props have to accept it and pass it to the deeply nested components.
Using Context API helps solve this issue because it allows you to declare the shared state within a context and wraps your entire application, allowing React components to access the context directly without passing the value from one component level to another.
To demonstrate how it works, let's add a login page to the todo list application. This page will save the username in a context, allowing us to access its value within other pages of the application.
First, consider that we have two components: a Login component and a Todo component in our application. After logging into the application, the username is displayed at the top of the Todo page.
Therefore, we will save the username within the context and access it from the context within the Todo page.
Before we proceed, React provides two methods to enable us to use the Context API: createContext and useContext. Their names explain exactly what they are used for.
Next, you need to create the AppContext.js file that stores the current user’s name.
import { createContext, useState } from "react";
//👇🏻 creates the context
export const AppContext = createContext();
//👇🏻 provides the stored value into other components
export const AppProvider = (props) => {
const [username, setUsername] = useState("");
const updateUsername = (value) => {
setUsername(value);
};
return (
<AppContext.Provider value={[username, updateUsername]}>
{props.children}
</AppContext.Provider>
);
};
The code snippet creates the username state within the Context and makes it available to all components within the application through the AppContext.Provider component.
Wrap the entire application with the AppProvider component to grant access to its values for all other components.
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./index.css";
import { AppProvider } from "./context/AppContext.jsx";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<AppProvider>
<App />
</AppProvider>
</React.StrictMode>
);
Create the login form that updates the username stored within the app context.
import { useContext } from "react";
import { useNavigate } from "react-router-dom";
import { AppContext } from "../context/AppContext";
const Login = () => {
//👇🏻 uses the App Context
const [username, updateUsername] = useContext(AppContext);
const navigate = useNavigate();
const handleNameChange = (e) => {
updateUsername(e.target.value);
};
const handleSignin = (e) => {
e.preventDefault();
//👇🏻 navigates to the Todos component
navigate("/todos");
};
return (
<div>
<h2>Sign in to your application</h2>
<form onSubmit={handleSignin}>
<label htmlFor='username'>Username</label>
<input
type='text'
name='username'
value={username}
onChange={(e) => handleNameChange(e)}
/>
<button>Sign in </button>
</form>
</div>
);
};
export default Login;
The code snippet above accepts the username and update its value within the context.
Finally, display the username within the Todos component.
import { useContext } from "react";
import { AppContext } from "../context/AppContext";
const Todos = () => {
const [username, setUsername] = useContext(AppContext);
//...other state management functions
return (
<div className='container'>
<header>
<h2> Todo List</h2>
<h3 className='username'>{username}</h3>
</header>
{state.todos.map((td) => (
<div key={td.id} className='todo_item'>
<p>{td.todo}</p>
<button onClick={() => handleDeleteTodo(td.id)}>Delete</button>
</div>
))}
<form className='input_container' onSubmit={handleAddInput}>
<input
type='text'
value={state.todoInput}
onChange={(e) => handleInputChange(e)}
className='inputField'
/>
<button type='submit'> Add Todo</button>
</form>
</div>
);
};
The code snippet above gets the username from the context and displays it on the page.
How to manage states using Redux Toolkit in React
Redux Toolkit is another alternative to the React Context API that allows us to manage states, and its functionality is similar to the useReducer hook in React.
With Redux Toolkit, you can create a store within your application that all your components can communicate with, similar to how components access context in the React Context API.
Redux Toolkit also allows you to create a slice for your states, providing a simplified way of writing the reducers and actions for that state.
Before you can use Redux Toolkit, you need to install the React Redux and Redux Toolkit packages.
npm install react-redux @reduxjs/toolkit
To illustrate how Redux Toolkit works, let's modify the login page to use Redux Toolkit for storing and updating the username.
First, create a redux folder containing the username slice, as shown below.
// In redux/username.js
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
value: "",
};
export const usernameSlice = createSlice({
name: "username",
initialState,
reducers: {
updateName: (state, action) => {
state.value = action.payload;
},
},
});
export const { updateName } = usernameSlice.actions;
export default usernameSlice.reducer;
From the code snippet, the createSlice
function accepts the state name, its initial value, and all the reducer functions related to the state in an object. Ensure you export the reducers at the end of the file.
Create a store/store.js within your application containing the entire Redux states.
import { configureStore } from "@reduxjs/toolkit";
import userNameReducer from "../redux/username";
export const store = configureStore({
reducer: {
username: userNameReducer,
},
});
Finally, make the store available to all components by wrapping your entire application with the Provider component from React Redux.
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import { Provider } from "react-redux"
import { store } from './store/store.js'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
)
Interacting with the Redux Toolkit
Redux Toolkit provides two hooks that enable us to interact with the states. They are the useSelector and useDipatch hook.
The useSelector hook enables us to select a particular state declared within Redux, and the useDispatch hook enables us to trigger different reducer functions.
Select the username state and update its value when the user logs into the application.
import { useNavigate } from "react-router-dom";
import { useSelector, useDispatch } from "react-redux";
import { updateName } from "../redux/username";
const Login = () => {
//selects the username Redux state
const username = useSelector((state) => state.username.value);
//initializes the useDispatch hook
const dispatch = useDispatch();
const navigate = useNavigate();
const handleNameChange = (e) => {
//updates the username state
dispatch(updateName(e.target.value));
};
const handleSignin = (e) => {
e.preventDefault();
navigate("/todos");
};
return (
<div className='mid_container'>
<h2>Sign in to your application</h2>
<form onSubmit={handleSignin} className='loginForm'>
<label htmlFor='username'>Username</label>
<input
type='text'
name='username'
value={username}
onChange={(e) => handleNameChange(e)}
className='inputField'
/>
<button>Sign in </button>
</form>
</div>
);
};
The code snippet above selects the username value from the Redux state and updates it using the useDispatch hook when users enter their username.
//selects the username value
const username = useSelector((state) => state.username.value);
const dispatch = useDispatch();
const handleNameChange = (e) => {
// updates the username state
dispatch(updateName(e.target.value));
};
How to use URLs to store states in React
This method is quite uncommon but very useful in cases where you want to persist or bookmark states within your application. For example, when you want your users to share a page's URL, and when other users visit the page, they should see the same user interface.
Suppose you are building an e-commerce website, and a user selects a variant of the product that they would love to share with a friend. How do you ensure that once the friend visits the page, they get the same view?
The URL state management method is the best solution. It modifies the page URL based on the data provided by the user. To demonstrate how it works, let's add a search functionality to the todo list application.
Before we proceed, you need to install the React Router package.
npm install react-router-dom
The React Router package provides the useSearchParams hook that enables us to modify and store data within a page's URL.
import { useSearchParams } from "react-router-dom";
const TodoList = () => {
const [searchParams, setSearchParams] = useSearchParams({ search: "" });
const filterText = searchParams.get("search");
return <div>{/**--App UI--**/}</div>;
};
The code snippet above adds a
search
query parameter to the page's URL, and it becomes something like this: http://localhost:5173/todos?search=team+hangout
Next, add a filter form field to the Todo list page to enable users to filter the todos via its name. The user's input also gets added to the page's URL as the input field is updated.
const TodoList = () => {
//...other state functions
return (
<div className='container'>
<header>
<h2> Todo List</h2>
<h3>{username}</h3>
</header>
{/*--filter todo form--*/}
<form>
<h2>Filter Todo</h2>
<input
type='text'
value={filterText}
onChange={(e) => handleFilter(e)}
/>
</form>
{state.todos.map((td) => (
<div key={td.id}>
<p>{td.todo}</p>
<button onClick={() => handleDeleteTodo(td.id)}>Delete</button>
</div>
))}
<form onSubmit={handleAddInput}>
<input
type='text'
value={state.todoInput}
onChange={(e) => handleInputChange(e)}
/>
<button type='submit'> Add Todo</button>
</form>
</div>
);
};
Create the handleFilter function that filters the todos, updates the page's URL, and returns only the todos that match the user's input.
const handleFilter = (e) => {
const filteredResult = state.todos.filter((item) =>
item.todo.toLowerCase().startsWith(e.target.value.toLowerCase())
);
setSearchParams(
(prev) => {
prev.set("search", e.target.value);
return prev;
},
{ replace: true }
);
if (filteredResult.length !== 0) {
dispatch({ type: ACTIONS.setTodo, payload: { todos: filteredResult } });
}
};
In conclusion, the useSearchParams hook provides a simple way for you to manage states effectively and provides a better user experience for your users.
Key Takeaways
So far, you have learned how to manage states using the useState and the useReducer hooks, the React Context API, the Redux Toolkit, and a page's URL.
Here are a few things to note:
- Use the useState hook for simple state management.
- Use useReducer when you have numerous states within a component or for states with complex transitions.
- Use the React Context API or Redux Toolkit for applications where multiple components require or modify a shared state.
- Use the URL method to store states, especially when you need to keep states even after a page refresh.
Thank you for reading.
If you like this blog and want to read more about ReactJS and JavaScript then start reading some of recent articles.
Top comments (0)