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>