I'm trying to turn from class components to functional components using the new Hooks. However it feels that with useCallback
I will get unnecessary renders of children unlike with class functions in class components.
Below I have two relatively simple snippets. The first is my example written as classes, and the second is my example re-written as functional components. The goal is to get the same behaviour with functional components as with class components.
Class component test-case
class Block extends React.PureComponent {
render() {
console.log("Rendering block: ", this.props.color);
return (
<div onClick={this.props.onBlockClick}
style = {
{
width: '200px',
height: '100px',
marginTop: '12px',
backgroundColor: this.props.color,
textAlign: 'center'
}
}>
{this.props.text}
</div>
);
}
};
class Example extends React.Component {
state = {
count: 0
}
onClick = () => {
console.log("I've been clicked when count was: ", this.state.count);
}
updateCount = () => {
this.setState({ count: this.state.count + 1});
};
render() {
console.log("Rendering Example. Count: ", this.state.count);
return (
<div style={{ display: 'flex', 'flexDirection': 'row'}}>
<Block onBlockClick={this.onClick} text={'Click me to log the count!'} color={'orange'}/>
<Block onBlockClick={this.updateCount} text={'Click me to add to the count'} color={'red'}/>
</div>
);
}
};
ReactDOM.render(<Example/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id='root' style='width: 100%; height: 100%'>
</div>
Functional component test-case
const Block = React.memo((props) => {
console.log("Rendering block: ", props.color);
return (
<div onClick={props.onBlockClick}
style = {
{
width: '200px',
height: '100px',
marginTop: '12px',
backgroundColor: props.color,
textAlign: 'center'
}
}>
{props.text}
</div>
);
});
const Example = () => {
const [ count, setCount ] = React.useState(0);
console.log("Rendering Example. Count: ", count);
const onClickWithout = React.useCallback(() => {
console.log("I've been clicked when count was: ", count);
}, []);
const onClickWith = React.useCallback(() => {
console.log("I've been clicked when count was: ", count);
}, [ count ]);
const updateCount = React.useCallback(() => {
setCount(count + 1);
}, [ count ]);
return (
<div style={{ display: 'flex', 'flexDirection': 'row'}}>
<Block onBlockClick={onClickWithout} text={'Click me to log with empty array as input'} color={'orange'}/>
<Block onBlockClick={onClickWith} text={'Click me to log with count as input'} color={'cyan'}/>
<Block onBlockClick={updateCount} text={'Click me to add to the count'} color={'red'}/>
</div>
);
};
ReactDOM.render(<Example/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id='root' style='width: 100%; height: 100%'>
</div>
In the first one(class components) I can update the count via the red block without re-rendering either of the blocks, and I can freely console log the current count via the orange block.
In the second one(functional components) updating the count via the red-block will trigger a re-render of both the red and cyan block. This is because the useCallback
will make a new instance of it's function because the count has changed, causing the blocks to get a new onClick
prop and thus re-render. The orange block won't re-render because the useCallback
used for the orange onClick
does not depend on the count value. This would be good but the orange block will not show the actual value of the count when you click on it.
I thought the point of having useCallback
was so that children don't get new instances of the same function and don't have unnecessary re-renders, but that seems to happen anyways the second the callback function uses a single variable which happens quite often if not always from my experience.
So how would I go about making this onClick
function within a functional component without having the children re-render? Is it at all possible?
Update (solution): Using Ryan Cogswell's answer below I've crafted a custom hook to make creating class-like functions easily.
const useMemoizedCallback = (callback, inputs = []) => {
// Instance var to hold the actual callback.
const callbackRef = React.useRef(callback);
// The memoized callback that won't change and calls the changed callbackRef.
const memoizedCallback = React.useCallback((...args) => {
return callbackRef.current(...args);
}, []);
// The callback that is constantly updated according to the inputs.
const updatedCallback = React.useCallback(callback, inputs);
// The effect updates the callbackRef depending on the inputs.
React.useEffect(() => {
callbackRef.current = updatedCallback;
}, inputs);
// Return the memoized callback.
return memoizedCallback;
};
I can then use this in the function component very easily like so and simply pass the onClick to the child. It will no longer re-render the child but still make use of updated vars.
const onClick = useMemoizedCallback(() => {
console.log("NEW I've been clicked when count was: ", count);
}, [count]);