Mastering React’s useReducer Hook: A Comprehensive Guide with Examples

Love Trivedi
ZestGeek
Published in
4 min readAug 8, 2024

React’s hooks API provides various ways to manage state and side effects in functional components. One of the most powerful and flexible hooks for state management is useReducer. This hook is particularly useful for handling complex state logic in a predictable manner. In this article, we'll explore the useReducer hook, explaining its use cases and providing practical coding examples to demonstrate its effectiveness.

What is useReducer?

useReducer is a hook that allows you to manage state using a reducer function. It's similar to how Redux manages state, but it's built into React for use within components. The reducer function takes the current state and an action, and returns a new state based on the action type.

Syntax

const [state, dispatch] = useReducer(reducer, initialState);
  • reducer: A function that determines the next state based on the current state and an action.
  • initialState: The initial state value.
  • state: The current state.
  • dispatch: A function to send actions to the reducer.

When to Use useReducer

  • Complex State Logic: When state logic involves multiple sub-values or when the next state depends on the previous state.
  • Centralized State Management: When you want to manage state transitions in a single place, making it easier to debug and test.

Real-Life Coding Examples

Example 1: Counter Component

A simple example to demonstrate the basics of useReducer is a counter component:

import React, { useReducer } from 'react';

const initialState = { count: 0 };

function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
}

function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);

return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
);
}

export default Counter;

In this example, the reducer function handles the logic for incrementing and decrementing the counter. The dispatch function is used to send actions to the reducer.

Example 2: Todo List

A more complex example is managing a todo list. This involves adding, removing, and toggling the completion status of todos:

import React, { useReducer, useState } from 'react';

const initialState = [];

function reducer(state, action) {
switch (action.type) {
case 'add':
return [...state, { id: Date.now(), text: action.text, completed: false }];
case 'toggle':
return state.map(todo =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
);
case 'remove':
return state.filter(todo => todo.id !== action.id);
default:
throw new Error();
}
}

function TodoList() {
const [state, dispatch] = useReducer(reducer, initialState);
const [text, setText] = useState('');

return (
<div>
<form
onSubmit={e => {
e.preventDefault();
dispatch({ type: 'add', text });
setText('');
}}
>
<input value={text} onChange={e => setText(e.target.value)} />
<button type="submit">Add Todo</button>
</form>
<ul>
{state.map(todo => (
<li key={todo.id}>
<span
style={{
textDecoration: todo.completed ? 'line-through' : 'none',
}}
onClick={() => dispatch({ type: 'toggle', id: todo.id })}
>
{todo.text}
</span>
<button onClick={() => dispatch({ type: 'remove', id: todo.id })}>
Remove
</button>
</li>
))}
</ul>
</div>
);
}

export default TodoList;

In this example, the reducer function handles the logic for adding, toggling, and removing todos. The useReducer hook provides a clear and predictable way to manage the state transitions of the todo list.

Example 3: Form Management

Managing the state of a complex form can be simplified with useReducer:

import React, { useReducer } from 'react';

const initialState = {
name: '',
email: '',
message: '',
};

function reducer(state, action) {
switch (action.type) {
case 'updateField':
return {
...state,
[action.field]: action.value,
};
case 'reset':
return initialState;
default:
throw new Error();
}
}

function ContactForm() {
const [state, dispatch] = useReducer(reducer, initialState);

const handleChange = (e) => {
dispatch({
type: 'updateField',
field: e.target.name,
value: e.target.value,
});
};

const handleSubmit = (e) => {
e.preventDefault();
// Submit form logic
console.log(state);
dispatch({ type: 'reset' });
};

return (
<form onSubmit={handleSubmit}>
<input
name="name"
value={state.name}
onChange={handleChange}
placeholder="Name"
/>
<input
name="email"
value={state.email}
onChange={handleChange}
placeholder="Email"
/>
<textarea
name="message"
value={state.message}
onChange={handleChange}
placeholder="Message"
/>
<button type="submit">Submit</button>
</form>
);
}

export default ContactForm;

Here, the reducer function manages the state of each form field and handles the form reset action. This approach ensures that form state management is centralized and easy to maintain.

Conclusion

The useReducer hook in React provides a robust way to manage state, particularly when dealing with complex state logic. By using a reducer function to determine state transitions, you can keep your state management logic clear and predictable. The real-life examples provided here illustrate how useReducer can be effectively used in different scenarios, from simple counters to complex form management.

Whether you are managing a simple state or a complex application, useReducer can help you handle state transitions in a clean and efficient manner, making your React applications more maintainable and scalable.

--

--

Love Trivedi
ZestGeek

Full Stack Developer | Problem Solver | Knowledge Share, 🚀 Expertise: JavaScript enthusiast specializing in ReactJS, Angular, and Node.js.