react useEffect comparing objects
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 the input is shallow enough that you think deep equality would still be fast, consider using JSON.stringify
:
const useExample = (apiOptions) => {
const [data, updateData] = useState([]);
const apiOptionsJsonString = JSON.stringify(apiOptions);
useEffect(() => {
const apiOptionsObject = JSON.parse(apiOptionsJsonString);
doSomethingCool(apiOptionsObject).then(response => {
updateData(response.data);
})
}, [apiOptionsJsonString]);
return {
data
};
};
Note it won’t compare functions.
Use apiOptions
as state value
I'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>;
}
Alternatively
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
.