\n","author":{"@type":"Person","name":"Ryan Cogswell"},"upvoteCount":32}}}
Reputation: 4439
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]);
Upvotes: 27
Views: 21347
Reputation: 56
This works too with minimal change in the current code.
useCallback
block since the deps is removed and the state value will not be update.const {useState} = React
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 ] = useState(0);
const countRef = React.useRef(count);
console.log("Rendering Example. Count: ", count);
const onClickWithout = React.useCallback(() => {
console.log("I've been clicked when count was: ", count, countRef.current);
}, []);
const onClickWith = React.useCallback(() => {
console.log("I've been clicked when count was: ", count, countRef.current);
}, [ ]);
const updateCount = React.useCallback(() => {
setCount(count => {
countRef.current = count+1
return count + 1
});
}, [ ]);
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("react"));
//ReactDOM.render( < Example / > , document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Upvotes: 1
Reputation: 81136
useCallback
will avoid unnecessary child re-renders due to something changing in the parent that is not part of the dependencies for the callback. In order to avoid child re-renders when the callback's dependencies are involved, you need to use a ref. Ref's are the hook equivalent to an instance variable.
Below I have onClickMemoized
using the onClickRef
which points at the current onClick
(set via useEffect
) so that it delegates to a version of the function that knows the current value of the state.
I also changed updateCount
to use the functional update syntax so that it doesn't need to have a dependency on count
.
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 onClick = () => {
console.log("I've been clicked when count was: ", count);
};
const onClickRef = React.useRef(onClick);
React.useEffect(
() => {
// By leaving off the dependency array parameter, it means that
// this effect will execute after every committed render, so
// onClickRef.current will stay up-to-date.
onClickRef.current = onClick;
}
);
const onClickMemoized = React.useCallback(() => {
onClickRef.current();
}, []);
const updateCount = React.useCallback(() => {
setCount(prevCount => prevCount + 1);
}, []);
return (
<div style={{ display: "flex", flexDirection: "row" }}>
<Block
onBlockClick={onClickMemoized}
text={"Click me to log with empty array as input"}
color={"orange"}
/>
<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.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id='root' style='width: 100%; height: 100%'>
</div>
And, of course, the beauty of hooks is that you can factor out this stateful logic into a custom hook:
import React from "react";
import ReactDOM from "react-dom";
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 useCount = () => {
const [count, setCount] = React.useState(0);
const logCount = () => {
console.log("I've been clicked when count was: ", count);
};
const logCountRef = React.useRef(logCount);
React.useEffect(() => {
// By leaving off the dependency array parameter, it means that
// this effect will execute after every committed render, so
// logCountRef.current will stay up-to-date.
logCountRef.current = logCount;
});
const logCountMemoized = React.useCallback(() => {
logCountRef.current();
}, []);
const updateCount = React.useCallback(() => {
setCount(prevCount => prevCount + 1);
}, []);
return { count, logCount: logCountMemoized, updateCount };
};
const Example = () => {
const { count, logCount, updateCount } = useCount();
console.log("Rendering Example. Count: ", count);
return (
<div style={{ display: "flex", flexDirection: "row" }}>
<Block
onBlockClick={logCount}
text={"Click me to log with empty array as input"}
color={"orange"}
/>
<Block
onBlockClick={updateCount}
text={"Click me to add to the count"}
color={"red"}
/>
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<Example />, rootElement);
Upvotes: 32