React Hooks useCallback 导致 child 到 re-render
React Hooks useCallback causes child to re-render
我正在尝试使用新的 Hooks 从 class 组件转换为功能组件。然而,感觉使用 useCallback
我会得到不必要的 children 渲染,这与 class 组件中的 class 函数不同。
下面我有两个相对简单的片段。第一个是我写成 classes 的例子,第二个是我写成 re-written 作为功能组件的例子。目标是使用功能组件获得与 class 组件相同的行为。
Class分量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>
功能组件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>
在第一个(class 组件)中,我可以通过红色块更新计数而无需 re-rendering 任何一个块,并且我可以自由地通过橙色块控制台记录当前计数.
在第二个(功能组件)中,通过 red-block 更新计数将触发红色和青色块的 re-render。这是因为 useCallback
将创建其功能的新实例,因为计数已更改,导致块获得新的 onClick
道具,因此 re-render。橙色块不会 re-render 因为用于橙色 onClick
的 useCallback
不依赖于计数值。这很好,但是当您单击橙色块时,它不会显示计数的实际值。
我认为 useCallback
的意义在于 children 不会获得相同功能的新实例并且没有不必要的 re-renders,但是无论如何似乎都会发生第二次回调函数使用单个变量,如果不总是根据我的经验,这种情况经常发生。
那么,在没有 children re-render 的情况下,我将如何在功能组件中实现此 onClick
功能?有可能吗?
更新(解决方案):
使用下面 Ryan Cogswell 的回答,我制作了一个自定义挂钩,可以轻松创建 class-like 函数。
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 传递给 child。它将不再 re-render child 但仍然使用更新的变量。
const onClick = useMemoizedCallback(() => {
console.log("NEW I've been clicked when count was: ", count);
}, [count]);
useCallback
将避免不必要的子项重新渲染,因为父项中的某些更改 不是 回调依赖项的一部分。为了避免在涉及回调的依赖项时重新呈现子项,您需要使用 ref。 Ref 是相当于实例变量的钩子。
下面我有 onClickMemoized
使用 onClickRef
指向当前 onClick
(通过 useEffect
设置)以便它委托给函数的一个版本知道状态的当前值。
我还更改了 updateCount
以使用功能更新语法,这样它就不需要依赖于 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>
当然,挂钩的美妙之处在于您可以将这种有状态逻辑分解到自定义挂钩中:
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);
这对当前代码的改动很小。
- 删除了 useCallback 中的 deps 参数
- 状态改变时更新 ref 值
- 不要使用来自状态的值,而是使用来自
useCallback
块内的 ref 的值,因为 deps 已被删除并且状态值将不会更新。
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>
我正在尝试使用新的 Hooks 从 class 组件转换为功能组件。然而,感觉使用 useCallback
我会得到不必要的 children 渲染,这与 class 组件中的 class 函数不同。
下面我有两个相对简单的片段。第一个是我写成 classes 的例子,第二个是我写成 re-written 作为功能组件的例子。目标是使用功能组件获得与 class 组件相同的行为。
Class分量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>
功能组件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>
在第一个(class 组件)中,我可以通过红色块更新计数而无需 re-rendering 任何一个块,并且我可以自由地通过橙色块控制台记录当前计数.
在第二个(功能组件)中,通过 red-block 更新计数将触发红色和青色块的 re-render。这是因为 useCallback
将创建其功能的新实例,因为计数已更改,导致块获得新的 onClick
道具,因此 re-render。橙色块不会 re-render 因为用于橙色 onClick
的 useCallback
不依赖于计数值。这很好,但是当您单击橙色块时,它不会显示计数的实际值。
我认为 useCallback
的意义在于 children 不会获得相同功能的新实例并且没有不必要的 re-renders,但是无论如何似乎都会发生第二次回调函数使用单个变量,如果不总是根据我的经验,这种情况经常发生。
那么,在没有 children re-render 的情况下,我将如何在功能组件中实现此 onClick
功能?有可能吗?
更新(解决方案): 使用下面 Ryan Cogswell 的回答,我制作了一个自定义挂钩,可以轻松创建 class-like 函数。
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 传递给 child。它将不再 re-render child 但仍然使用更新的变量。
const onClick = useMemoizedCallback(() => {
console.log("NEW I've been clicked when count was: ", count);
}, [count]);
useCallback
将避免不必要的子项重新渲染,因为父项中的某些更改 不是 回调依赖项的一部分。为了避免在涉及回调的依赖项时重新呈现子项,您需要使用 ref。 Ref 是相当于实例变量的钩子。
下面我有 onClickMemoized
使用 onClickRef
指向当前 onClick
(通过 useEffect
设置)以便它委托给函数的一个版本知道状态的当前值。
我还更改了 updateCount
以使用功能更新语法,这样它就不需要依赖于 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>
当然,挂钩的美妙之处在于您可以将这种有状态逻辑分解到自定义挂钩中:
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);
这对当前代码的改动很小。
- 删除了 useCallback 中的 deps 参数
- 状态改变时更新 ref 值
- 不要使用来自状态的值,而是使用来自
useCallback
块内的 ref 的值,因为 deps 已被删除并且状态值将不会更新。
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>