React useReducer vs React useState: When to use one over the other?

You might have just learned about React.useState, and now you’re hearing about React.useReducer and to use that instead of React.useState!

At least that happen to me. So a couple questions came into mind:

  • What is React.useReducer used for?
  • When do I use React.useReducer instead ofReact.useState?

Let’s go over each one of these questions, and see if I can clarify these for you.

What is React useReducer used for?

Well, anytime I hear the term reducer it reminds me of the JS Array.reducer() function.

And anything with the term reducer in React, also reminds me of Redux reducers.

React.useReducer in fact, is like a mini Redux reducers.


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

React.useReducer is a React hook function that accepts a reducer function, and a initial state.

The hook function returns and array with 2 values.

The first value is the state value, and the second value is the dispatch function to trigger an action.

You can read my previous article, “Understanding how reducers work in Redux“, to catch up on how reducers work, and how to trigger actions with dispatch().

The same concept applies to React.useReducer.

Now the question is

When do you use React.useReducer instead of React.useState?

This is quite simple actually.

Use React.useReducer when

  • The state value is an object or an array.
  • When the logic to update state is super complex
  • You need for a more predictable, and maintainable state architecture

A great example that is a good use for React.useReducer are forms.

Form data is typically bundled up in a key-value object. It then gets JSON.stringify() to get sent to an API endpoint.

On top of the form data, you have form status such as pending, success, error.

And those statuses may come with messages or response data.

Here’s a code example to demonstrate how React.useReducer helps alleviate some of this complex data.


const initialState = Object.freeze({
  data: {
    username: "",
    password: ""
  },
  isValid: false,
  status: "FROZEN",
  error: null,
  response: null
});

function enterData(state, action) {
  const { fieldName, value } = action.payload || {};

  if (state.data.hasOwnProperty(fieldName)) {
    // Update data property values
    const data = {
      ...state.data,
      [fieldName]: value
    };

    return {
      ...state,
      data,
      isValid: noEmptyFields(data)
    };
  }

  return state;
}

function noEmptyFields(data) {
  if ("object" !== typeof data) {
    return false;
  }

  const fields = Object.keys(data);

  for (let index = 0; index < fields.length; index++) {
    // Get field name identifier
    const fieldName = fields[index];

    // Get field raw value
    const fieldValue = data[fieldName];

    if (fieldValue.length < 1) {
      return false;
    }
  }

  return true;
}

function success(state, action) {
  return {
    ...state,
    status: "READY",
    error: null,
    response: action.payload
  };
}

function failure(state, action) {
  return {
    ...state,
    status: "ERROR",
    error: action.payload,
    response: null
  };
}

function reducer(state, action) {
  switch (action.type) {
    case "form/enter-data":
      return Object.assign({}, state, enterData(state, action));

    case "form/update-status":
      return Object.assign({}, state, { status: action.payload });

    case "form/success":
      return Object.assign({}, state, success(state, action));

    case "form/failure":
      return Object.assign({}, state, failure(state, action));

    case "form/reset":
      return initialState;
    default:
      return state;
  }
}

const FooBarForm = () => {
  const [state, dispatch] = React.useReducer(reducer, initialState);

  const handleChange = (e: any) =>
    dispatch({
      type: "form/enter-data",
      payload: {
        fieldName: e.target.name,
        value: e.target.value
      }
    });

  const handleSubmit = () => {
    dispatch({ type: "form/update-status", payload: "VERIFYING" });

    // Simulate  API fetch  call
    setTimeout(() => {
      // Comment out a dispatch statement to see a successful
      // or failure simulated calls!

      dispatch({
        type: "form/success",
        payload: {
          codeStatus: 200,
          redirectToDashboard: true
        }
      });

      // dispatch({
      //   type: 'form/failure',
      //   payload: {
      //     codeStatus: 401,
      //     message: 'Incorrect credentials. Please try again!',
      //   }
      // });
    }, 2000);
  };


  return (
    <>
      {state.status === "VERIFYING" && <h2>We're checking your creds!</h2>}
      {state.error && state.error.message && <h2>{state.error.message}</h2>}
      {state.response && state.response.redirectToDashboard && (
        <h2>You're going to get redirected in a second</h2>
      )}
      <label>
        Username
        <input name="username" onChange={handleChange} />
      </label>
      <br />
      <label>
        Password
        <input name="password" onChange={handleChange} />
      </label>
      <br />
      <button onClick={handleSubmit} disabled={!state.isValid}>
        Submit
      </button>
    </>
  );
};

Use React.useState when

Here’s an example where React.useState is just enough.


const UseStateExample = () => {
  const [showHiddenPassword, setShowHiddenPassword] = React.useState(false);

  const handleToggle = () => setShowHiddenPassword(!showHiddenPassword);

  return (
    <>
      <input type={showHiddenPassword ? "text" : "password"} />
      <button onClick={handleToggle}>Toggle hidden password</button>
    </>
  );
};

I like to tweet about React and post helpful code snippets. Follow me there if you would like some too!