State management is a fundamental aspect of React. React provides different native ways to manage and maintain state. One of the most commonly used way to manage a state is useState
hook.
When you start your journey of learning hooks in React, useState
will probably be your first destination. The useState
hook provides a simple API to manage a component state. Below is an example of useState
hook.
const [count, setCount] = useState(0)
The useState
hook returns two variables:
- count - an actual state
- setCount - a state updater function
A React component generally contains the logic that returns some JSX i.e. the logic to render UI. When you add state management to the component then it contains both logic for rendering a UI and managing the state. When a component gets big, it becomes quite challenging to maintain both state management and UI rendering logic. Although, this works totally fine still a React component should separate the UI and state management logic.
Let’s look at the below example:
import { useState } from "react";
import { v4 as uuidv4 } from "uuid";
const ListState = () => {
const defaultTodo = [
{ id: uuidv4(), title: "\"Write a new blog\", isComplete: false },"
{ id: uuidv4(), title: "\"Read two pages of Rework\", isComplete: true },"
{ id: uuidv4(), title: "\"Organize playlist\", isComplete: false },"
];
const [todo, setTodo] = useState(defaultTodo);
const [item, setItem] = useState("");
// Marks a given todo as complete or incomplete
const handleTodoChange = (id) => {
const updatedTodos = todo.map((item) => {
if (item.id === id) {
return { ...item, isComplete: !item.isComplete };
}
return item;
});
setTodo(updatedTodos);
};
// Adds a new todo item to the list
const handleAddItem = (e) => {
e.preventDefault();
const updatedTodos = [
...todo,
{ id: uuidv4(), title: "item, isComplete: false },"
];
setTodo(updatedTodos);
setItem("");
};
// Removes todo item from the list based on given ID
const handleDeleteItem = (id) => {
const updatedTodos = todo.filter((item) => item.id !== id);
setTodo(updatedTodos);
};
return (
<div className="container">
<div className="new-todo">
<input
placeholder="Add New Item"
value={item}
onChange={(e) => setItem(e.target.value)}
className="todo-input"
/>
<button onClick={handleAddItem} className="add-todo">
Add
</button>
</div>
<ul className="todo-list">
{todo.length > 0 ? (
todo.map((item) => (
<li key={item.id} className="list-item">
<input
type="checkbox"
checked={item.isComplete}
onChange={() => handleTodoChange(item.id)}
/>
<p>{item.title}</p>
<button
onClick={() => handleDeleteItem(item.id)}
className="delete-todo"
>
Delete
</button>
</li>
))
) : (
<p>List is empty</p>
)}
</ul>
</div>
);
};
export default ListState;
The above code renders a simple Todo list where a user can do the following things:
- Add a new todo item
- Delete a todo item
- Mark a todo as complete/incomplete.
The above component contains both the logic for handling state and rendering JSX.
React provides a native hook called useReducer
using which we can separate the logic for UI and state management. A useReducer
hook is an alternative to useState
hook.
What is useReducer
?
The useReducer
hook allows you to add state management to your component similar to useState
hook. The only difference is that it does this by using a reducer pattern. The API for useReducer
hook is as follows:
const [state, dispatch] = useReducer(reducerFn, initialState)
The useReducer
hook takes two parameters:
- A reducer function that contains the logic for state management.
- An initial state
The useReducer
hook returns two variables:
- The current state
- A dispatch function which when called invokes the reducer function to update the state.
Let’s learn to use this hook by refactoring the above Todo list component.
import { useState, useReducer } from "react";
import { ListReducerFn } from "../utils/functions";
import { defaultTodo } from "../utils/defaults";
const ListReducer = () => {
const [todo, dispatch] = useReducer(ListReducerFn, defaultTodo);
const [item, setItem] = useState("");
// Marks a given todo as complete or incomplete
const handleTodoChange = (id) =>
dispatch({ type: "UPDATE", payload: { id } });
// Adds a new todo item to the list
const handleAddItem = (e) => {
e.preventDefault();
dispatch({ type: "ADD", payload: { item } });
setItem("");
};
// Removes todo item from the list based on given ID
const handleDeleteItem = (id) =>
dispatch({ type: "DELETE", payload: { id } });
return (
<div className="container">
<div className="new-todo">
<input
placeholder="Add New Item"
value={item}
onChange={(e) => setItem(e.target.value)}
className="todo-input"
/>
<button onClick={handleAddItem} className="add-todo">
Add
</button>
</div>
<ul className="todo-list">
{todo.length > 0 ? (
todo.map((item) => (
<li key={item.id} className="list-item">
<input
type="checkbox"
checked={item.isComplete}
onChange={() => handleTodoChange(item.id)}
/>
<p>{item.title}</p>
<button
onClick={() => handleDeleteItem(item.id)}
className="delete-todo"
>
Delete
</button>
</li>
))
) : (
<p>List is empty</p>
)}
</ul>
</div>
);
};
export default ListReducer;
// utils/defaults.js
import { v4 as uuidv4 } from "uuid";
export const defaultTodo = [
{ id: uuidv4(), title: "Write a new blog", isComplete: false },
{ id: uuidv4(), title: "Read two pages of Rework", isComplete: true },
{ id: uuidv4(), title: "Organize playlist", isComplete: false },
];
// utils/functions.js
import { v4 as uuidv4 } from "uuid";
export const ListReducerFn = (state, action) => {
switch (action.type) {
case "ADD": {
const updatedTodos = [
...state,
{ id: uuidv4(), title: action.payload.item, isComplete: false },
];
return updatedTodos;
}
case "DELETE": {
const updatedTodos = state.filter(
(item) => item.id !== action.payload.id
);
return updatedTodos;
}
case "UPDATE": {
const updatedTodos = state.map((item) => {
if (item.id === action.payload.id) {
return { ...item, isComplete: !item.isComplete };
}
return item;
});
return updatedTodos;
}
default: {
throw new Error(`Unsupported action: ${action.type}`);
}
}
};
As you can see, the JSX for both versions of Todo list component is same. The only difference is in the logic for handling state management. Let’s discuss the differences between the Todo list component that uses useState
and useReducer
hooks.
Here is a function to add a new todo list item written using useState
logic.
// Adds a new todo item to the list (useState version)
const handleAddItem = (e) => {
e.preventDefault();
const updatedTodos = [
...todo,
{ id: uuidv4(), title: item, isComplete: false },
];
setTodo(updatedTodos);
setItem("");
};
In the above code, handleAddItem
function is used for adding a new todo item to the list. We are creating a new array of updated todo list items and setting the state with updated todo list that also includes newly added item. We are updating the state directly from the function itself.
Let’s look at the useReducer
version of the same function.
// Adds a new todo item to the list (useReducer version)
const handleAddItem = (e) => {
e.preventDefault();
dispatch({ type: "ADD", payload: { item } });
setItem("");
};
The above function does the same thing i.e. it adds a new todo item to the list. In this scenario, the state management logic is separated and we are not directly updating the state from the function. Instead, here we are using dispatch
function provided by useReducer
to dispatch an action. Whenever an action is dispatched, the reducer function is called to modify and return the new state based on the type of action.
What is dispatch
function?
A dispatch is a special function that dispatches an action object. It basically acts as a request to update the state. The dispatch
function takes an action object as a parameter. You can pass anything to dispatch function but generally you should pass only the useful information to update the state.
Here is a sample action object.
const action = {
type: "ADD",
payload: {
todo: "Write a new blog article"
}
}
The action object should generally contain following two properties:
- type - It basically tells reducer what type of action has happened. (eg. ‘ADD’, ‘DELETE’ etc.)
- payload (optional) - An optional property with some additional information required to update the state.
The dispatch function invokes the reducer function to update and return the new state. The logic to update the state is done inside the reducer function instead of a React component. In this way, the state management can be separated from UI rendering logic.
What is a reducer
function?
A reducer function handles all the logic of how a state should be modified. It takes two parameters:
- A current state
- An action
Following is a sample reducer function that handles the logic of updating todo list state.
export const ListReducerFn = (state, action) => {
switch (action.type) {
case "ADD": {
const updatedTodos = [
...state,
{ id: uuidv4(), title: action.payload.item, isComplete: false },
];
return updatedTodos;
}
case "DELETE": {
const updatedTodos = state.filter(
(item) => item.id !== action.payload.id
);
return updatedTodos;
}
case "UPDATE": {
const updatedTodos = state.map((item) => {
if (item.id === action.payload.id) {
return { ...item, isComplete: !item.isComplete };
}
return item;
});
return updatedTodos;
}
default: {
throw new Error(`Unsupported action: ${action.type}`);
}
}
};
Whenever an action is dispatched from a component, the reducer function is invoked to update and return the new state.
TL;DR
The useState
and useReducer
hooks allow you to add state management to the React component. The useReducer
hook provides more flexibility as it allows you to separate UI rendering and state management logic.
You can check the code for above sample here.
Top comments (0)