Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What advantages does useReducer actually have over useState?

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

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 say complexState is complex have 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:

  • "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:

  • 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?

like image 443
Giraphi Avatar asked Feb 26 '21 13:02

Giraphi


People also ask

What is the difference between usestate and usereducer?

Get to know useReducer. 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.

What is the difference between usereducer and reducer?

useReducer returns a state and a dispatch function, while receiving a reducer and an initial state. What is a reducer? A reducer is a pure function which has two parameters, a state and an action (now we'll understand the dispatch from useReducer).

Why use usereducer over usestate in react?

Let’s review a few common reasons why people choose useReducer over useState: So business logic can be centralized in the reducer as opposed to scattered about the component Reducers are pure functions that are easy to test in isolation of React

Is usereducer good for complex state logic?

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 say complexState is complex have many modification actions with a lot of logic each. How does useReducer help here?


2 Answers

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), 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)
like image 155
Giraphi Avatar answered Dec 06 '22 12:12

Giraphi


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.

like image 39
mw509 Avatar answered Dec 06 '22 12:12

mw509