常见的 React hooks 闭包陷阱,如何避免?
React hooks 闭包陷阱是什么,常见场景?
React 提供了一些性能优化函数: React.memo 、useMemo、useCallback。
- useMemo:缓存值,只有依赖项变更的时候才会重新计算
const cachedValue = useMemo(fn=>calculateValue, dependencies);
- useCallback:缓存函数,只有依赖项变更的时候才会重新更新
const cachedFn = useCallback(fn, dependencies);
- React.memo:缓存组件,当 props 不变时,不会执行 render。arePropsEqual 是可选函数,自定义新旧 props 的对比逻辑, 返回 true 缓存,返回 false 不缓存。
const MemoizedComponent = memo(SomeComponent, arePropsEqual?); const arePropsEqual=(oldProps: Props, newProps: Props) => boolean
使用上面这些函数来优化性能时,这些函数与外部的 state 形成闭包,导致函数中获取到的 state 值不是最新的,这就是闭包陷阱。
React 闭包陷阱代码演示
如下代码中,包含一个 App (计时器)组件和一个 Child 组件, 当点击 Child 组件时需要返回 App 组件中最新的 state 值;
import React, { useEffect } from "react";
import "antd/dist/antd.css";
import { Button, ButtonProps } from "antd";
const Child = ({ onClick }: ButtonProps) => {
console.log("render child");
return (
<Button onClick={onClick} type="primary">
Button
</Button>
);
};
const App: React.FC = () => {
const [count, setCount] = React.useState(0);
useEffect(() => {
const time = setInterval(() => {
setCount((count) => count + 1);
}, 1000);
return () => {
clearInterval(time);
};
}, []);
const handleClick = () => {
console.log("count:", count);
};
return (
<>
<h2>{count}</h2>
<Child onClick={handleClick} />
</>
);
};
export default App;
这样没什么问题,但是每次渲染的时候 Child 组件都会执行 render
为了防止 App 组件在更新的时候重复渲染 Child 子组件,我们可以使用 React.memo 包裹下 Child 子组件, handleClick 也使用 useCallback 包裹,这样就能保证 Child 子组件只 render 一次。
const Child = React.memo(({ onClick }: ButtonProps) => {
console.log("render child");
return (
<Button onClick={onClick} type="primary">
Button
</Button>
);
});
const handleClick = useCallback(() => {
console.log("count:", count);
}, []);
这样一来 useCallback 和 state 就形成了一个闭包,每次打印的 state 就是初始化的 state。
为了获得最新的 state 值,必须将 count 参数写进 useCallback 的第二个参数。
const handleClick = useCallback(() => {
console.log("count:", count);
}, [ count ]);
但这样,又会导致 Child 组件更新。那么有什么好的解决办法呢?既能防止子组件的更新,又可以获取到最新的 state 值呢?
如何避免
我们可以使用 useRef 来存一个函数,每次更新的时候设置 ref.current 的值,通过函数来获取最新的 state 值。
const App: React.FC = () => {
const [count, setCount] = React.useState(0);
const ref = React.useRef<VoidFunction>();
useEffect(() => {
const time = setInterval(() => {
setCount((count) => count + 1);
}, 1000);
return () => {
clearInterval(time);
};
}, []);
const fn = () => {
console.log("count:", count);
};
ref.current = fn;
const handleClick = useCallback(() => {
ref.current();
}, []);
return (
<>
<h2>{count}</h2>
<Child onClick={handleClick} />
</>
);
};
总结
解决闭包陷阱的方法:
- 当页面更新频率低时,不要使用 useMemo、useCallback 缓存函数来优化页面,在真实影响体验的情况下再来考虑优化;
- 将更新依赖的参数写进 useCallback 的第二个参数中;
- 使用 useRef 来保存函数,用一个函数实时获取最新的 state 值;