Choosing between useReducer and useState in React

React community is vast and growing rapidly. For the same business case, there are different ways to solve a problem.

And, whenever there are multiple solutions to do the same thing, the question arises - “When to use one over the other?”

The answer most of the time is - It depends on the use case.

One such most-asked question while handling states is -

When to use useReducer over useState hook?

Let’s understand these hooks in detail and discuss which one should be preferred and when.

The useState hook

The useState is a hook that allows us to have state variables in functional components. It takes the initial state as an argument and returns an array of two entries

  • State value
  • A function to update the state

The useReducer hook

The useReducer(reducer, initialState) hook accept 3 arguments:

  • Reducer function
  • Initial state
  • Initializer function

The hook then returns an array of 2 items:

  • Current state
  • Dispatch function
  const [state, dispatch] = useReducer(reducer, initialArg, init?) 
 

Use-case of a state with JavaScript primitives

Using the useState hook

Let’s check out the example of setting up a locale using useState.

function App() {
  const { t,  i18n} = useTranslation();
  const defaultLocale = localStorage['locale'] ? localStorage['locale'] : 'en';
  const [currentLocale, setCurrentLocale] = useState(defaultLocale);

  const localeList = [
    { name: 'English', code: 'en', lang: 'English' },
    { name: '中文', code: 'zh', lang: 'Chinese' },
    { name: 'русский', code: 'ru', lang: 'Russian' },
    { name: 'Française', code: 'fr', lang: 'French' }
  ];
  const onChangeLanguage = (e) => {
    const selectedLocale = e.target.value;
    setCurrentLocale(selectedLocale);
    localStorage.setItem('locale', selectedLocale)
    i18n.changeLanguage(selectedLocale);
  }
  return (
    <div className="App">
      <select onChange={onChangeLanguage} defaultValue={currentLocale}>
        {
          localeList.map((locale, index) => (
            <option key={index} value={locale.code}>{locale.name}</option>
          ))
        }
      </select>
      <div className="translation">
      {t('hello_world')}
      </div>
    </div>
  );
}
 

Pretty simple, right?

Using the useReduce hook

Now, let’s look into the useReducer hook.

function localeReducer(state, action) {
    switch (action.type) {
      case 'CHANGE_LOCALE': {
        return {...state, locale: action.locale}
      }
      default: {
        throw new Error(`Unhandled action type: ${action.type}`)
      }
    }
  }
  function App() {
  // Other code same as the previous one  
  const onChangeLanguage = (e) => {
    const selectedLocale = e.target.value;
    dispatch({
      type: 'CHANGE_LOCALE',
      locale: selectedLocale
    })
    localStorage.setItem('locale', selectedLocale)
    i18n.changeLanguage(selectedLocale);
  }
  } 
 

This code looks a bit more complex than the useState code, isn’t it?

This example involves a primitive value ‘locale’(string) which is not a complex object or an array. So, this use case is more suitable for useState.

Use-case of a state with an object

Now, let’s consider an example of a form taken from the React docs.

We have a user object with two fields: name and age.

Using the useState hook

Firstly let’s write the example using useState.

export default function Form() {
  const [user, setUser] = useState({ name: 'Chetan', age: 30 })

  function handleButtonClick() {
    setUser(previousState => ({
      ...previousState, // Copy the old fields
      ...{ age: previousState.age + 1 }
    }));
  }
  function handleInputChange(e) {
    setUser(previousState => ({
      ...previousState,
      ...{ name: e.target.value }
    }));
  }
  return (
    <div>
      <input
        value={user.name}
        onChange={handleInputChange}
      />
      <button onClick={handleButtonClick}>
        Increment age
      </button>
      <p>Hello, {user.name}. You are {user.age}.</p>
    </div>
  );
}

If the logic of our code gets complicated then the setter function can be large and it will be difficult to handle it. Also, we have to take care of returning new items in every setter function using the spreading operator. This becomes very difficult to manage.

To simplify our lives, we have a useReducer hook to our rescue!

Using the useReducer hook

Now, let’s write the same example using useReducer.

//Calculates and returns the next state
function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      return {
        name: state.name,
        age: state.age + 1
      };
    }
    case 'changed_name': {
      return {
        name: action.nextName,
        age: state.age
      };
    }
  }
  throw Error('Unknown action: ' + action.type);
}

export default function Form() {
  const [user, dispatch] = useReducer(reducer, { name: 'Chetan', age: 30 });

  function handleButtonClick() {
    dispatch({ type: 'incremented_age' });
  }

  function handleInputChange(e) {
    dispatch({
      type: 'changed_name',
      nextName: e.target.value
    }); 
  }

  return (
    <div>
      <input
        value={user.name}
        onChange={handleInputChange}
      />
      <button onClick={handleButtonClick}>
        Increment age
      </button>
      <p>Hello, {user.name}. You are {user.age}.</p>
    </div>
  );
}

Looks simple and clean.

Isn’t it?

useReducer is very similar to useState, but it lets us move the state update logic from event handlers into a single function outside of our component.

The point to note is when we have a type of state as objects or arrays, it is convenient to use useReducer.

Thumb rules to decide when to use useState or useReducer

Prefer useState if we have:

  1. JavaScript primitives(string, boolean, number) as a state( eg. our first use case )
  2. Simple business logic
  3. Different properties that don’t change in any correlated way and can be managed by multiple useState hooks

Prefer useReducer if we have:

  1. JavaScript objects or arrays as a state ( eg. our second use case )
  2. Complicated business logic more suitable for a reducer function
  3. Different properties tied together that should be managed in one state object

Final thoughts

We should consider incrementally adopting useReducer as our state and validation requirements begin to get more complex, warranting the additional effort.

While adopting useReducer for complex objects frequently, if there are many pitfalls around mutation, introducing  Immer  could be worthwhile.

React docs recommend using a reducer if we often encounter bugs due to incorrect state updates in some components, and want to introduce more structure to its code.

Using useState or useReducer is a matter of preference. We can convert between useState and useReducer back and forth: they are equivalent!

We don’t have to use reducers for everything. We can mix and match!

If the project has gotten to the point of state management complexity, we may want to look at some even more scalable solutions, such as Mobx, Zustand, or XState to meet our needs.

But let’s not forget,

Start Simple

and

Add complexity only as needed.

Need help on your Ruby on Rails or React project?

Join Our Newsletter