前言
之前整理过 整理下最近做的产品里 比较典型的代码规范问题,里面有一个关于React Hooks
的规范,当时只是提了一下,今天详细说下想法,分析下正确使用方式,欢迎大佬参与讨论。
结论
先上结论:
- 使用React内置Hooks:
useState
:需要设置必要初始值。useEffect
:不要随意加依赖项。推荐使用 [useMount](#推荐使用 useMount "#useMount")useCallback
React.memo
:不要再用了!(如果有效率问题,可以考虑使用,仅仅是考虑!)useMemo
:多数情况下不需要用,可以用在复杂业务逻辑中。useRef
:不要在组件或DOM上的ref.current
上随意设置属性。ESLint rule react/display-name
:函数组件名定义。
- 第三方封装好的Hooks:
useMount
:只在组件初始化时执行的Hook
。useUnmount
:在组件卸载(unmount)时执行的Hook
。useUnmountedRef
:获取当前组件是否已经卸载的Hook
。useUpdateEffect
:对应useEffect
Hooks,但是会忽略第一次渲染(比如 mount),类似类组件里的componentUpdate
。useLatest
:返回当前最新值的Hook
,可以避免闭包问题。useLatest
返回的永远是最新值。useSetState
:管理 object 类型 state 的Hooks
,用法与 class 组件的this.setState
基本一致。只支持浅层对象合并。
- 闭包问题:
- 推荐使用 [useLatest](#推荐使用 useLatest 解决闭包问题。 "#useLatest") 解决闭包问题。
1. 使用React内置Hooks注意事项
- 使用每个Hook时,都要想下两个问题:
这个Hook是做什么用的?
我是否真的需要使用它?
- 下面会介绍下每个Hooks具体使用注意事项。
1.1 useState
useState
需要有必要的初始值,比如string用''
, array用[]
,bool用false\true
,对象用null\{}
等。- 当设置为
null
时,jsx里也需要有非空判断。
如果有需要定义很多歌state时,推荐使用 useSetState
1.2 useEffect
- 如果只需要首次渲染触发,第二个参数
dependencies
需要设置为[]
,避免重复触发。- 推荐使用 useMount
- 尽量避免把不需要监听触发的变量,放在
dependencies
里,避免重触发。 - 如果想监听某个变量改变,不想第一次渲染触发,推荐使用 useUpdateEffect
dependencies
写不全会提示 eslint warning:react-hooks/exhaustive-deps
,可以忽略,这个warning只是提醒要写必要的dependencies
。也不会有闭包问题。
1.3 useCallback \ React.memo
用来缓存一个函数,一般用于缓存事件回调函数。
- 先说结论:业务中99%情况下,不需要使用
useCallback
!也不要使用useCallback
!useCallback
是用来减少子组件重复渲染作用的,是为了优化提高渲染效率,但是在不使用情况下,大部分场景下都不会产生效率、页面卡顿等问题。useCallback
要配合子组件的shouldComponentUpdate
或者React.memo
一起来使用才能起到作用,否则就是反向优化。
- 一般有以下两种场景有需求:
- common组件中,考虑某些组件可能会同时大量渲染,需要子组件配合
shouldComponentUpdate
或者React.memo
使用。 - 个别页面中,组件数量过多,或者有比较深的层级关系,比如树结构、递归结构,导致了渲染慢页面卡顿问题,可以考虑使用
useCallback
进行优化。(需要有实际测试出来的问题,再考虑使用此优化,不过有些情况下可能并不是这种重复渲染问题导致的)
- common组件中,考虑某些组件可能会同时大量渲染,需要子组件配合
- 反向优化 :
- 要配合子组件的
shouldComponentUpdate
或者React.memo
一起来使用才能起到作用,否则不但不会提升性能,还有可能降低性能。 - 用了会有闭包问题,需要写依赖项,更容易出问题。
- 大量
useCallback
定义,可读性很差。
- 要配合子组件的
- 优点:
- 代码少;
- 不会有闭包问题(除了特殊case,建议用useLatest解决);
- 不需要写依赖项,不会有eslint warning;
- 可读性好;
- 不用import use。
React.memo
同理。
1.4 useMemo
- 一般用于缓存比较复杂的、不想重复计算或渲染的逻辑。
- 如果逻辑比较简单,就没必要用了,直接定义变量就行。
- 也可以用来缓存局部render,参考例子。
jsx
// 1. 缓存复杂逻辑
const data = useMemo(() => {
const newList = list.map(item => { /* ... */ })
newList.push({ /* ... */ });
return newList;
}, [list]);
const data = useMemo(() => {
return // 复杂计算逻辑
}, [item.num1, item.num2]);
// 2. 如果逻辑比较简单,就没必要用了,直接定义变量就行
const data = useMemo(() => {
return !(selectedItems.length === 1 && selectedItems[0].status !== Status.Inactive);
}, [selectedItems]);
const data = useMemo(() => {
return status == 'pending' ? 'red' : 'blue';
}, [status]);
// good 可以直接定义变量,或者直接写在jsx里
const data = !(selectedItems.length === 1 && selectedItems[0].status !== Status.Inactive);
const data = status == 'pending' ? 'red' : 'blue';
// 3.
const render = useMemo(() => {
// 复杂逻辑
return (
<div>
{list.map((item, index) => (<div key={index}>{item.name}</div>))}
</div>
);
}, [list]);
1.5 useRef
- 如果想定义组件内的变量,可以用
useRef
定义。- 注意:如果有些状态不需要绑定到jsx里,只用于变量存储,这种建议用
useRef
定义,而不是定义在state
里。
- 注意:如果有些状态不需要绑定到jsx里,只用于变量存储,这种建议用
- 不要在子组件的
ref.current
上随意设置属性。- 有些业务把一些业务数据存在了某个common控件的ref上,比如
<Form ref={formRef} ... formRef.current.name = 'Mark'
上,。不要这么用,可以自己定义一个ref随便赋值。
- 有些业务把一些业务数据存在了某个common控件的ref上,比如
1.6 函数组件名定义
ESLint rule对应:react/display-name
github.com/jsx-eslint/... 建议启用,原因详见link说明。
大多出现在定义匿名函数组件、或使用高阶组件的地方。
js
// bad
export default React.forwardRef((props, ref) => {
return (<div ref={ref}>...</div>);
});
// good
const Page1 = (props, ref) => {
return (<div ref={ref}>...</div>);
};
export default React.forwardRef(Page1);
js
// bad
const Page1 = React.forwardRef((props, ref) => {
return (<div ref={ref}>...</div>);
});
// good
const Page1 = React.forwardRef((props, ref) => {
return (<div ref={ref}>...</div>);
});
Page1.displayName = "Page1";
js
// bad
export default () => {
return (<div>...</div>);
}
// good
const Page1 = () => {
return (<div>...</div>);
}
export default Page1;
// or
export default function Page1s() {
return (<div>...</div>);
}
2. 第三方封装好的Hooks
这里主要介绍下第三方比较流行的 React Hooks 库 ahooks
如果代码里只需要用到几个常用的Hooks,推荐参考源码定义到自己工程common中使用。
下面介绍下几个比较常用的Hooks:
2.1 useMount
只在组件初始化时执行的 Hook
。注意不支持return () => { }
方式卸载,需要卸载用 useUnmount
。
优点:不用写 dependencies
,也不会有eslint warning。
js
useMount(() => {
// component mount
});
2.2 useUnmount
在组件卸载(unmount)时执行的 Hook
。
js
useUnmount(() => {
// component unmount
});
2.3 useUnmountedRef
获取当前组件是否已经卸载的 Hook
。
js
const unmountedRef = useUnmountedRef();
useMount(() => {
setTimeout(() => {
if (!unmountedRef.current) {
// component is alive
}
}, 3000);
});
2.4 useUpdateEffect
对应 useEffect
Hooks,但是会忽略第一次渲染(比如 mount),类似类组件里的 componentUpdate
。
js
// 首次渲染不执行,之后每次渲染都会执行。
useUpdateEffect(() => {
console.log('useUpdateEffect')
});
// 首次渲染不执行,由于没有deps,所有后续渲染都不会执行。
useUpdateEffect(() => {
console.log('useUpdateEffect')
}, []);
// 首次渲染不执行,deps变化时每次执行。
useUpdateEffect(() => {
console.log('useUpdateEffect')
}, [count]);
2.5 useLatest
返回当前最新值的 Hook
,可以避免闭包问题。useLatest
返回的永远是最新值。
注意不要滥用,有需要用再用。
js
const [count, setCount] = useState(0);
const latestCountRef = useLatest(count);
useMount(() => {
const interval = setInterval(() => {
setCount(latestCountRef.current + 1);
}, 1000);
return () => clearInterval(interval);
});
2.6 useSetState
管理 object 类型 state 的 Hooks
,用法与 class 组件的 this.setState
基本一致。只支持浅层对象合并。
js
const [state, setState] = useSetState({
text: '',
count: 0,
active: false,
});
const onClick = () => {
setState({ text: 't2', count: state.count + 1, active: true });
// or
setState((prev) => ({ count: prev.count + 1, active: !prev.active }));
}
总结
本文主要分析了下一些常用的React Hooks
的使用看法及建议。另外介绍了一些比较常用的第三方Hooks的封装和使用。
总之,不要再用 useCallback
了! 赶快把代码里的useCallback
以及加的 dependencies
都删了吧!
我觉得 useCallback
useMemo
这种Hooks只是为了解决一些特殊case而使用的,类似 useTransition
useDeferredValue
这种Hooks,为了特殊需求而出的解决方案。用了 useCallback
确实可以优化效率问题,但是以现代系统主流性能,以及浏览器的性能,已经足够支撑普通页面或者一些较为复杂的页面。一个好的框架应该是 易用 高效率开发 维护性高 ,正常开发时不需要关心我这块会不会有效率问题啊、这个Hook能不能用啊这种问题,等遇到问题了,像效率优化啊,或者复杂的需求这些,应该交给专业的解决方案去解决。不要为了少部分的需求 去影响大部分的使用者。
最后八卦下,刚发现 React官网 里Hooks列表竟然是按字母排序的,useCallback
直接排在了第二位,你敢相信吗?