I know you can tell React to skip an effect by passing an array as an optional second argument.
For example:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
But what if i want to control over the comparison? to add my own comparison logic.
I would expect something like React.memo
that you can pass a function as a second argument.
In a comment above Gabriele Petrioli links to the React.memo documentation that explains how to implement shouldComponentUpdate. I was googling combinations of shouldComponentUpdate + useEffect + "react hooks", and this came up as the result. So after solving my problem with the linked documentation I thought I would bring the information here as well.
This is the old way of implementing shouldComponentUpdate:
class MyComponent extends React.Component{
shouldComponentUpdate(nextProps){
return nextProps.value !== this.props.value;
}
render(){
return (
<div>{"My Component " + this.props.value}</div>
);
}
}
The New React Hooks way:
React.memo(function MyComponent (props) {
return <div>{ "My Component " + props.value }</div>;
})
I know you were probably asking for more in your question, but for anyone coming from Google looking for how to implement shouldComponentUpdate using React Hooks, there you go.
The documentation is here:
how-do-i-implement-shouldcomponentupdate
Here is a custom hook which takes an updater function, and returns a value that changes only when the updater function returns true, which can be passed in the second argument to useEffect
or useCallback
or useMemo
to force a re-render:
function useShouldRecalculate(shouldRecalculateFn) {
const prevValueRef = useRef(0);
if(shouldRecalculateFn()) {
// If we need to recalculate, change the value we return
prevValueRef.current += 1;
} // else we return the same value as the previous render, which won't trigger a recalculation
return prevValueRef.current;
}
For example, this will update the document title only when count is even:
const shouldUpdateTitle = useShouldRecalculate(
() => count % 2 === 0
);
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [shouldUpdateTitle]); // eslint-disable-line react-hooks/exhaustive-deps
Why you probably shouldn't do this
In most cases, I wouldn't recommend doing this.
Generally, there's going to be cleaner ways to accomplish the same task, using idiomatic hooks API. (The above example could have just put an if
block around the line that updates the document's title.)
Perhaps more importantly, the deps
argument isn't just about optimization, but about keeping closure values up-to-date, and avoiding bugs like:
const [count, setCount] = useState(0)
const increment = useCallback(() => {
// Bug: `count` is always 0 here, due to incorrect use of the `deps` argument
setCount(count + 1)
}, [])
This bug will be caught by the react-hooks/exhaustive-deps
linter rule, but you'll have to disable that rule anywhere where you use custom logic to control execution.
Use custom memoization logic is likely to make your components more bug-prone and harder to reason about in general. So I'd consider this useShouldRecalculate
hook something of a last resort.
An alternative might be to use useRef to hold your data, and use useState ONLY to store the data you want to display.
Sometimes this works better than the memo approach: I've had a case recently where React was still rendering needlessly when using React.memo, and it was messing up some PIXI display.
The approach below fixed it for me... hopefully I did not do an anti-pattern :-)
const countRef = useRef(0);
const [countDisplay, setCountDisplay] = useState(0);
yourUpdateFunction = () => {
// This is where count gets updated
countRef.current = countRef.current + 1;
if ((countRef.current % 2) === 0) setCountDisplay(countRef.current);
}
return (<p>countDisplay</p>);
Adding to PAT-O-MATION's answer,
React.memo also accepts a second parameter, which is function which can be used to determine if a component
should render or not
if the function returns true then component won't re-render on change of that prop, conversely it updates when the return value is false
function SomeComp({prop1, prop2) {
return(
..
)
}
React.memo(SomeComp, (props, nextProps)=> {
if(props.prop1 === nextProps.prop1) {
// don't re-render/update
return true
}
})
Note: Component would only re-render when the callback function returns false, so in the above case even if the prop2 value changes it won't re-render
Passing the optional second array argument kicks in the effect function if the property passed in the array changes - just like shouldComponentUpdate
method in a class component would run when props passed to the component change. You can decide in the effect function based upon the value of the parameter - if you want to the effect to apply or not.