useState vs useReducer
Asked Answered
S

4

43

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.

(quote from https://reactjs.org/docs/hooks-reference.html#usereducer)

I'm interested in the bold part, which states that useReducer should be used instead of useState when being used in contexts.

I tried both variants, but they don't appear to differ.

The way I compared both approaches was as follows:

const [state, updateState] = useState();
const [reducerState, dispatch] = useReducer(myReducerFunction);

I passed each of them once to a context object, which was being consumed in a deeper child (I just ran separate tests, replacing the value by the function that I wanted to test).

<ContextObject.Provider value={updateState // dispatch}>

The child contained these functions

const updateFunction = useContext(ContextObject);
useEffect(
  () => {
    console.log('effect triggered');
    console.log(updateFunction);
  },
  [updateFunction]
);

In both cases, when the parent rerendered (because of another local state change), the effect never ran, indicating that the update function isn't changed between renders. Am I reading the bold sentence in the quote wrong? Or is there something I'm overlooking?

Saguaro answered 12/2, 2019 at 9:19 Comment(1)
My guess would be that dispatch is an immutable function and callbacks created in nested functions create new references every time and so break equality checks in shouldComponentUpdateIyre
B
32

useReducer also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks.

The above statement is not trying to indicate that the setter returned by useState is being created newly on each update or render. What it means is that when you have a complex logic to update state you simply won't use the setter directly to update state, instead you will write a complex function which in turn would call the setter with updated state something like

const handleStateChange = () => {
   // lots of logic to derive updated state
   updateState(newState);
}

ContextObject.Provider value={{state, handleStateChange}}>

Now in the above case everytime the parent is re-rendered a new instance of handleStateChange is created causing the Context Consumer to also re-render.

A solution to the above case is to use useCallback and memoize the state updater method and use it. However for this you would need to take care of closure issues associated with using the values within the method.

Hence it is recommended to use useReducer which returns a dispatch method that doesn't change between re-renders and you can have the manipulation logic in the reducers.

Bunton answered 12/2, 2019 at 9:25 Comment(6)
So, if I'm understanding correctly, if I only use it to directly set a value, both functions are equal?Saguaro
@JDansercoer, yes, in that case both of the functions will be equal. In fact useState uses useReducer internallyBunton
Also, side note: in your example, it will always make the child re-render, since the value object is created every render, and objects are by reference thus not the same :)Saguaro
True, I just wanted to demonstrate.Bunton
So is it safe to say that useReducer is functionally equivalent to useCallback except the action includes a "payload" rather than just relying on component-scoped state? Other reasons for using useReducer seem more like choices over coding convention.Understate
@ecoe, no useReducer is not functionally equivalent to useCallback. useCallback is for memoization of functions whereas useReducer is used to store and update states. The dispatch returned by useReducer doesn't change on each render and hence can pass it to child components applying memoizationBunton
K
5

Practical observation on useReducer and useState -

UseState:

In my React Native project I've 1 screen containing 25+ different states created using useState.

I'm calling an api in useEffect (componentDidMount) and on getting the response based on some conditions, I'm setting up these 25 states, calling 25 state setter function for each function.

I've put a re-rendering counter and checked my screen is re-rendered 14 times.

re-rendering count likewise :

let count = 0;

export default function Home(props) {
  count++;
  console.log({count});
  //...
  // Rest of the code
 }

UseReducer :

Then I've moved these 25 states in useReducer states, And used only single action to update these states on API response.

I've observed there is only 2 re-render.

//API calling method:
    fetchData()
    {
     const response = await AuthAxios.getHomeData(); 
     dispatch({type: 'SET_HOME_DATA', data: response.data});
    
    }

//useReducer Code:
const initialStaes = {
  state1: null,
  state2: null,
   //.....More States 
  state27: null,
  state28: null
}

const HomeReducer = (state, action) => {
  switch (action.type) {
    case 'SET_HOME_DATA': {

      return {
        ...state,
        state1: (Data based on conditions),
        state2: !(some Conditions ),
        //....More states
        state27: false
       }
    }
  }
}

Advantage of useReducer in this case :

  1. Using useReducer I've reduced number of re-renders on the screen, hence better performance and smoothness of the App.
  2. Number of lines is reduced in my screen itself. It improved code readablity.
Karikaria answered 3/10, 2022 at 6:53 Comment(2)
Good Explanation. It was on point with a example.Puton
but for only avoiding re-render is still approachable by consolidate all state into 1 object and just 1 useState for the object. which is still far simpler than useReducer . hence useState is still preferable for this case..Chalk
A
1

When you need to care about it

If you create a callback on render and pass it to a child component, the props of that child will change. However, when the parent renders, a regular component will rerender (to the virtual dom), even props remain the same. The exception is a classComponent that implements shouldComponentUpdate, and compares props (such as a PureComponent).

This is an optimization, and you should only care about it if rerendering the child component requires significant computation (If you render it to the same screen multiple times, or if it will require a deep or significant rerender).

If this is the case, you should make sure:

  1. Your child is a class component that extends PureComponent
  2. Avoid passing a newly created function as a prop. Instead, pass dispatch, the setter returned from React.useState or a memoized customized setter.

Using a memoized customized setter

While I would not recommend building a unique memoized setter for a specific component (there are a few things you need to look out for), you could use a general hook that takes care of implementation for you.

Here is an example of a useObjState hook, which provides an easy API, and which will not cause additional rerenders.


const useObjState = initialObj => {
  const [obj, setObj] = React.useState(initialObj);
  const memoizedSetObj = React.useMemo(() => {
    const helper = {};
    Object.keys(initialObj).forEach(key => {
      helper[key] = newVal =>
        setObj(prevObj => ({ ...prevObj, [key]: newVal }));
    });
    return helper;
  }, []);
  return [obj, memoizedSetObj];
};

function App() {
  const [user, memoizedSetUser] = useObjState({
    id: 1,
    name: "ed",
    age: null,
  });

  return (
      <NameComp
        setter={memoizedSetUser.name}
        name={user.name}
      />
  );
}

const NameComp = ({name, setter}) => (
  <div>
    <h1>{name}</h1>
      <input
        value={name}
        onChange={e => setter(e.target.value)}
      />
  </div>
)

Demo

Awhirl answered 22/6, 2019 at 9:29 Comment(3)
This is not true. Setters for useState and useReducer are guaranteed to be equal between renders (as stated in the React docs), so there is no need to memoize it.Saguaro
@JDansercoer, setters returned from useState are equal throught renders. I'm not memoizing that setter, but the setter returned from useObjectState.Awhirl
@JDansercoer, StackOverFlow and the community aspires to be a polite and welcoming environment. At the very least it will provide a better quality of answers, and comments. What don't you understand? What don't you like about the current answer? Please let me know and I'll adjust and clarify.Awhirl
S
0

useReducer and useState are both React hooks used for managing state in a component. The key difference between the two hooks is in how they manage state updates.

  • useState is a simpler hook that allows you to manage a single state value in a component. It works by providing two values: the current state value and a function to update the state value. When you call the state update function, React will re-render the component with the new state value.

  • useReducer, on the other hand, is a more powerful hook that allows you to manage complex state updates in a more structured way. It works by dispatching actions to update state based on the current state and the action payload. The useReducer hook takes two arguments: a reducer function and an initial state value. The reducer function takes the current state and an action object as arguments, and returns the new state value.

In general, you should use useState for simple state management needs, such as managing a boolean flag or a single input value. Use useReducer for more complex state management needs, such as managing state that has multiple values or managing state based on different types of actions.

One important thing to note is that useReducer can help prevent unnecessary re-renders of a component by allowing you to specify exactly which parts of the state have changed. This can be particularly useful in large applications where performance is critical.

Schoening answered 11/4, 2023 at 15:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.