Using the useReducer Hook in React

Last Updated on June 17, 2024

import {useReducer} from "react";

interface CounterAction {
    type: CounterType,
    data: number
}

enum CounterType {
    INCREASE,
    DECREASE
}

interface CounterState {
    count: number
}

function CounterReducer(state: CounterState, action: CounterAction): CounterState {

    if (action.type === CounterType.INCREASE) {
        return {...state, count: state.count + action.data}
    } else if (action.type === CounterType.DECREASE) {
        return {...state, count: state.count - action.data}
    }

    return {...state};
}

function Counter() {

    // initial value for counter
    const counterInitializerArg: CounterState = {count: 0};
    // counter value
    let counterState: CounterState;
    // this is a function with CounterAction as parameter, it will increase/decrease counter value by 1
    let counterDispatch: (payload: CounterAction) => void;

    [counterState, counterDispatch] = useReducer(CounterReducer, counterInitializerArg);

    return (
        <>
            <div>
                Count : {counterState.count}
            </div>
            <div>
                <button type="button" className="btn"
                        onClick={event => counterDispatch({type: CounterType.INCREASE, data: 1})}>Increase
                </button>
                <button type="button" className="btn"
                        onClick={event => counterDispatch({type: CounterType.DECREASE, data: 1})}>Decrease
                </button>
            </div>
        </>
    );
}

export default Counter;

Specifying the initial state

There are two different ways to initialize useReducer state. You may choose either one depending on the use case. The simplest way is to pass the initial state as a second argument:

const [state, dispatch] = useReducer(
  reducer,
  {count: initialCount}
);

Lazy initialization

You can also create the initial state lazily. To do this, you can pass an init function as the third argument. The initial state will be set to init(initialArg).

It lets you extract the logic for calculating the initial state outside the reducer. This is also handy for resetting the state later in response to an action:

function init(initialCount) {
  return {count: initialCount};
}

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

function Counter({initialCount}) {
  const [state, dispatch] = useReducer(reducer, initialCount, init);
  return (
    <>
      Count: {state.count}
      <button
        onClick={() => dispatch({type: 'reset', payload: initialCount})}>
        Reset
      </button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

References
https://reactjs.org/docs/hooks-reference.html#usereducer
https://dev.to/craigaholliday/using-the-usereducer-hook-in-react-with-typescript-27m1