Back to all posts

useReducer(): manage complex state


It is an alternative to using the useState hook and can be helpful when your component’s state logic becomes more complex and involves multiple actions.
Practical Example

// login.jsx

import React, { useState, useEffect, useReducer } from "react";

import Card from "../UI/Card/Card";
import classes from "./Login.module.css";
import Button from "../UI/Button/Button";

const emailReducer = (state, action) => {
  console.log(action);

  // checking action type
  if (action.type === "USER_INPUT") {
    return { value: action.val, isValid: action.val.includes("@") };
  }

  if (action.type === "INPUT_BLUR") {
    return { value: state.value, isValid: state.value.includes("@") };
  }

  // if action type not matched
  return {
    value: "",
    isValid: false
  };
};

const passwordReducer = (state, action) => {
  console.log(action);

  if (action.type === "USER_INPUT") {
    return {
      value: action.val,
      isValid: action.val.trim().length > 6
    };
  }

  if (action.type === "INPUT_BLUR") {
    return { value: state.value, isValid: state.value.trim().length > 6 };
  }

  return {
    value: "",
    isValid: false
  };
};

const Login = (props) => {
  // const [enteredEmail, setEnteredEmail] = useState("");
  // const [emailIsValid, setEmailIsValid] = useState();
  // const [enteredPassword, setEnteredPassword] = useState("");
  // const [passwordIsValid, setPasswordIsValid] = useState();
  //above commented code replaced with useReducer

  const [formIsValid, setFormIsValid] = useState(false);

  const [emailState, dispatchEmail] = useReducer(emailReducer, {
    value: "",
    isValid: null
  });

  const [passwordState, dispatchPassword] = useReducer(passwordReducer, {
    value: "",
    isValid: null
  });

  const { isValid: emailIsValid } = emailState;
  const { isValid: passwordIsValid } = passwordState;

  useEffect(() => {
    const identifier = setTimeout(() => {
      console.log("Checking form Validity");
      setFormIsValid(emailState.isValid && passwordState.isValid);
    }, 500);

    return () => {
      console.log("CLEANUP");
      clearTimeout(identifier); // clearing timeout
    };
  }, [emailIsValid, passwordIsValid]); // Run every time enteredEmail and enteredPassword state changes

  const emailChangeHandler = (event) => {
    // setEnteredEmail(event.target.value);
    dispatchEmail({
      type: "USER_INPUT",
      val: event.target.value
    });

    // setFormIsValid(event.target.value.includes("@") && passwordState.isValid);
  };

  const passwordChangeHandler = (event) => {
    // setEnteredPassword(event.target.value);
    dispatchPassword({
      type: "USER_INPUT",
      val: event.target.value
    });

    // setFormIsValid(emailState.isValid && event.target.value.trim().length > 6);
  };

  const validateEmailHandler = () => {
    // setEmailIsValid(emailState.isValid);

    dispatchEmail({
      type: "INPUT_BLUR"
      // val: event.target.value
    });
  };

  const validatePasswordHandler = () => {
    // setPasswordIsValid(enteredPassword.trim().length > 6);
    dispatchPassword({
      type: "INPUT_BLUR"
      // val: event.target.value
    });
  };

  const submitHandler = (event) => {
    event.preventDefault();
    props.onLogin(emailState.value, passwordState.value);
  };

  return (
    <Card className={classes.login}>
      <form onSubmit={submitHandler}>
        <div
          className={`${classes.control} ${
            emailState.isValid === false ? classes.invalid : ""
          }`}
        >
          <label htmlFor="email">E-Mail</label>
          <input
            type="email"
            id="email"
            value={emailState.value}
            onChange={emailChangeHandler}
            onBlur={validateEmailHandler}
          />
        </div>
        <div
          className={`${classes.control} ${
            passwordState.isValid === false ? classes.invalid : ""
          }`}
        >
          <label htmlFor="password">Password</label>
          <input
            type="password"
            id="password"
            value={passwordState.value}
            onChange={passwordChangeHandler}
            onBlur={validatePasswordHandler}
          />
        </div>
        <div className={classes.actions}>
          <Button type="submit" className={classes.btn} disabled={!formIsValid}>
            Login
          </Button>
        </div>
      </form>
    </Card>
  );
};

export default Login;

Another Example

import React, { useReducer } from 'react';

// Step 1: Create the reducer function
const initialState = { count: 0 };

const counterReducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 };
    case 'DECREMENT':
      return { ...state, count: state.count - 1 };
    default:
      return state;
  }
};

const Counter = () => {
  // Step 2: Use the useReducer hook to initialize state and get dispatch function
  const [state, dispatch] = useReducer(counterReducer, initialState);

  // Step 3: Define functions to dispatch actions
  const increment = () => {
    dispatch({ type: 'INCREMENT' });
  };

  const decrement = () => {
    dispatch({ type: 'DECREMENT' });
  };

  return (
    <div>
      <h1>Counter: {state.count}</h1>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
};

export default Counter;

Adding Nested Properties As Dependencies To useEffect

In above example. We used object destructuring to add object properties as dependencies to useEffect().

const { someProperty } = someObject;
useEffect(() => {
  // code that only uses someProperty ...
}, [someProperty]);

This is a very common pattern and approach, which is why I typically use it and why I show it here (I will keep on using it throughout the course).

I just want to point out, that they key thing is NOT that we use destructuring but that we pass specific properties instead of the entire object as a dependency.

We could also write this code and it would work in the same way.

useEffect(() => {
  // code that only uses someProperty ...
}, [someObject.someProperty]);

This works just fine as well!

But you should avoid this code:

useEffect(() => {
  // code that only uses someProperty ...
}, [someObject]);

Why?

Because now the effect function would re-run whenever ANY property of someObject changes – not just the one property (someProperty in the above example) our effect might depend on.

useReducer Vs useState

Credits: https://www.udemy.com/course/react-the-complete-guide-incl-redux/

Use Reducer using TypeScript

import { useReducer } from 'react';

type StateType = {
  count: number;
};

type ActionType =
  | { type: 'INCREMENT'; payload: number }
  | { type: 'DECREMENT'; payload: number };

const reducer = (state: StateType, action: ActionType): StateType => {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + action.payload };
    case 'DECREMENT':
      return { count: state.count - action.payload };
    default:
      return state;
  }
};

const initialState: StateType = {
  count: 0,
};

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

  const incrementHandler = (): void => {
    dispatch({
      type: 'INCREMENT',
      payload: 1,
    });
  };

  const decrementHandler = (): void => {
    dispatch({
      type: 'DECREMENT',
      payload: 1,
    });
  };

  return (
    <>
      <h1>Count Changes</h1>
      <div>
        <p>Count: {state.count}</p>
        <div>
          <button onClick={incrementHandler}>+</button>
          <button onClick={decrementHandler}>-</button>
        </div>
      </div>
    </>
  );
}

export default App;

Check this example: https://stackblitz.com/edit/vitejs-vite-5xrzgk?embed=1&file=src%2FApp.tsx