前言
之前整理过 整理下最近做的产品里 比较典型的代码规范问题,里面有一个关于React Hooks的规范,当时只是提了一下,今天详细说下想法,分析下正确使用方式,欢迎大佬参与讨论。
结论
先上结论:
- 使用React内置Hooks:
useState:需要设置必要初始值。useEffect:不要随意加依赖项。推荐使用 [useMount](#推荐使用 useMount "#useMount")useCallbackReact.memo:不要再用了!(如果有效率问题,可以考虑使用,仅仅是考虑!)useMemo:多数情况下不需要用,可以用在复杂业务逻辑中。useRef:不要在组件或DOM上的ref.current上随意设置属性。ESLint rule react/display-name:函数组件名定义。
 - 第三方封装好的Hooks:
useMount:只在组件初始化时执行的Hook。useUnmount:在组件卸载(unmount)时执行的Hook。useUnmountedRef:获取当前组件是否已经卸载的Hook。useUpdateEffect:对应useEffectHooks,但是会忽略第一次渲染(比如 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 直接排在了第二位,你敢相信吗?
