I am using react useEffect
hooks and checking if an object has changed and only then run the hook again.
My code looks like this.
const useExample = (apiOptions) => { const [data, updateData] = useState([]); useEffect(() => { const [data, updateData] = useState<any>([]); doSomethingCool(apiOptions).then(res => { updateData(response.data); }) }, [apiOptions]); return { data }; };
Unfortunately it keeps running as the objects are not being recognised as being the same.
I believe the following is an example of why.
const objA = { method: 'GET' } const objB = { method: 'GET' } console.log(objA === objB)
Perhaps running JSON.stringify(apiOptions)
works?
In other words, useEffect runs even though the object value(e.g. user.name ) is still the same. useEffect uses shallow equality comparison to identify whether dependencies are updated.
The objects have to be the exact same object in order for useEffect to skip running the effect. So even if the contents are the exact same, if a new object is created for the subsequent render, useEffect will re-run the effect.
useDeepCompareEffect is a drop-in replacement for useEffect . It will not do a reference equality check but a deep comparison before trying to run the effect. Installation per NPM/Yarn npm install use-deep-compare-effect.
You can have multiple useEffects in your code and this is completely fine! As hooks docs say, you should separate concerns. Multiple hooks rule also applies to useState - you can have multiple useState in one component to separate different part of the state, you don't have to build one complicated state object.
apiOptions
as state valueI'm not sure how you are consuming the custom hook but making apiOptions
a state value by using useState
should work just fine. This way you can serve it to your custom hook as a state value like so:
const [apiOptions, setApiOptions] = useState({ a: 1 }) const { data } = useExample(apiOptions)
This way it's going to change only when you use setApiOptions
.
Example #1
import { useState, useEffect } from 'react'; const useExample = (apiOptions) => { const [data, updateData] = useState([]); useEffect(() => { console.log('effect triggered') }, [apiOptions]); return { data }; } export default function App() { const [apiOptions, setApiOptions] = useState({ a: 1 }) const { data } = useExample(apiOptions); const [somethingElse, setSomethingElse] = useState('default state') return <div> <button onClick={() => { setApiOptions({ a: 1 }) }}>change apiOptions</button> <button onClick={() => { setSomethingElse('state') }}> change something else to force rerender </button> </div>; }
You could write a deep comparable useEffect
as described here:
function deepCompareEquals(a, b){ // TODO: implement deep comparison here // something like lodash // return _.isEqual(a, b); } function useDeepCompareMemoize(value) { const ref = useRef() // it can be done by using useMemo as well // but useRef is rather cleaner and easier if (!deepCompareEquals(value, ref.current)) { ref.current = value } return ref.current } function useDeepCompareEffect(callback, dependencies) { useEffect( callback, dependencies.map(useDeepCompareMemoize) ) }
You can use it like you'd use useEffect
.
I just found a solution which works for me.
You have to use usePrevious()
and _.isEqual()
from Lodash. Inside the useEffect()
, you put a condition if the previous apiOptions
equals to the current apiOptions
. If true, do nothing. If false updateData().
Example :
const useExample = (apiOptions) => { const myPreviousState = usePrevious(apiOptions); const [data, updateData] = useState([]); useEffect(() => { if (myPreviousState && !_.isEqual(myPreviousState, apiOptions)) { updateData(apiOptions); } }, [apiOptions]) }
usePrevious(value)
is a custom hook which create a ref
with useRef()
.
You can found it from the Official React Hook documentation.
const usePrevious = value => { const ref = useRef(); useEffect(() => { ref.current = value; }); return ref.current; };
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With