What advantages does useReducer actually have over useState?
Asked Answered
R

3

25

I'm struggling to understand when and why exactly useReducer has advantages when compared to useState. There are many arguments out there but to me, none of them makes sense and in this post, I'm trying to apply them to a simple example.

Maybe I am missing something but I don't see why useReducer should be used anywhere over useState. I hope you can help me to clarify this.

Let's take this example:

Version A - with useState

function CounterControls(props) {
  return (
    <>
      <button onClick={props.increment}>increment</button>
      <button onClick={props.decrement}>decrement</button>
    </>
  );
}

export default function App() {
  const [complexState, setComplexState] = useState({ nested: { deeply: 1 } });

  function increment() {
    setComplexState(state => {
      // do very complex logic here that depends on previous complexState
      state.nested.deeply += 1;
      return { ...state };
    });
  }

  function decrement() {
    setComplexState(state => {
      // do very complex logic here that depends on previous complexState
      state.nested.deeply -= 1;
      return { ...state };
    });
  }

  return (
    <div>
      <h1>{complexState.nested.deeply}</h1>
      <CounterControls increment={increment} decrement={decrement} />
    </div>
  );
}

See this stackblitz

Version B - with useReducer

import React from "react";
import { useReducer } from "react";

function CounterControls(props) {
  return (
    <>
      <button onClick={() => props.dispatch({ type: "increment" })}>
        increment
      </button>
      <button onClick={() => props.dispatch({ type: "decrement" })}>
        decrement
      </button>
    </>
  );
}

export default function App() {
  const [complexState, dispatch] = useReducer(reducer, {
    nested: { deeply: 1 }
  });

  function reducer(state, action) {
    switch (action.type) {
      case "increment":
        state.nested.deeply += 1;
        return { ...state };
      case "decrement":
        state.nested.deeply -= 1;
        return { ...state };
      default:
        throw new Error();
    }
  }

  return (
    <div>
      <h1>{complexState.nested.deeply}</h1>
      <CounterControls dispatch={dispatch} />
    </div>
  );
}

See this stackblitz

Comparing these two Variants, are there any potential advantages that Version B's approach (going with useReducer) would have over Version A's approach? At least for me, I don't see a reason to go with useReducer in a situation like this. But in which scenarios should I?

In a lot of articles (including the docs) two argumentations seem to be very popular:

"useReducer is good for complex state logic". In our example, let's assume complexState is actually complex and there are many modification actions with a lot of logic each. How does useReducer help here? For complex states wouldn't it be even better to have individual functions instead of having a single 200 lines reducer function?

"useReducer is good if the next state depends on the previous one". I can do the exact same thing with useState, can't I? Simply write setState(oldstate => {...})

Potential other advantages with useReducer:

  • "I don't have to pass down multiple functions but only one reducer": Ok, but I could also wrap my functions into one "actions" object with useCallback etc. And as already mentioned, having different logic in different functions seems like a good thing for me.
  • "I can provide the reducer with a context so my complex state can easily be modified throughout the app". Yes, but you could just as well provide individual functions from that context (maybe wrapped by useCallback)

Disadvantages I see with useReducer:

  • Multiple different actions in a single super-long function seems confusing
  • More prone to errors, since you have to examine the reducer function or rely on typescript etc. to find out what string you can pass on to the reducer and what arguments come with it. When calling a function this is much more straightforward.

With all that in mind: Can you give me a good example where useReducer really shines and that can't easily be rewritten to a version with useState?

Rheingold answered 26/2, 2021 at 13:43 Comment(0)
R
17

A couple of months later, I feel like I have to add some insights to this topic. If choosing between useReducer and useState was just a matter of personal preferences, why would people write stuff like this:

Dan Abramov on twitter:

useReducer is truly the cheat mode of Hooks. You might not appreciate it at first but it avoids a whole lot of potential issues that pop up both in classes and in components relying on useState. Get to know useReducer.

React docs

useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. useReducer also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks.

React docs:

We recommend to pass dispatch down in context rather than individual callbacks in props.

So let's try to nail it down and find a scenario, where useReducer clearly shines over useState:

What if the update-function needs to be called from an `useEffect` in a nested component?

VersionA's approach (useState & pass down callbacks) can have problems with this:

  • For semantical and linting reasons, the effect should have the update-function as dependency.
  • However this would mean that the effect gets called every time the update-function is re-declared. In the question's example "Version A" this would be at every render of App!
  • Calling useCallback on the function helps, but this pattern can quickly become tedious, especially if we need to additionally call useMemo on an actions object. (Also I'm no expert on this, but it doesn't sound very convincing from a performance perspective)
  • Additionally if the function has a dependency that changes often (like a user input to a text field), even useCallback wouldn't help much.

If we go with a reducer instead:

  • The reducer's dispatch function always has a stable identity! (See react docs)
  • This means, that we can safely work with it in effects, knowing that it won't change under normal circumstances! Even if the reducer-function changes, dispatch's identity stays the same and doesn't trigger the effect.
  • However we still get the up-to-date version of the reducer-function when we call it!

Again, see Dan Abramov's Twitter Post:

And the “dispatch” identity is always stable, even if the reducer is inline. So you can rely on it for perf optimizations and pass dispatch down the context for free as a static value.

Practical Example

In this code, I try to highlight some of the advantages of working with useReducer that I tried to describe previously:

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

function MyControls({ dispatch }) {
  // Cool, effect won't be called if reducer function changes.
  // dispatch is stable!
  // And still the up-to-date reducer will be used if we call it
  useEffect(() => {
    function onResize() {
      dispatch({ type: "set", text: "Resize" });
    }

    window.addEventListener("resize", onResize);
    return () => window.removeEventListener("resize", onResize);
  }, [dispatch]);

  return (
    <>
      <button onClick={() => dispatch({ type: "set", text: "ABC" })}>
        Set to "ABC"
      </button>
      <button onClick={() => dispatch({ type: "setToGlobalState" })}>
        Set to globalAppState
      </button>
      <div>Resize to set to "Resized"</div>
    </>
  );
}

function MyComponent(props) {
  const [headlineText, dispatch] = useReducer(reducer, "ABC");

  function reducer(state, action) {
    switch (action.type) {
      case "set":
        return action.text;
      case "setToGlobalState":
        // Cool, we can simply access props here. No dependencies
        // useCallbacks etc.
        return props.globalAppState;
      default:
        throw new Error();
    }
  }

  return (
    <div>
      <h1>{headlineText}</h1>
      <MyControls dispatch={dispatch} />
    </div>
  );
}

export default function App() {
  const [globalAppState, setGlobalAppState] = useState("");

  return (
    <div>
      global app state:{" "}
      <input
        value={globalAppState}
        onChange={(e) => setGlobalAppState(e.target.value)}
      />
      <MyComponent globalAppState={globalAppState} />
    </div>
  );
}

See this codesandbox

  • Even though the reducer function changes on every user-input, dispatch's identity stays the same! It doesn't trigger the effect
  • Still we get the up-to-date version of the function every time we call it! It has full access to the component's props.
  • No memoizing/useCallback etc. needed. In my opinion this alone makes the code much cleaner, especially because we should "rely on useMemo as a performance optimization, not as a semantic guarantee" (react docs)
Rheingold answered 14/12, 2021 at 9:46 Comment(4)
Great answer. Upvote this one if you read it.Mitchum
From the docs: React guarantees that setState function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.Emerson
Brilliant example, @Gifari ! I spent a couple of hours reading it to get a better understanding of the concepts behind userReducer. I also posted an edited version with some added styling and comments here: codesandbox.io/p/sandbox/…Antitoxin
@Antitoxin Can you relink? CheersStaves
X
4

I believe this may end up in an argument of opinions. however, this extraction from a simple article speaks for me so here it is with a link to the whole article at the bottom.

useReducer() is an alternative to useState() which gives you more control over the state management and can make testing easier. All the cases can be done with useState() method, so in conclusion, use the method that you are comfortable with, and it is easier to understand for you and colleagues.

Ref. Article: https://dev.to/spukas/3-reasons-to-usereducer-over-usestate-43ad#:~:text=useReducer()%20is%20an%20alternative,understand%20for%20you%20and%20colleagues.

Xerography answered 26/2, 2021 at 13:46 Comment(1)
I always preferred useReducer because you don't have to specify the full state on the update for the update to produce the full state. useState will only update with the state given unless given the full state as well. So it is prone to mistakes if you don't know how it works internally and you could easily introduce bugs.Subset
H
0

From my understanding, useReducer, if you implement it correctly, should have all related states managed in one place regarding all action types. By doing so, one can reduce the chances of introducing the bugs because, for every action type, one need update all affected states and carry over the rest of the unchanged states too. This method has advantage over the multiple useState, which need to maintain states separately and as a result some states' updates might be missed.

Hardily answered 11/1, 2023 at 23:51 Comment(1)
so by understanding this, actually it is just like object oriented concept. the method function in object oriented will take care of all data property accordingly. but due in react claims to be functionality programming so lacks of this object oriented concept, and thus creating this bulky boiler plate reducer?Mariquilla

© 2022 - 2024 — McMap. All rights reserved.