'`useState`, only update component when values in object change

Problem

useState always triggers an update even when the data's values haven't changed.

Here's a working demo of the problem: demo

Background

I'm using the useState hook to update an object and I'm trying to get it to only update when the values in that object change. Because React uses the Object.is comparison algorithm to determine when it should update; objects with equivalent values still cause the component to re-render because they're different objects.

Ex. This component will always re-render even though the value of the payload stays as { foo: 'bar' }

const UseStateWithNewObject = () => {
  const [payload, setPayload] = useState({});
  useEffect(
    () => {
      setInterval(() => {
        setPayload({ foo: 'bar' });
      }, 500);
    },
    [setPayload]
  );

  renderCountNewObject += 1;
  return <h3>A new object, even with the same values, will always cause a render: {renderCountNewObject}</h3>;
};

Question

Is there away that I can implement something like shouldComponentUpdate with hooks to tell react to only re-render my component when the data changes?



Solution 1:[1]

If I understand well, you are trying to only call setState whenever the new value for the state has changed, thus preventing unnecessary rerenders when it has NOT changed.

If that is the case you can take advantage of the callback form of useState

const [state, setState] = useState({});
setState(prevState => {
  // here check for equality and return prevState if the same

  // If the same
  return prevState; // -> NO RERENDER !

  // If different
  return {...prevState, ...updatedValues}; // Rerender
});

Here is a custom hook (in TypeScript) that does that for you automatically. It uses isEqual from lodash. But feel free to replace it with whatever equality function you see fit.

import { isEqual } from 'lodash';
import { useState } from 'react';

const useMemoizedState = <T>(initialValue: T): [T, (val: T) => void] => {
  const [state, _setState] = useState<T>(initialValue);

  const setState = (newState: T) => {
    _setState((prev) => {
      if (!isEqual(newState, prev)) {
        return newState;
      } else {
        return prev;
      }
    });
  };

  return [state, setState];
};

export default useMemoizedState;

Usage:

const [value, setValue] = useMemoizedState({ [...] });

Solution 2:[2]

Is there away that I can implement something like shouldComponentUpdate with hooks to tell react to only re-render my component when the data changes?

Commonly, for state change you compare with previous value before rendering with functional useState or a reference using useRef:

// functional useState
useEffect(() => {
  setInterval(() => {
    const curr = { foo: 'bar' };
    setPayload(prev => (isEqual(prev, curr) ? prev : curr));
  }, 500);
}, [setPayload]);
// with ref
const prev = useRef();
useEffect(() => {
  setInterval(() => {
    const curr = { foo: 'bar' };
    if (!isEqual(prev.current, curr)) {
      setPayload(curr);
    }
  }, 500);
}, [setPayload]);

useEffect(() => {
  prev.current = payload;
}, [payload]);

For completeness, "re-render my component when the data changes?" may be referred to props too, so in this case, you should use React.memo.

If your function component renders the same result given the same props, you can wrap it in a call to React.memo for a performance boost in some cases by memoizing the result. This means that React will skip rendering the component, and reuse the last rendered result.

Edit affectionate-hellman-ujtbv

Solution 3:[3]

The generic solution to this that does not involve adding logic to your effects, is to split your components into:

  • uncontrolled container with state that renders...
  • dumb controlled stateless component that has been memoized with React.memo

Your dumb component can be pure (as if it had shouldComponentUpdate implemented and your smart state handling component can be "dumb" and not worry about updating state to the same value.

Example:

Before

export default function Foo() {
  const [state, setState] = useState({ foo: "1" })
  const handler = useCallback(newValue => setState({ foo: newValue }))

  return (
    <div>
      <SomeWidget onEvent={handler} />
      Value: {{ state.foo }}
    </div>
  )

After

const FooChild = React.memo(({foo, handler}) => {
  return (
    <div>
      <SomeWidget onEvent={handler} />
      Value: {{ state.foo }}
    </div>
  )
})

export default function Foo() {
  const [state, setState] = useState({ foo: "1" })
  const handler = useCallback(newValue => setState({ foo: newValue }))

  return <FooChild handler={handler} foo={state.foo} />
}

This gives you the separation of logic you are looking for.

Solution 4:[4]

You can use memoized components, they will re-render only on prop changes.

const comparatorFunc = (prev, next) => {
  return prev.foo === next.foo
}

const MemoizedComponent = React.memo(({payload}) => {
    return (<div>{JSON.stringify(payload)}</div>)
}, comparatorFunc);

Solution 5:[5]

Relying on expanding the variable inside the Python statement is not a good idea since the statement need to be given as a string where shell variables can be expanded (i.e. use double quotes not single quotes) and that everything needs to be properly escaped so that the shell does not interpret things it is not supposed to. This can be very complex.

Much easier is to give the variables as an additional command line argument and then access the argument with sys.argv, like this:

for i in a b c d ; do 
    python3 -c 'import sys; print(sys.argv[1]);' $i
done

To get a single line value back catch the result of the python code into a shell variable:

for i in a b c d ; do 
    foo=`python3 -c 'import sys; print("in python: " + sys.argv[1]);' $i`
    echo "in shell: $foo"
done

The result from this:

in shell: in python: a
in shell: in python: b
in shell: in python: c
in shell: in python: d

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3 Brandon
Solution 4
Solution 5