November 4, 2022

React useReducer Hook With Examples

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-

  1. useReducer separates the state management logic from the rendering logic which should actually be the focus of the component.
  2. 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-

  1. You will have to import useReducer in order to use it.
  2. Initial state is defined as an object with count property having initial value as 0.
    const initialState = {
        count:0
    };
    
  3. useReducer is called with a reducer function and initial state.
    const [state, dispatch] = useReducer(reducer, initialState);
    
  4. 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>
    
  5. 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-

  1. 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.
  2. 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.
  3. 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.
useReducer React example

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

No comments:

Post a Comment