自从 react@16.8 正式发布react hooks已经有一段时间了,这段时间我也一直在项目里使用hook的方式来写组件,其间也遇到了不少问题,下面列一下踩坑记录。
该篇假设你已经了解react hooks的基本用法,如果对react hooks毫不了解,建议先阅读官方文档。
想在第一次render前执行的代码,可以放在useState里
类似class component里的constructor
和componentWillMount
。例如
const instance = useRef(null);
useState(() => {
instance.current = 'initial value';
});
useState里数据务必为immutable
虽然class component的state也提倡使用immutable数据,但不是强制的,因为只要调用了setState
就会触发更新。但是使用useState
时,如果在更新函数里传入同一个对象将无法触发更新。
举个例子,有时可能会写出这种代码
const [list, setList] = useState([2,32,1,534,44]);
return (
<>
<ol>
{list.map(v => <li key={v}>{v}</li>)}
</ol>
<button
onClick={() => {
// bad 这样无法触发更新
setList(list.sort((a, b) => a - b));
// good 必须传入一个新的对象
setList(list.slice().sort((a, b) => a - b));
}}
>sort</button>
</>
)
useMemo has no semantic guarantee
这句话出自useMemo的API Reference。
You may rely on
useMemo
as a performance optimization, not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works withoutuseMemo
— and then add it to optimize performance.
也就是说在未来react的版本,useMemo
在必要时会清除缓存而重新执行creater function,所以最好不要使用useMemo
来执行具有side effect的函数。
举个例子,比如我想实现基于lodash throttle的hook,使用useMemo
来实现会比较理想,类似下面的代码
const thRef = useRef(null);
useMemo(() => {
if (thRef.current) {
thRef.current.cancel();
}
thRef.current = _.throttle(fn, wait, options);
}, [wait, options && options.leading, options &&options.trailing]);
// ...other codes
目前这么写是没问题,但是考虑到未来react实现了上述“forget”特性的话,这样的代码就可能产生预料之外的结果,解决方法就是自己实现一个稳定的useMemo,实现示例。
useEffect和useLayoutEffect有什么区别?
简单来说就是调用时机不同,useLayoutEffect
和原来componentDidMount
&componentDidUpdate
一致,在react完成DOM更新后马上同步调用的代码,会阻塞页面渲染。而useEffect
是会在整个页面渲染完才会调用的代码。
官方建议优先使用useEffect
However, we recommend starting with useEffect first and only trying useLayoutEffect if that causes a problem.
在实际使用时如果想避免页面抖动(在useEffect
里修改DOM很有可能出现)的话,可以把需要操作DOM的代码放在useLayoutEffect
里。
不过useLayoutEffect
在服务端渲染时会出现一个warning,要消除的话得用useEffect
代替或者推迟渲染时机。见说明和讨论。
在useEffect和useLayoutEffect里使用async函数
因为async函数肯定会返回一个Promise
,会和useEffect
返回的cleanup函数混淆所以不要直接将async function
传给useEffect
,最简单的解决方法是IIFE
。
useEffect(() => {
(async () => {
await fetchSomething();
})();
}, []);
使用useCallback时,要注意闭包问题(或者说是Capture Value特性)
和避免使用inline function一样,有时需要使用useCallback
来优化性能,但是useCallback
会返回之前的闭包,使用到的局部变量是不会更新的。
举个例子
const [count, setCount] = useState(0);
const increaseCount = useCallback(() => {
setCount(count + 1);
}, []);
// 因为useCallback总是返回第一次render时传入的闭包,increaseCount内访问到的count永远都是0
increaseCount(); // 相当于setCount(1);
increaseCount(); // 无论调用几次都是setCount(1);
解决方法就是避免引用外部的局部变量
const [count, setCount] = useState(0);
const vRef = useRef(0);
const increaseCount = useCallback(() => {
// 传入function的话每次都能拿到最新值
setCount(prevCount => prevCount + 1);
// 用一个对象来保存,适用于不需要触发更新的情况
vRef.current += 1;
}, []);
// 使用useReducer解决
const [count, increase] = useReducer((c, increment) => c + increment, 0);
const increaseCount = useCallback(() => {
increase(1);
}, []);
但是注意不要这么写。
关于Capture Value可以参考这篇文章。
useEffect、useCallback、useMemo等API的第二个参数数组的长度不能变
有时可能会写出这样的代码
const [selectedStatuses, setSelected] = useState([]);
useEffect(() => {
fetchListById(selectedStatuses);
}, selectedStatuses);
这里如果将selectedStatuses
从[]
更新为['active']
是不会触发effect的,react也会给你一个warning。相关源码。
这里最好将整个state作为deps的一项传入,或者使用一个key来控制
useEffect(() => {
fetchListById(selectedStatuses);
}, [selectedStatuses]);
泛型参数怎么写?
通常我们会这么写函数组件
const MyCom: React.FC<MyComProps> = (props) => {
return <div>...</div>
};
但是在参数里使用了泛型,就不能这么写了,因为在变量声明里必须要指定确切的类型,所以这里要回到传统function
的写法。放心,这么写也是有类型提示的。
// generic props
interface MyComProps<T> {
value?: T;
onChange?(value: T): void;
}
function MyCom<T extends { type: string; }>(props: MyComProps<T>) {
return <div>{props.value && props.value.type}</div>;
}
不过如果要用React.forwardRef
的话目前没什么什么优雅的方案,还是需要明确类型才行。
使用React.memo
和React.forwardRef
包装的组件为什么提示我children类型不对?
过去使用Component
、FC
等类型定义组件时一般不需要我们定义props里children
的类型,因为在上述类型里已经帮你默认加上了 { children?: ReactNode }
的定义。但是@types/react
的维护者认为这样导致children
几乎没有类型约束,组件开发者应该显式地声明children
类型。所以对新的API应该都不会自动加上children
的定义了,需要开发者手动添加。
详情见讨论。
使用act
给react hooks写单元测试
react@16.8给test-utils新加了一个 act
API,关于这个API可以看看作者写的这篇通俗易懂的解释。
这里简单总结一下act
主要是为了解决useEffect
的测试问题出现的,因为useEffect
的执行时机会很晚,甚至在断言之后。如果在useEffect
里执行了ui变更,就很难写测试了,虽然可以用useLayoutEffect
解决,但是不能为了通过测试而修改原代码。这里用act
就能很好地解决了,这个API能同步执行所有useEffect
以及相应的更新,在断言时就能拿到正确的结果了。
总之所有可能触发更新的代码都应该放到act
里,不然test-utils会给一个warning。
另外act
会batchUpdate,可能会导致一个隐藏bug,见文章中这段内容,需要注意一下。
Function Component与ant design的Form
Form非常适合使用react hooks来实现,官方目前看来没什么进展,不过目前hoc也没什么问题。
用FC写的自定义表单控件会有ref相关的warning,因为antd form需要拿到组件的ref
,而FC默认是没有实例的。这里我们可以通过React.forwardRef
+ useImperativeHandle
解决,官方示例。不过这么写validateFieldsAndScroll
这个API可能就没用了,建议把ref
传给底层的表单元素组件。
我收集了一些常用的hooks,欢迎使用和一起开发。
https://frezc.github.io/react-hooks-common/
References
- useEffect vs useLayoutEffect
- react官方文档
- https://github.com/threepointone/react-act-examples/blob/master/sync.md
- https://github.com/facebook/react
- https://github.com/DefinitelyTyped/DefinitelyTyped/pull/33602
- https://ant-design.gitee.io/components/form-cn/#components-form-demo-customized-form-controls
- https://juejin.im/post/5c9827745188250ff85afe50