我正在尝试使用新的Hooks从类组件转变为功能组件。但是,感觉与useCallback类组件中的类函数不同,我将获得不必要的子代渲染。
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与计数值无关。这样做会很好,但是当您单击橙色块时,它不会显示计数的实际值。
onClick
我以为useCallback这样做的目的是让子代不会获得相同功能的新实例并且不必进行不必要的重新渲染,但这似乎在第二种情况下都会发生,即回调函数使用单个变量,如果并非总是根据我的经验。
那么,如何onClick在不重新渲染子级的情况下在功能组件中实现此功能呢?有可能吗?
更新(解决方案): 使用下面的Ryan Cogswell的答案,我精心制作了一个自定义钩子,以使创建类函数变得容易。
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传递给孩子即可。它将不再重新渲染子级,但仍使用更新的var。
const onClick = useMemoizedCallback(() => { console.log("NEW I've been clicked when count was: ", count); }, [count]);
useCallback会避免由于父级中发生的某些更改(而 不是 回调的依赖项)而导致不必要的子级重新渲染。为了避免在涉及回调的依赖项时重新渲染子项,您需要使用ref。引用是等效于实例变量的钩子。
在下面,我onClickMemoized使用onClickRefwhich指向当前onClick(通过设置useEffect),从而将其委托给知道状态当前值的函数版本。
onClickMemoized
onClickRef
useEffect
我还更改updateCount为使用功能性更新语法,因此它不需要依赖count。
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( () => { 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.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>
而且,钩子的优点当然在于您可以将这种有状态逻辑分解为自定义钩子:
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(() => { 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);