DEV Community

Should you useState or useReducer 🤷? Doesn't matter really. Explained in 3 mins.

The React doc recommends useReducer for handling complex state values. But to me they are equally powerful. Let me show you how.

useReducer can replace useState

First, the simpler case: any useState can be implemented using useReducer. In fact, the useState hook itself is implemented by a reducer.

Let's create a simple React state with useState. The state contains a count number.

type State = { count: number };

const [state, setState] = React.useState<State>({ count: 0 });

We can re-implement the same with useReducer.

type Action = {
  type: 'COUNT_CHANGED',
  count: number,
};

const reducer: React.Reducer<State, Action> = (
  prevState: State,
  action: Action
): State => {
  switch (action.type) {
    case "COUNT_CHANGED":
      return { ...prevState, count: action.count };
  }
};

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

Except for more lines of code, they function exactly the same.

Here useReducer takes in two parameters.

  • The first being a reducer function: (prevState, action) => newState. Upon dispatching an action, it updates (reduces) the prevState to a newState.
  • The second is the initial state, same to the one passed into useState.

We have only one action called COUNT_CHANGED. So the following two lines will trigger the same state update:

// with useState
setState({ count: 1 });

// with useReducer
dispatch({ type: 'COUNT_CHANGED', count: 1 });

useState can replace useReducer, too

One claimed advantage of useReducer is its ability to handle complex state values. Let's create an example here. Let's say we have a root-level form component that contains three input components, and we want each input to handle its own value. The UI looks like below:

    <UserForm>
      <FirstNameInput />
      <LastNameInput />
      <AgeInput />
    </UserForm>

We create a reducer below to handle 3 input values:

// A complex state with user name and age
type UserState = {
  name: {
    first: string,
    last: string,
  },
  age: number,
};

// Three action types to update each state value
type Action =
  | {
      type: "FIRST_NAME_CHANGED";
      first: string;
    }
  | {
      type: "LAST_NAME_CHANGED";
      last: string;
    }
  | {
      type: "AGE_CHANGED";
      age: number;
    };


const reducer: React.Reducer<UserState, Action> = (
  prevState: UserState,
  action: Action
): UserState => {
  switch (action.type) {
    case "FIRST_NAME_CHANGED":
      return { ...prevState, name: { ...prevState.name, first: action.first } };
    case "LAST_NAME_CHANGED":
      return { ...prevState, name: { ...prevState.name, last: action.last } };
    case "AGE_CHANGED":
      return { ...prevState, age: action.age };
  }
};

And now use it in our UserForm component. Note that dispatch is passed into each Input so they can trigger actions to update their own field.

const UserForm = () => {
  const [state, dispatch] = React.useReducer(reducer, {
    name: { first: "first", last: "last" },
    age: 40
  });
  return (
    <React.Fragment>
      <FirstNameInput value={state.name.first} dispatch={dispatch} />
      <LastNameInput value={state.name.last} dispatch={dispatch} />
      <AgeInput value={state.age} dispatch={dispatch} />
    </React.Fragment>
  )
}

Done. This is how useReducer can work for complex states. Now how to convert to useState?

A naive way is passing down one big state object to each Input. We have to pass down the entire state because each Input needs to know the 'full picture' of current state for it to properly construct a new state. Something like below:

// This is a bad example.
const UserForm = () => {
  const [state, setState] = React.useState({
    name: { first: "first", last: "last" },
    age: 40
  });
  return (
    <React.Fragment>
      <FirstNameInput state={state} setState={setState} />
      <LastNameInput state={state} setState={setState} />
      <AgeInput state={state} setState={setState} />
    </React.Fragment>
  )
}

This is bad for several reasons:

  1. No separation of duties: Each Input now requires full state as its props to work. Making it harder to refactor and unit test.
  2. Poor performance: Any state change will trigger all Inputs to re-render.

In fact, these are exactly the reasons why React team suggests using useReducer for this kind of complex state.

But that doesn't mean we can't use useState to achieve the same result. It just requires a bit more crafting.

function Counter() {
  const { state, setFirstName, setLastName, setAge } = useComplexState({
    name: { first: "first", last: "last" },
    age: 40
  });
  return (
    <React.Fragment>
      <FirstNameInput value={state.name.first} setFirstName={setFirstName} />
      <LastNameInput value={state.name.last} setLastName={setLastName} />
      <AgeInput value={state.age} setAge={setAge} />
    </React.Fragment>
  )
}

// A custom hook that returns setter functions for each field.
// This is similar to what the reducer above is doing,
// we simply convert each action into its own setter function.
function useComplexState(initialState: UserState): any {
  const [state, setState] = React.useState<UserState>(initialState);
  const setFirstName = first =>
    setState(prevState => ({
      ...prevState,
      name: { ...prevState.name, first }
    }));
  const setLastName = last =>
    setState(prevState => ({
      ...prevState,
      name: { ...prevState.name, last }
    }));
  const setAge = age => setState(prevState => ({ ...prevState, age }));
  return { state, setFirstName, setLastName, setAge };
}

In fact, we can completely rewrite useReducer with only useState:

const useReducerImplementedByUseState = (reducer, initialState) => {
  const [state, setState] = React.useState<State>(initialState);
  const dispatch = (action: Action) => setState(prevState => reducer(prevState, action));
  return [state, dispatch];
};

// above implementation
const [state, dispatch] = useReducerImplementedByUseState(reducer, initialState);
// is same with
const [state, dispatch] = useReducer(reducer, initialState);

In conclusion,

  • For simple value state, do useState for it uses less lines.
  • For complex state, use whichever you feel like at the moment 🤪

Do you prefer useState or useReducer in your projects? Share your thoughts in the comment below ❤️

Top comments (0)