React Hooks发布快一年了,我在项目中大量使用,总结了一些进阶最佳实践,今天分享给大家。

React,Hooks,从,2019年2月,React,16.8,正式,发布,到,现在,已经,快,一年,了,Hooks,的,出现,彻底,改变,了,我们,写,React,组件,的,方式,让,函数,组件,也,能,拥有,状态,和,生命周期,能力,代码,更,简洁,更,易复用。

我,在,项目,中,大量,使用,Hooks,也,踩,了,不少,坑,积累,了,一些,进阶,最佳,实践,今天,这篇,文章,就,来,分享,我,总结,的,React,Hooks,进阶,最佳,实践,包括,常用,Hooks,的,使用,技巧,自定义,Hooks,的,设计,性能,优化,常见,坑,和,解决,方案,等等,希望,能,给,使用,React,Hooks,的,朋友,一些,参考,和,启发。

一、useState的进阶技巧

useState,是,最,常用,的,Hook,用来,在,函数,组件,中,添加,状态,基本,用法,很,简单,但是,也,有,一些,进阶,技巧,很多,人,可能,不,太,了解。

1. 函数式更新,避免闭包陷阱

useState,返回,的,更新,函数,支持,函数式,更新,也就是,传入,一个,函数,参数,是,上一个,状态,返回,新,状态,这,在,新,状态,依赖,旧,状态,的,时候,很,有用,也,能,避免,闭包,陷阱。

比如:

// 不推荐,可能有闭包问题
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);

// 推荐,函数式更新
const increment = () => setCount(prevCount => prevCount + 1);

函数式,更新,的,好处,是,它,能,保证,拿到,的,是,最新,的,状态,不,会,受,闭包,影响,尤其是,在,异步,操作,或者,setTimeout,里,更新,状态,的,时候,用,函数式,更新,能,避免,拿到,旧,状态,的,问题,所以,当,新,状态,依赖,旧,状态,的,时候,推荐,用,函数式,更新。

2. 惰性初始化,避免重复计算

useState,的,初始值,支持,传入,一个,函数,进行,惰性,初始化,也就是,这个,函数,只,在,组件,第一次,渲染,的,时候,执行,一次,返回,初始,状态,后续,渲染,不会,再,执行,这,在,初始值,计算,比较,昂贵,的,时候,很,有用,能,避免,每次,渲染,都,重复,计算。

比如:

// 不推荐,每次渲染都会执行 expensiveComputation
const [state, setState] = useState(expensiveComputation(props));

// 推荐,惰性初始化,只在第一次渲染执行
const [state, setState] = useState(() => expensiveComputation(props));

如果,初始值,需要,复杂,计算,或者,从,localStorage,读取,等等,比较,耗时,的,操作,一定,要,用,惰性,初始化,传入,函数,而,不是,直接,传,计算,结果,不然,每次,组件,渲染,都会,执行,这个,昂贵,的,计算,影响,性能。

3. 复杂状态用useReducer,不要用多个useState

如果,组件,的,状态,比较,复杂,有,多个,相关,的,状态,字段,或者,状态,更新,逻辑,比较,复杂,不要,用,多个,useState,来,管理,推荐,用,useReducer,来,管理,复杂,状态,代码,更,清晰,也,更,易维护。

比如,一个,表单,有,多个,字段,还有,加载,状态,错误,状态,等等,用,多个,useState,会,很,散,更新,逻辑,也,分散,不好,维护,用,useReducer,把,相关,状态,放,在,一起,更新,逻辑,也,集中,在,reducer,里,更,清晰,更,易维护。

二、useEffect的进阶技巧

useEffect,是,另一个,最,常用,的,Hook,用来,处理,副作用,比如,数据,请求,订阅,DOM,操作,等等,但是,useEffect,也,是,最,容易,踩,坑,的,Hook,很多,人,用,不好,下面,说,一些,进阶,技巧。

1. 依赖数组一定要正确,不要遗漏

useEffect,的,第二个,参数,是,依赖,数组,指定,这个,effect,依赖,哪些,变量,只有,这些,变量,变化,的,时候,effect,才,会,重新,执行,依赖,数组,一定,要,正确,不要,遗漏,依赖,也,不要,多,写,不,相关,的,依赖,不然,会,出,问题。

很多,人,容易,遗漏,依赖,导致,effect,里,用,了,旧,的,变量,值,出现,bug,比如,effect,里,用,了,props.id,但是,依赖,数组,没,写,props.id,结果,props.id,变化,了,effect,没有,重新,执行,还是,用,的,旧,的,id,数据,就,错,了,所以,写,useEffect,的,时候,一定,要,检查,effect,里,用,了,哪些,变量,都,要,放,到,依赖,数组,里,不要,遗漏。

当然,也,不要,为了,不,遗漏,就,把,所有,变量,都,放,进去,只,放,effect,真正,依赖,的,变量,不,相关,的,不要,放,不然,会,导致,effect,频繁,重新,执行,影响,性能,也,可能,造成,无限,循环,等,问题,所以,依赖,数组,要,准确,只,包含,effect,真正,依赖,的,变量。

2. 清理函数一定要写,避免内存泄漏

useEffect,可以,返回,一个,清理,函数,在,组件,卸载,或者,effect,重新,执行,前,调用,用来,清理,副作用,比如,取消,订阅,清除,定时器,取消,请求,等等,这,很,重要,不,写,清理,函数,可能,会,导致,内存,泄漏,或者,其他,问题。

比如:

useEffect(() => {
  const timer = setInterval(() => {
    console.log('tick');
  }, 1000);
  
  // 清理函数,清除定时器
  return () => clearInterval(timer);
}, []);

如果,不,写,清理,函数,组件,卸载,了,定时器,还,在,运行,就,会,内存,泄漏,而且,可能,在,组件,卸载,后,还,尝试,更新,状态,导致,警告,和,bug,所以,只要,effect,里,有,订阅,定时器,事件,监听,等等,需要,清理,的,副作用,一定,要,写,清理,函数,避免,内存,泄漏,和,其他,问题。

3. 不要在effect里直接写async函数

很多,人,喜欢,在,useEffect,里,直接,写,async,函数,来,处理,异步,请求,但是,这,是,不对,的,因为,useEffect,的,回调,函数,如果,是,async,的,返回,的,是,Promise,而,不是,清理,函数,会,导致,问题,React,也,会,警告。

错误,写法:

// 错误,async函数返回Promise,不是清理函数
useEffect(async () => {
  const data = await fetchData();
  setData(data);
}, []);

正确,写法,是,在,effect,里,定义,一个,async,函数,然后,调用,它:

useEffect(() => {
  const fetchData = async () => {
    const data = await api.getData();
    setData(data);
  };
  fetchData();
}, []);

这样,effect,的,回调,还是,普通,函数,返回,undefined,或者,清理,函数,不会,有,问题,async,逻辑,封装,在,内部,函数,里,正常,执行,这,是,正确,的,写法,大家,要,注意,不要,直接,给,useEffect,传,async,函数。

4. 多个effect分离职责,不要写在一起

如果,组件,有,多个,不相关,的,副作用,不要,都,写,在,一个,useEffect,里,要,分成,多个,useEffect,每个,effect,只,做,一件,事,职责,单一,这样,代码,更,清晰,也,更,易维护,依赖,也,更,清晰,不,容易,出,问题。

比如,一个,组件,既要,请求,用户,数据,又,要,监听,窗口,大小,变化,这,两个,是,不相关,的,副作用,应该,分成,两个,useEffect,不要,写,在,一个,里,这样,每个,effect,的,逻辑,和,依赖,都,清晰,也,不,会,因为,一个,副作用,的,依赖,变化,导致,另一个,也,重新,执行。

三、useMemo和useCallback的正确使用

useMemo,和,useCallback,是,用来,做,性能,优化,的,Hook,useMemo,用来,缓存,计算,结果,useCallback,用来,缓存,函数,引用,但是,很多,人,用,不好,要么,滥用,要么,没用,对,下面,说,说,正确,的,使用,方法。

1. 不要滥用,不是所有东西都要缓存

很多,人,觉得,useMemo,和,useCallback,能,优化,性能,就,到处,用,所有,计算,都,包,useMemo,所有,函数,都,包,useCallback,其实,这,是,不对,的,useMemo,和,useCallback,本身,也,有,开销,它们,需要,保存,缓存,比较,依赖,等等,如果,计算,很,简单,或者,函数,很,简单,用,useMemo,和,useCallback,反而,可能,比,不用,更,慢,因为,缓存,的,开销,超过,了,重新,计算,的,开销,所以,不要,滥用,只,在,真正,需要,的,地方,用。

什么时候,真正,需要,呢,对于,useMemo,当,计算,很,昂贵,比如,复杂,的,循环,大量,数据,处理,等等,重新,计算,开销,大,的,时候,用,useMemo,缓存,结果,避免,每次,渲染,都,重新,计算,对于,useCallback,当,函数,作为,props,传给,子组件,而,子组件,用,React.memo,优化,了,的,时候,用,useCallback,缓存,函数,引用,避免,子组件,因为,函数,引用,变化,而,不必要,重新,渲染,这,两种,情况,是,真正,需要,用,的,其他,情况,不,需要,用,用,了,反而,增加,开销。

2. useMemo缓存计算结果,依赖要正确

useMemo,的,用法,是,传入,一个,计算,函数,和,依赖,数组,返回,缓存,的,计算,结果,只有,依赖,变化,的,时候,才,重新,计算,和,useEffect,类似,依赖,数组,一定,要,正确,包含,计算,函数,里,用,的,所有,变量,不要,遗漏,不然,会,拿到,旧,值,出,bug。

比如:

const filteredList = useMemo(() => {
  return list.filter(item => item.type === filterType);
}, [list, filterType]);

这里,计算,用,了,list,和,filterType,依赖,数组,就,要,包含,这,两个,不要,遗漏,不然,filterType,变化,了,filteredList,不会,重新,计算,还是,旧,结果,就,错,了。

3. useCallback缓存函数引用,配合React.memo使用

useCallback,的,用法,是,传入,一个,函数,和,依赖,数组,返回,缓存,的,函数,引用,只有,依赖,变化,的,时候,才,返回,新,的,函数,引用,否则,返回,缓存,的,引用,useCallback,主要,用,在,函数,作为,props,传给,子组件,的,场景,配合,React.memo,使用,避免,子组件,不必要,重新,渲染。

比如:

// 父组件
const handleClick = useCallback(() => {
  console.log('clicked');
}, []);

return <Child onClick={handleClick} />;

// 子组件,用React.memo优化
const Child = React.memo(({ onClick }) => {
  return <button onClick={onClick}>Click</button>;
});

如果,不用,useCallback,父组件,每次,渲染,都会,创建,新,的,handleClick,函数,引用,变化,导致,React.memo,失效,子组件,每次,都,重新,渲染,用,了,useCallback,handleClick,引用,稳定,子组件,就,不,会,不必要,重新,渲染,了,这,就是,useCallback,的,正确,使用,场景,要,配合,React.memo,才,有,意义,如果,子组件,没,用,React.memo,用,useCallback,也,没,什么,用,因为,子组件,本来,就,会,随,父组件,重新,渲染。

四、自定义Hooks的设计原则

自定义,Hooks,是,React,Hooks,的,精髓,能,把,组件,逻辑,抽取,出来,复用,但是,自定义,Hooks,的,设计,也,有,一些,原则,和,技巧,下面,说,说。

1. 命名以use开头,这是约定

自定义,Hook,的,命名,一定,要,以,use,开头,比如,useUserData,useWindowSize,等等,这,是,React,的,约定,也是,ESLint,插件,检查,Hooks,规则,的,依据,只有,以,use,开头,的,函数,才,被,认为,是,Hook,才,会,检查,Hooks,规则,所以,自定义,Hook,一定,要,以,use,开头,不要,用,其他,命名,不然,可能,会,有,问题。

2. 只封装可复用逻辑,不要过度抽象

自定义,Hook,是,用来,封装,可,复用,的,逻辑,的,如果,一段,逻辑,在,多个,组件,里,都,用,到,就,可以,抽取,成,自定义,Hook,复用,但是,不要,为了,用,自定义,Hook,而,过度,抽象,把,只,用,一次,的,逻辑,也,抽取,成,Hook,这样,反而,增加,复杂度,降低,可读性,所以,自定义,Hook,只,封装,真正,可,复用,的,逻辑,不要,过度,抽象。

而且,自定义,Hook,的,职责,要,单一,一个,Hook,只,做,一件,事,不要,把,不相关,的,逻辑,都,塞,到,一个,Hook,里,不然,Hook,会,很,复杂,难,用,也,难,维护,要,拆分,成,多个,小,的,职责,单一,的,Hook,组合,使用,这样,更,灵活,也,更,易复用。

3. 返回值设计要清晰,优先用对象返回

自定义,Hook,的,返回值,设计,也,很,重要,要,清晰,易懂,方便,使用,一般,来说,如果,返回,多个,值,推荐,用,对象,返回,而,不是,数组,因为,对象,返回,调用,方,可以,按需,解构,不用,关心,顺序,也,方便,以后,扩展,新,的,返回值,不会,影响,现有,调用,而,数组,返回,顺序,固定,扩展,麻烦,调用,方,也,要,记住,顺序,不,方便。

比如:

// 推荐,对象返回
const useUserData = (userId) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    // fetch data
  }, [userId]);
  
  return { data, loading, error };
};

// 使用,按需解构
const { data, loading } = useUserData(1);

对象,返回,调用,方,只,解构,需要,的,字段,不用,关心,其他,字段,顺序,也,不,重要,以后,Hook,增加,新,的,返回,字段,也,不,影响,现有,调用,很,灵活,所以,推荐,用,对象,返回,除非,返回值,很,简单,只有,一两个,而且,类似,useState,那样,有,明确,的,顺序,语义,才,用,数组,返回。

4. 自定义Hook也要遵守Hooks规则

自定义,Hook,内部,也,要,遵守,Hooks,规则,比如,只能,在,顶层,调用,Hooks,不能,在,循环,条件,嵌套,函数,里,调用,依赖,数组,要,正确,等等,和,在,组件,里,用,Hooks,一样,要,遵守,规则,不然,会,出,bug,而且,自定义,Hook,内部,用,的,其他,Hooks,的,依赖,也要,正确,不要,因为,是,自定义,Hook,就,忽略,依赖,问题。

五、常见的坑和解决方案

最后,说,一些,React,Hooks,常见,的,坑,和,解决,方案,帮,大家,避免,踩,坑。

坑一:无限循环

这,是,最,常见,的,坑,之一,useEffect,或者,useMemo,的,依赖,数组,里,有,对象,或者,数组,每次,渲染,都,是,新,的,引用,导致,依赖,变化,effect,重新,执行,然后,又,更新,状态,导致,重新,渲染,又,触发,effect,无限,循环。

比如:

// 可能无限循环
const [data, setData] = useState([]);

useEffect(() => {
  fetchData().then(res => {
    setData(res.data);
  });
}, [data]); // data是数组,每次更新都是新引用,导致无限循环

解决,方案,是,检查,依赖,数组,不要,把,会,在,effect,里,更新,的,状态,放,到,依赖,里,也,不要,把,每次,渲染,都,新,创建,的,对象,数组,函数,放,到,依赖,里,除非,用,useMemo,或者,useCallback,稳定,了,引用,要,仔细,分析,effect,真正,依赖,什么,只,放,真正,需要,的,依赖,避免,不必要,的,重新,执行,和,无限,循环。

坑二:闭包陷阱,拿到旧值

这,也是,常见,的,坑,在,useEffect,或者,事件,处理,函数,里,因为,闭包,拿到,的,是,旧,的,状态,或者,props,值,导致,bug,尤其是,在,setTimeout,setInterval,或者,异步,回调,里,容易,出现。

解决,方案,有,几个,一是,用,函数式,更新,更新,状态,的,时候,用,函数式,更新,拿到,最新,状态,二是,把,用到,的,变量,放,到,useEffect,的,依赖,数组,里,让,变量,变化,时,effect,重新,执行,拿到,新值,三是,用,useRef,保存,最新,值,在,需要,的,地方,从,ref,里,读,最新,值,根据,不同,场景,选择,合适,的,方案,避免,闭包,陷阱。

坑三:忘记清理副作用

前面,说,过,useEffect,里,的,副作用,比如,订阅,定时器,事件,监听,等等,要,写,清理,函数,不然,会,内存,泄漏,很多,人,容易,忘记,写,清理,函数,导致,问题,所以,写,useEffect,的,时候,要,养成,习惯,只要,有,需要,清理,的,副作用,就,写,清理,函数,返回,出去,避免,内存,泄漏。

坑四:在条件或循环里调用Hooks

Hooks,规则,要求,只能,在,组件,顶层,调用,Hooks,不能,在,条件,循环,嵌套,函数,里,调用,不然,会,导致,Hooks,调用,顺序,不一致,出,bug,很多,人,容易,犯,这个,错误,比如,在,if,里,调用,useState,或者,useEffect,这,是,不对,的,一定要,在,顶层,调用,Hooks,条件,判断,要,放,在,Hook,内部,或者,用,其他,方式,处理,不要,在,条件,里,调用,Hooks。

六、写在最后

React,Hooks,是,React,近年来,最,重要,的,更新,之一,彻底,改变,了,我们,写,React,组件,的,方式,让,函数,组件,更,强大,代码,更,简洁,更,易复用,但是,Hooks,也,有,一些,学习,成本,和,容易,踩,的,坑,需要,我们,去,学习,和,积累,经验,才能,用,好。

今天,分享,了,我,在,项目,中,使用,React,Hooks,总结,的,一些,进阶,最佳,实践,包括,useState,useEffect,useMemo,useCallback,的,使用,技巧,自定义,Hooks,的,设计,原则,以及,常见,的,坑,和,解决,方案,希望,能,给,大家,一些,参考,和,启发,帮,大家,写,出,更,优雅,更,高效,的,React,代码。

当然,这些,只是,我,个人,的,一些,经验,和,心得,可能,不,一定,都,对,也,不,一定,适合,所有,场景,仅供,参考,大家,还是,要,根据,自己,的,实际,项目,情况,选择,合适,的,方法,而且,React,Hooks,也,在,不断,发展,新,的,Hooks,和,最佳,实践,也,在,不断,出现,大家,要,多,关注,官方,文档,和,社区,动态,不断,学习,更新,自己,的,知识。

最后,希望,大家,都,能,用,好,React,Hooks,写,出,更,优秀,的,React,应用,也,欢迎,大家,在,评论,区,分享,你们,使用,React,Hooks,的,经验,和,踩,过,的,坑,一起,交流,一起,进步,谢谢,大家,的,阅读。