轻松学会 React 钩子:以 useEffect() 为例

作者: 阮一峰

日期: 2020年9月15日

五年多前,我写过 React 系列教程。不用说,内容已经有些过时了。

我本来不想碰它们了,觉得框架一直在升级,教程写出来就会过时。

但是,最近我逐渐体会到 React 钩子(hooks)非常好用,重新认识了 React 这个框架,觉得应该补上关于钩子的部分。

下面就来谈谈,怎样正确理解钩子,并且深入剖析最重要的钩子之一的useEffect()。内容会尽量通俗,让不熟悉 React 的朋友也能看懂。欢迎大家参考我以前写的《React 框架入门》《React 最常用的四个钩子》

本文得到了 开课吧 的支持,结尾有 React 视频学习资料。希望通过视频来系统学习 React 的同学,可以关注。

一、React 的两套 API

以前,React API 只有一套,现在有两套:类(class)API 和基于函数的钩子(hooks) API。

任何一个组件,可以用类来写,也可以用钩子来写。下面是类的写法。


class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

再来看钩子的写法,也就是函数。


function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

这两种写法,作用完全一样。初学者自然会问:"我应该使用哪一套 API?"

官方推荐使用钩子(函数),而不是类。因为钩子更简洁,代码量少,用起来比较"轻",而类比较"重"。而且,钩子是函数,更符合 React 函数式的本质。

下面是类组件(左边)和函数组件(右边)代码量的比较。对于复杂的组件,差的就更多。

但是,钩子的灵活性太大,初学者不太容易理解。很多人一知半解,很容易写出混乱不堪、无法维护的代码。那就不如使用类了。因为类有很多强制的语法约束,不容易搞乱。

二、类和函数的差异

严格地说,类组件和函数组件是有差异的。不同的写法,代表了不同的编程方法论。

类(class)是数据和逻辑的封装。 也就是说,组件的状态和操作方法是封装在一起的。如果选择了类的写法,就应该把相关的数据和操作,都写在同一个 class 里面。

函数一般来说,只应该做一件事,就是返回一个值。 如果你有多个操作,每个操作应该写成一个单独的函数。而且,数据的状态应该与操作方法分离。根据这种理念,React 的函数组件只应该做一件事情:返回组件的 HTML 代码,而没有其他的功能。

还是以上面的函数组件为例。


function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

这个函数只做一件事,就是根据输入的参数,返回组件的 HTML 代码。这种只进行单纯的数据计算(换算)的函数,在函数式编程里面称为 "纯函数"(pure function)。

三、副效应是什么?

看到这里,你可能会产生一个疑问:如果纯函数只能进行数据计算,那些不涉及计算的操作(比如生成日志、储存数据、改变应用状态等等)应该写在哪里呢?

函数式编程将那些跟数据计算无关的操作,都称为 "副效应" (side effect) 。如果函数内部直接包含产生副效应的操作,就不再是纯函数了,我们称之为不纯的函数。

纯函数内部只有通过间接的手段(即通过其他函数调用),才能包含副效应。

四、钩子(hook)的作用

说了半天,那么钩子到底是什么?

一句话,钩子(hook)就是 React 函数组件的副效应解决方案,用来为函数组件引入副效应。 函数组件的主体只应该用来返回组件的 HTML 代码,所有的其他操作(副效应)都必须通过钩子引入。

由于副效应非常多,所以钩子有许多种。React 为许多常见的操作(副效应),都提供了专用的钩子。

  • useState():保存状态
  • useContext():保存上下文
  • useRef():保存引用
  • ......

上面这些钩子,都是引入某种特定的副效应,而 useEffect()是通用的副效应钩子 。找不到对应的钩子时,就可以用它。其实,从名字也可以看出来,它跟副效应(side effect)直接相关。

五、useEffect() 的用法

useEffect()本身是一个函数,由 React 框架提供,在函数组件内部调用即可。

举例来说,我们希望组件加载以后,网页标题(document.title)会随之改变。那么,改变网页标题这个操作,就是组件的副效应,必须通过useEffect()来实现。


import React, { useEffect } from 'react';

function Welcome(props) {
  useEffect(() => {
    document.title = '加载完成';
  });
  return <h1>Hello, {props.name}</h1>;
}

上面例子中,useEffect()的参数是一个函数,它就是所要完成的副效应(改变网页标题)。组件加载以后,React 就会执行这个函数。(查看运行结果

useEffect()的作用就是指定一个副效应函数,组件每渲染一次,该函数就自动执行一次。组件首次在网页 DOM 加载后,副效应函数也会执行。

六、useEffect() 的第二个参数

有时候,我们不希望useEffect()每次渲染都执行,这时可以使用它的第二个参数,使用一个数组指定副效应函数的依赖项,只有依赖项发生变化,才会重新渲染。


function Welcome(props) {
  useEffect(() => {
    document.title = `Hello, ${props.name}`;
  }, [props.name]);
  return <h1>Hello, {props.name}</h1>;
}

上面例子中,useEffect()的第二个参数是一个数组,指定了第一个参数(副效应函数)的依赖项(props.name)。只有该变量发生变化时,副效应函数才会执行。

如果第二个参数是一个空数组,就表明副效应参数没有任何依赖项。因此,副效应函数这时只会在组件加载进入 DOM 后执行一次,后面组件重新渲染,就不会再次执行。这很合理,由于副效应不依赖任何变量,所以那些变量无论怎么变,副效应函数的执行结果都不会改变,所以运行一次就够了。

七、useEffect() 的用途

只要是副效应,都可以使用useEffect()引入。它的常见用途有下面几种。

  • 获取数据(data fetching)
  • 事件监听或订阅(setting up a subscription)
  • 改变 DOM(changing the DOM)
  • 输出日志(logging)

下面是从远程服务器获取数据的例子。(查看运行结果


import React, { useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ hits: [] });

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        'https://hn.algolia.com/api/v1/search?query=redux',
      );

      setData(result.data);
    };

    fetchData();
  }, []);

  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}

export default App;

上面例子中,useState()用来生成一个状态变量(data),保存获取的数据;useEffect()的副效应函数内部有一个 async 函数,用来从服务器异步获取数据。拿到数据以后,再用setData()触发组件的重新渲染。

由于获取数据只需要执行一次,所以上例的useEffect()的第二个参数为一个空数组。

八、useEffect() 的返回值

副效应是随着组件加载而发生的,那么组件卸载时,可能需要清理这些副效应。

useEffect()允许返回一个函数,在组件卸载时,执行该函数,清理副效应。如果不需要清理副效应,useEffect()就不用返回任何值。


useEffect(() => {
  const subscription = props.source.subscribe();
  return () => {
    subscription.unsubscribe();
  };
}, [props.source]);

上面例子中,useEffect()在组件加载时订阅了一个事件,并且返回一个清理函数,在组件卸载时取消订阅。

实际使用中,由于副效应函数默认是每次渲染都会执行,所以清理函数不仅会在组件卸载时执行一次,每次副效应函数重新执行之前,也会执行一次,用来清理上一次渲染的副效应。

九、useEffect() 的注意点

使用useEffect()时,有一点需要注意。如果有多个副效应,应该调用多个useEffect(),而不应该合并写在一起。


function App() {
  const [varA, setVarA] = useState(0);
  const [varB, setVarB] = useState(0);
  useEffect(() => {
    const timeoutA = setTimeout(() => setVarA(varA + 1), 1000);
    const timeoutB = setTimeout(() => setVarB(varB + 2), 2000);

    return () => {
      clearTimeout(timeoutA);
      clearTimeout(timeoutB);
    };
  }, [varA, varB]);

  return <span>{varA}, {varB}</span>;
}

上面的例子是错误的写法,副效应函数里面有两个定时器,它们之间并没有关系,其实是两个不相关的副效应,不应该写在一起。正确的写法是将它们分开写成两个useEffect()


function App() {
  const [varA, setVarA] = useState(0);
  const [varB, setVarB] = useState(0);

  useEffect(() => {
    const timeout = setTimeout(() => setVarA(varA + 1), 1000);
    return () => clearTimeout(timeout);
  }, [varA]);

  useEffect(() => {
    const timeout = setTimeout(() => setVarB(varB + 2), 2000);

    return () => clearTimeout(timeout);
  }, [varB]);

  return <span>{varA}, {varB}</span>;
}

十、参考链接

(正文完)

React 系统视频

对于每个想进大厂的前端开发者来说,React 是绕不过的坎,面试肯定会问到,业务也很可能会用。不懂一点 React 技术栈,大大降低了个人竞争力。

退一步说,即使你用不到 React,但是它的很多思想已经影响到了整个业界,比如虚拟 DOM、JSX、函数式编程、immutable 的状态、单向数据流等等。懂了 React,面对其他轮子时,你也能得心应手。

但是,大家都知道 React 学习曲线比较陡峭,不少人抱怨:苦苦学了1个多月却进展缓慢怎么办?

别着急,这里有一份开课吧的 《React 原理剖析 + 组件化》 系统视频。不仅讲解了原理,还包括了综合性的实战项目,里面用到了 react-router、redux、react-redux、antd 等 React 全家桶。

访问这个链接,或者微信扫描下面的二维码,就可以免费领取。

(完)

留言(38条)

受教了, 感谢分享.

阮老师,文中提到清理函数在组件每次重新渲染之前执行,准确的来说应该是每次渲染之后清除上一次的effect。
Dan的博文中是这么说的:

React only runs the effects after letting the browser paint. This makes your app faster as most effects don’t need to block screen updates. Effect cleanup is also delayed. The previous effect is cleaned up after the re-render with new props

学习了,感谢阮老师的分享

@jq:

谢谢指出,我改了一下。确切说,是在每次副效应函数重新运行之前,会清理上一次的副效应。

用好hook的前提是深入理解函数式编程的思想,感觉这块自己还有很多不理解的地方

真及时啊,刚在学这个hook,就出来了!

面向过程--面向对象--函数式,感觉又回到了面向过程式编程的时候

感谢阮老师分享,这篇感觉写的也不错:https://overreacted.io/a-complete-guide-to-useeffect/

最后一点不明白,有人可以解释一下吗?

"
但是,钩子的灵活性太大,初学者不太容易理解。很多人一知半解,很容易写出混乱不堪、无法维护的代码。那就不如使用类了。因为类有很多强制的语法约束,不容易搞乱。
"

太赞同了,上个星期接手了一个同事的代码,兼职就是一座用hooks堆叠出来的屎山,维护的可能性为0,我真差点找领导投诉他了,但是觉得他为人还是不错,忍下了,自己用class重写了他的代码。

今年刚开始在工作中使用react,直接使用的hooks

看了您的这篇教学后,我感觉我之前写了一坨屎

写的真好

useContext()的例子有没有啊

useContext()的例子有没有啊

我感觉hook挺好用啊 网络请求拿过来的数据要渲染不是用hook 还有用户交互不用hook不好太整啊 hook屎山是什么

你们所说的维护性是什么意思 能解释下么 我刚入行react三个月 用hook用的太香了 都有点后怕了 什么样的HOOK没有可维护性

谢谢你,经常阅读你的文章,对我帮助很大,谢谢你愿意分享出自己的知识点,还辛苦写出来供我们这群小笨蛋阅读,生活很残酷感谢有你 ----安逸尘

受教了,谢谢老师。

学习了,原来react还可以这么写。

用了hooks来写还是香的,再也不用class了

6啊,阮老师果然很厉害,hook我感觉我会类了。。

函数式组件和类组件的对比图不太恰当吧,类组件用的是 state 作为数据源,而函数式组件用的是 props,代码行数自然有差异,类组件多了初始化 state 的步骤,并且在 mount 的时候又更新了一下。都用 props 的话,差异不大吧。函数式组件那边改用 useState 可能会更有说服力一些。

引用mdp的发言:

最后一点不明白,有人可以解释一下吗?

是第九点吗?我的理解是两个不相关的副效应,他们的依赖也不同,如果写在一起,只要其中一个的依赖发生变化,另一个依赖即使没变,它也被动的被更新。所以分开写,不影响彼此是最好的。

引用mdp的发言:

最后一点不明白,有人可以解释一下吗?

我的理解是useEffect的第二个数据数组中任意一个参数变化后该钩子就会执行,所以mounded后1s该钩子执行,然后中途该钩子又改变了数组中A的值,所以1s该钩子再次执行,而数据b要每隔2s更新,根本就没有足够时间,所以要拆分他们写成两个独立的useEffect钩子才可以正常执行

很清楚!谢谢老师

比如一个页面拆分了5个组件,其他4个组件的某个值变化都会导致第5个组件的响应,那useEffect也不能合并写吗,如果不合并写会导致很多问题,合并写了也会导致很多问题,感觉很难控制。请问针对于这种情况要怎么处理。第一次进来是需要所有值都有,第二次切换的时候会导致一个值变了,其他两个值还是旧的,因为监听了好几个,所以会重复执行好几次,比如下面这种。但是单独切换某个组件一个值又需要去响应变化。
useEffect(() => {
if (viewer) {
if (tifData.layerName && clipData && opacityData) {
loadServer(
tifData.name, tifData.time, viewer, tifData.url, tifData.layerName,
clipData ? clipData.shpName : null,
opacityData
);
}
}
}, [tifData, clipData, opacityData, viewer, loadServer]);

阮老师可不可以说下useMemo和useCallback的区别

太爱了,阮老师的文章真的每次都能把特别复杂的东西讲的特别简单

希望老师多写一些这类文章,我在其他文章里学习useEffect(),学得一头雾水,但是看了老师的文章,思路瞬间清晰了

谢谢老师的分享,看了之后对钩子的使用方面感觉清晰了很多。

里面的观点很认同,学习React不仅仅是学习那些基础的知识,更是学习其设计思想。

引用coco的发言:

阮老师可不可以说下useMemo和useCallback的区别

useMemo 返回的是函数的执行结果,是一个值
useCallback 返回的是函数的缓存,是一个函数
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

初学者还是得先学好类式组件先呐

非常感谢老师的分享,对于从class 转入hook来说写业务已经够用了,非常感谢

感谢老师分享。关于网络请求是一个副效应而在useEffect中去发起请求;这一点,是否能够在组件初始化完成之前就去发起请求,不应该是早请求早响应吗?

优质好文

对于一个初学者来说真的清晰易懂,非常感觉阮老师

我要发表看法

«-必填

«-必填,不公开

«-我信任你,不会填写广告链接