useState vs useReducer

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.


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