Я пытаюсь перейти от компонентов класса к функциональным компонентам с помощью новых хуков. Однако кажется, что с useCallback
я получу ненужные отрисовки детей в отличие от функций класса в компонентах класса.
Ниже у меня есть два относительно простых фрагмента. Первый - мой пример, написанный как классы, а второй - мой пример, переписанный как функциональные компоненты. Цель состоит в том, чтобы получить то же поведение с функциональными компонентами, что и с компонентами класса.
Тестовый набор компонентов класса
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>
Пример функционального компонента
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>
В первом (компоненты класса) я могу обновлять счет через красный блок без повторного рендеринга любого из блоков, и я могу свободно записывать текущий счет через оранжевый блок.
Во втором (функциональные компоненты) обновление счетчика с помощью красного блока вызовет повторную визуализацию как красного, так и голубого блока. Это связано с тем, что useCallback
создаст новый экземпляр своей функции, поскольку счетчик изменился, в результате чего блоки получили новую onClick
опору и, таким образом, произвели повторный рендеринг. Оранжевый блок не будет повторно отображаться, потому что useCallback
, используемый для оранжевого onClick
, не зависит от значения счетчика. Это было бы хорошо, но оранжевый блок не будет отображать фактическое значение счетчика при нажатии на него.
Я думал, что смысл в useCallback
заключался в том, чтобы дети не получали новые экземпляры той же функции и не имели ненужных повторных визуализаций, но это происходит в любом случае, как только функция обратного вызова использует один переменная, которая случается довольно часто, если не всегда из моего опыта.
Итак, как бы я мог сделать эту функцию onClick
внутри функционального компонента без повторного рендеринга детей? Это вообще возможно?
Обновление (решение):
Используя ответ Райана Когсвелла, приведенный ниже, я создал специальный хук, чтобы упростить создание функций, подобных классам.
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;
};
Затем я могу очень легко использовать это в компоненте функции и просто передать onClick дочернему элементу. Он больше не будет перерисовывать дочерний объект, но все равно будет использовать обновленные переменные.
const onClick = useMemoizedCallback(() => {
console.log("NEW I've been clicked when count was: ", count);
}, [count]);