You might have used
useState hook in React to manage state in your application and it works fine for most of the cases but there is
another hook useReducer
in React that can also help with managing the state in your application.
useReducer() hook has two advantages over the useState hook which are-
- useReducer separates the state management logic from the rendering logic which should actually be the focus of the
component.
- useReducer() hook is better placed to manage complex state in your application. So, if your state may change
because of several conditions and relies on some complex logic then you can think of using useReducer instead of
useState hook.
React useReducer hook Syntax
useReducer accepts three arguments and returns an array with exactly two values. You can use array destructuring to assign
these array items to variables.
const [state, dispatchFunction] = useReducer(reducerFunction, initialArg, init)
Parameters
1. reducerFunction: The reducer function that specifies how the state gets updated. It gets the state and action as
arguments, and returns the next state. State and action can be of any types. It must be a pure function.
A function is considered pure, if it follows the following rules:
- The function always returns the same output for the same set of arguments.
- The function does not produce any side-effects.
2. initialArg: The value from which the initial state is calculated. It can be a value of any type.
3. init: This is an optional parameter. It is an initializer function that specifies how the initial state is calculated.
If initializer function is not specified, the initial state is set to initialArg. Otherwise, the initial state is set to
the result of calling init(initialArg).
Returns
useReducer returns an array with exactly two values:
1. The current state.
2. The dispatch function that lets you update the state to a different value and trigger a re-render.
How does useReducer hook work
Initially it may be a little confusing to understand how does useReducer() hook work as lot of work is done by React
internally so let's try to understand the working.
1. Initial State- initialArg which is the second parameter in the useReducer specifies the initial state. It can be
of any type but generally you will use an object. For example, if you have a counter state which can be changed by two
actions 'increment' and 'decrement', then initial state of counter may be 0.
const initialState = {
count:0
};
2. Dispatch function- dispatch function is returned by the useReducer hook itself. You don't write the logic for dispatch
function but you call it. While calling the dispatch function you need to pass the action argument.
action argument specifies the action performed by the user. It can be a value of any type. By convention, an action is
usually an object with a type property. For example, on click of Increment button you can dispatch an action of type
'Increment'.
<button onClick={() => dispatch({type: 'Increment'})}>Increment</button>
Optionally you can also send other properties with additional information.
function handleInputChange(e) {
dispatch({
type: 'change_value',
nextValue: e.target.value
});
}
3. Reducer function- The reducer function (first argument in the useReducer) is called automatically when you call
dispatch. You will write the logic for reducer function. Reducer function you've provided will be passed two arguments-
the current state and the action (action argument you've passed to dispatch).
4. Reducer function returns the new state and the state is updated to this new state triggering a re-rendering.
useReducer hook React examples
As the first example let's take the often-used example of incrementing and decrementing count state and showing the
current count using useReducer.
1. useReducer Counter example
import { useReducer } from "react"
const initialState = {
count:0
};
const reducer = (state, action) =>{
let countState;
console.log(action.type)
switch(action.type){
case 'Increment':
countState = {count: state.count + 1};
break;
case 'Decrement':
countState = {count: state.count - 1};
break;
default:
throw new Error();
}
return countState;
}
const Counter = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return(
<div>
<p>Current Count- {state.count}</p>
<button onClick={() => dispatch({type: 'Increment'})}>Increment</button>
<button onClick={() => dispatch({type: 'Decrement'})}>Decerement</button>
</div>
)
}
export default Counter;
Some important points about the code-
- You will have to import useReducer in order to use it.
- Initial state is defined as an object with count property having initial value as 0.
const initialState = {
count:0
};
- useReducer is called with a reducer function and initial state.
const [state, dispatch] = useReducer(reducer, initialState);
- dispatch is called with appropriate actions as click event handling functions.
<button onClick={() => dispatch({type: 'Increment'})}>Increment</button>
<button onClick={() => dispatch({type: 'Decrement'})}>Decerement</button>
- reducer function gets the dispatched action as one of the argument and uses switch statement to determine the
action type and change the state accordingly. Note that reducer function is written outside the component function
that is possible because reducer function doesn't use any data that is inside the Component. That is also one of the
advantages; useReducer separates the state management logic from the rendering logic.
2. useReducer with form example
This is a better example of useReducer where form validation is done using useReducer. To keep this example a bit simple
not much styling is done except some inline styling and it uses two separate reducers to manage form data state and form
validation state. Focus is more on understanding the working of useReducer hook in React.
In the form there are 3 fields first name, last name and email. Initial state for form field values and validation is
defined as given below.
const initialValueState = {
firstName: "",
lastName: "",
email: ""
}
const initialValidityState = {
firstNameError: false,
lastNameError: false,
emailError: false
}
If you want to manage both values and validation states in a single state you can use state object as given below.
As mentioned above, in this example we'll use two separate states.
const initialState = {
firstName: { value: "", touched: false, hasError: true, errorMessage: "" },
lastName: { value: "", touched: false, hasError: true, errorMessage: "" },
email: { value: "", touched: false, hasError: true, errorMessage: "" },
isFormValid: false,
}
Form validation with useReducer
import { useReducer } from "react";
const initialValueState = {
firstName: "",
lastName: "",
email: ""
}
const initialValidityState = {
firstNameError: false,
lastNameError: false,
emailError: false,
isFormValid: false
}
const formReducer = (state, action) => {
const {name, value} = action.type
// Uses Computed property name ES6
return{
...state, [name]: value,
}
}
const formValidityReducer = (state, action) => {
let isValid = false;
switch(action.type){
case "VALIDATE_FIRST_NAME":
isValid = action.data.firstName.trim().length > 0 ? true: false
// For formvalidity validity has to be checked for all the form fields
return{
...state,
...({firstNameError: !isValid, isFormValid: isValid &&
(action.data.lastName.trim() !=="") &&
(action.data.email.trim().length > 0 && action.data.email.includes("@"))})
}
case "VALIDATE_LAST_NAME":
isValid = action.data.lastName.trim().length > 0 ? true: false
return{
...state,
...({lastNameError: !isValid, isFormValid: isValid &&
(action.data.firstName.trim() !=="") &&
(action.data.email.trim().length > 0 && action.data.email.includes("@"))})
}
case "VALIDATE_EMAIL":
isValid = (action.data.email.trim().length > 0 && action.data.email.includes("@") ) ? true: false
return{
...state,
...({emailError: !isValid, isFormValid: isValid &&
(action.data.firstName.trim() !=="") &&
(action.data.lastName.trim() !=="")})
}
default:
return state
}
}
const ReduceForm = () => {
const [formValues, setFormValues] = useReducer(formReducer, initialValueState);
const [formValidity, setFormValidity] = useReducer(formValidityReducer, initialValidityState)
const onSubmitHandler = (event) => {
event.preventDefault();
// just displaying field values
console.log(formValues)
}
return(
<form onSubmit={onSubmitHandler}>
<label htmlFor="firstName">First Name</label>
<input
name="firstName"
onChange={(e) =>setFormValues({type:e.target})}
style={{backgroundColor:formValidity.firstNameError ? "#fce4e4" : ""}}
onBlur={(e) => setFormValidity({type: "VALIDATE_FIRST_NAME", data: formValues})}
type="text"/>
{formValidity.firstNameError && <p style={{display: "inline-block",color: "#cc0033"}}>First name is required</p>}<br />
<label htmlFor="lastName">Last Name</label>
<input
name="lastName"
onChange={(e) =>setFormValues({type:e.target})}
style={{backgroundColor:formValidity.lastNameError ? "#fce4e4" : ""}}
onBlur={(e) => setFormValidity({type: "VALIDATE_LAST_NAME", data: formValues})}
type="text"/>
{formValidity.lastNameError && <p style={{display: "inline-block",color: "#cc0033"}}>Last name is required</p>}
<br />
<label htmlFor="email">Email</label>
<input
name="email"
onChange={(e) =>setFormValues({type:e.target})}
style={{backgroundColor:formValidity.emailError ? "#fce4e4" : ""}}
onBlur={(e) => setFormValidity({type: "VALIDATE_EMAIL", data: formValues})}
type="text"/>
{formValidity.emailError && <p style={{display: "inline-block",color: "#cc0033"}}>Valid email is required</p>}
<br />
<div>
<input disabled={!formValidity.isFormValid} type="submit" value="Submit"/>
</div>
</form>
)
}
export default ReduceForm;
Some points about the code-
- Two useReducer hooks are created, in one reducer function is named formReducer and dispatch function is named setFormValues. In another reducer function is named formValidityReducer and dispatch function is named setFormValidityData.
- In the formfields, onChange event is used to dispatch action for updating field values state and onBlur event is used to dispatch action for updating field validation state.
- Submit button is disabled if the form is not valid. For individual fields input box styling changes and a message is displayed in case of error.
That's all for the topic React useReducer Hook With Examples. If something is missing or you have something
to share about the topic please write a comment.
You may also like