In React, the useReducer hook is an alternative to useState for managing more complex state logic. It is particularly useful when the state depends on previous state values, or when the state logic involves multiple sub-values, as is the case with more complex objects or arrays. It can be compared to how you would use a reducer in Redux, but on a local component level.
Basic Syntax of useReducer
The useReducer hook takes in two arguments:
- A reducer function that contains the state logic.
- An initial state value.
It returns an array with two elements:
- The current state.
- A dispatch function to trigger state updates.
const [state, dispatch] = useReducer(reducer, initialState);
The Reducer Function
The reducer function is responsible for determining how the state should be updated based on the action passed to it. It accepts two arguments:
- state: The current state.
- action: An object containing information (such as
typeandpayload) about how the state should be updated.
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();
}
}
Example 1: Simple Counter with useReducer
In this example, we use useReducer to manage the state of a counter.
import React, { useReducer } from 'react';
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('Unknown action');
}
}
function Counter() {
const initialState = { count: 0 };
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
</div>
);
}
export default Counter;
Explanation:
- The
reducerfunction manages the state updates based on theaction.type. - The
dispatchfunction is used to send actions to the reducer (e.g.,dispatch({ type: 'increment' })). state.countis rendered, and we can increment or decrement it via buttons.
Example 2: Managing Form State with useReducer
In this example, we manage a form’s state with useReducer. It handles multiple fields and updates them using a single reducer function.
import React, { useReducer } from 'react';
const initialState = {
username: '',
email: '',
};
function formReducer(state, action) {
switch (action.type) {
case 'SET_FIELD':
return {
...state,
[action.field]: action.value,
};
case 'RESET_FORM':
return initialState;
default:
return state;
}
}
function UserForm() {
const [state, dispatch] = useReducer(formReducer, initialState);
const handleChange = (e) => {
dispatch({
type: 'SET_FIELD',
field: e.target.name,
value: e.target.value,
});
};
const handleReset = () => {
dispatch({ type: 'RESET_FORM' });
};
return (
<div>
<form>
<div>
<label>Username:</label>
<input
type="text"
name="username"
value={state.username}
onChange={handleChange}
/>
</div>
<div>
<label>Email:</label>
<input
type="email"
name="email"
value={state.email}
onChange={handleChange}
/>
</div>
<button type="button" onClick={handleReset}>Reset</button>
</form>
<p>Username: {state.username}</p>
<p>Email: {state.email}</p>
</div>
);
}
export default UserForm;
Explanation:
- We store multiple form fields (
usernameandemail) in the state. - The reducer handles updates by looking at the
action.fieldand updating the corresponding value (action.value). - The form input elements trigger the
handleChangemethod, which dispatches an action to update the state. - A reset button calls
dispatch({ type: 'RESET_FORM' })to reset the form back to the initial state.
When to Use useReducer Over useState
While useState is often more straightforward, useReducer is preferred when:
- Complex state logic: If the state logic involves multiple sub-values or is deeply nested.
- State transition logic: When state transitions depend on the previous state.
- Multiple actions: When many types of actions need to be handled (e.g., form handling with multiple fields).
- Non-trivial state updates: If the updates to the state are non-trivial and would involve complex state manipulation.
Example 3: Complex State Management
If your state involves multiple values and actions, useReducer becomes more readable and manageable compared to multiple useState calls.
import React, { useReducer } from 'react';
const initialState = { loading: false, error: null, data: [] };
function dataFetchReducer(state, action) {
switch (action.type) {
case 'FETCH_INIT':
return {
...state,
loading: true,
error: null,
};
case 'FETCH_SUCCESS':
return {
...state,
loading: false,
data: action.payload,
};
case 'FETCH_FAILURE':
return {
...state,
loading: false,
error: action.error,
};
default:
throw new Error();
}
}
function DataFetchingComponent() {
const [state, dispatch] = useReducer(dataFetchReducer, initialState);
const fetchData = async () => {
dispatch({ type: 'FETCH_INIT' });
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
dispatch({ type: 'FETCH_SUCCESS', payload: data });
} catch (error) {
dispatch({ type: 'FETCH_FAILURE', error: error.message });
}
};
return (
<div>
{state.loading ? <p>Loading...</p> : <ul>{state.data.map(item => <li key={item.id}>{item.name}</li>)}</ul>}
{state.error && <p>Error: {state.error}</p>}
<button onClick={fetchData}>Fetch Data</button>
</div>
);
}
export default DataFetchingComponent;
Explanation:
- This example manages loading, error, and data states for data fetching.
- Different actions (
FETCH_INIT,FETCH_SUCCESS,FETCH_FAILURE) control how the state is updated during the data-fetching process.
Summary
useReduceris ideal for managing complex state logic or state transitions.- It works well for scenarios where you have multiple actions or depend on the previous state for updates.
- The pattern is similar to managing state in Redux, making it scalable for components with more complex logic.
