0

Assume I have this code:

const [obj, setObj] = useState({ value: 0 });

// somewhere else

setState({value: 0});

// somewhere else

const value = useMemo(() => obj.value, [ obj ]);

// somewhere else

useEffect(() => { console.log(obj.value) }, [ obj ] );

Is it possible to have something like:

const [ obj, setObj ] = useStateWithSelector(obj, (prev, next) => prev.value === next.value);

So I can tell react to rerender only if the equalityFunction

(prev, next) => prev.value === next.value

return false ?

entropyfeverone
  • 1,052
  • 2
  • 8
  • 20

1 Answers1

2

react suggested idiom

The solution is not to use complex data types like Object or Array as dependencies for React effects. Use [obj.value] instead of [obj].

From the useEffect docs:

The array of dependencies is not passed as arguments to the effect function. Conceptually, though, that’s what they represent: every value referenced inside the effect function should also appear in the dependencies array

Run the code example below, enter a value and click Set several times. Notice the state is only changed when a fresh value is typed in the input.

function App() {
  const [input, setInput] = React.useState("")
  const [obj, setObj] = React.useState({ value: input })
  React.useEffect(_ => {
    console.log("state changed", obj.value)
  }, [obj.value]) // ✅ don't use objects as dependencies
  return <div>
    <input
      onChange={e => setInput(e.target.value)}
      value={input}
      placeholder="enter any value"
    />
    <button
      onClick={_ => setObj({ value: input })}
      children="Set"
    />
    <p>Repeated presses of <kbd>Set</kbd> will not triggger state change</p>
    <pre>{JSON.stringify(obj)}</pre>
  </div>
}

ReactDOM.render(<App/>, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>

custom hook solution

If you must use [obj] as a dependency, you could write a useStateWithGuard custom hook like you suggested.

function useStateWithGuard(initState, guard) {
  const [value, setValue] = React.useState(initState)
  return [
    value,
    next => setValue(prev => guard(prev, next) ? prev : next)
  ]
}

Run the code example below, enter a value and click Set several times. [obj] is used as a dependency but the state only changes when the new value passes the guard.

function App() {
  const [input, setInput] = React.useState("")
  const [obj, setObj] = useStateWithGuard(
    { value: "" },
    (prev, next) => prev.value === next.value
  )
  React.useEffect(_ => {
    console.log("state changed", obj.value)
  }, [obj]) // ⚠️ works now but maybe still a bad practice
  return <div>
    <input
      onChange={e => setInput(e.target.value)}
      value={input}
      placeholder="enter any value"
    />
    <button
      onClick={_ => setObj({ value: input })}
      children="Set"
    />
    <p>Repeated presses of <kbd>Set</kbd> will not triggger state change</p>
    <pre>{JSON.stringify(obj)}</pre>
  </div>
}

function useStateWithGuard(initState, guard) {
  const [value, setValue] = React.useState(initState)
  return [
    value,
    next => setValue(prev => guard(prev, next) ? prev : next)
  ]
}

ReactDOM.render(<App/>, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
よつば
  • 467
  • 1
  • 8
  • The guard function doesn’t work as intended because it should definately set the object to the next value property (maybe another property has changed) but only “trigger” react if the property value has changed because the value is the reason I want to make a chain of actions. – entropyfeverone Mar 24 '22 at 08:09
  • If another property changed, that means the state has changed and should trigger rerender. You write the guard when you call the hook so it can be whatever you want. The `useStateWithGuard` is an example and the code is easy enough that you can change it to meet your exact needs. – よつば Mar 24 '22 at 14:08