不要再用useCallback了!分析React Hooks正确使用方式

前言

之前整理过 整理下最近做的产品里 比较典型的代码规范问题,里面有一个关于React Hooks的规范,当时只是提了一下,今天详细说下想法,分析下正确使用方式,欢迎大佬参与讨论。

结论

先上结论:

  1. 使用React内置Hooks:
    1. useState:需要设置必要初始值。
    2. useEffect:不要随意加依赖项。推荐使用 [useMount](#推荐使用 useMount "#useMount")
    3. useCallback React.memo不要再用了!(如果有效率问题,可以考虑使用,仅仅是考虑!)
    4. useMemo:多数情况下不需要用,可以用在复杂业务逻辑中。
    5. useRef不要在组件或DOM上的 ref.current 上随意设置属性。
    6. ESLint rule react/display-name:函数组件名定义。
  2. 第三方封装好的Hooks:
    1. useMount:只在组件初始化时执行的 Hook
    2. useUnmount:在组件卸载(unmount)时执行的 Hook
    3. useUnmountedRef:获取当前组件是否已经卸载的 Hook
    4. useUpdateEffect:对应 useEffect Hooks,但是会忽略第一次渲染(比如 mount),类似类组件里的 componentUpdate
    5. useLatest:返回当前最新值的 Hook,可以避免闭包问题。useLatest 返回的永远是最新值。
    6. useSetState:管理 object 类型 state 的 Hooks,用法与 class 组件的 this.setState 基本一致。只支持浅层对象合并。
  3. 闭包问题:
    1. 推荐使用 [useLatest](#推荐使用 useLatest 解决闭包问题。 "#useLatest") 解决闭包问题。

1. 使用React内置Hooks注意事项

  1. 使用每个Hook时,都要想下两个问题:这个Hook是做什么用的? 我是否真的需要使用它?
  2. 下面会介绍下每个Hooks具体使用注意事项。

1.1 useState

  1. useState 需要有必要的初始值,比如string用'', array用[],bool用false\true,对象用null\{}等。
  2. 当设置为null时,jsx里也需要有非空判断。

如果有需要定义很多歌state时,推荐使用 useSetState

1.2 useEffect

  1. 如果只需要首次渲染触发,第二个参数 dependencies 需要设置为 [],避免重复触发。
  2. 尽量避免把不需要监听触发的变量,放在 dependencies 里,避免重触发。
  3. 如果想监听某个变量改变,不想第一次渲染触发,推荐使用 useUpdateEffect
  4. dependencies 写不全会提示 eslint warning:react-hooks/exhaustive-deps,可以忽略,这个warning只是提醒要写必要的 dependencies。也不会有闭包问题。

1.3 useCallback \ React.memo

用来缓存一个函数,一般用于缓存事件回调函数。

  1. 先说结论:业务中99%情况下,不需要使用 useCallback !也不要使用 useCallback
    • useCallback 是用来减少子组件重复渲染作用的,是为了优化提高渲染效率,但是在不使用情况下,大部分场景下都不会产生效率、页面卡顿等问题。
    • useCallback 要配合子组件的 shouldComponentUpdate 或者 React.memo 一起来使用才能起到作用,否则就是反向优化。
  2. 一般有以下两种场景有需求:
    • common组件中,考虑某些组件可能会同时大量渲染,需要子组件配合 shouldComponentUpdate 或者 React.memo 使用。
    • 个别页面中,组件数量过多,或者有比较深的层级关系,比如树结构、递归结构,导致了渲染慢页面卡顿问题,可以考虑使用 useCallback 进行优化。(需要有实际测试出来的问题,再考虑使用此优化,不过有些情况下可能并不是这种重复渲染问题导致的)
  3. 反向优化
    • 要配合子组件的 shouldComponentUpdate 或者 React.memo 一起来使用才能起到作用,否则不但不会提升性能,还有可能降低性能。
    • 用了会有闭包问题,需要写依赖项,更容易出问题。
    • 大量 useCallback 定义,可读性很差
  4. 优点:
    1. 代码少;
    2. 不会有闭包问题(除了特殊case,建议用useLatest解决);
    3. 不需要写依赖项,不会有eslint warning;
    4. 可读性好;
    5. 不用import use。
  5. React.memo 同理。

1.4 useMemo

  1. 一般用于缓存比较复杂的、不想重复计算或渲染的逻辑。
  2. 如果逻辑比较简单,就没必要用了,直接定义变量就行。
  3. 也可以用来缓存局部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

  1. 如果想定义组件内的变量,可以用 useRef 定义。
    • 注意:如果有些状态不需要绑定到jsx里,只用于变量存储,这种建议用 useRef 定义,而不是定义在 state 里。
  2. 不要在子组件的 ref.current 上随意设置属性。
    • 有些业务把一些业务数据存在了某个common控件的ref上,比如 <Form ref={formRef} ... formRef.current.name = 'Mark' 上,。不要这么用,可以自己定义一个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 直接排在了第二位,你敢相信吗?

相关推荐
正小安1 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch3 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光3 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   3 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   3 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web3 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常3 小时前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇4 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr4 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
Tiffany_Ho5 小时前
【TypeScript】知识点梳理(三)
前端·typescript