用React刻自己的投资Dashboard Day9 - useEffect hook

tags: 2021铁人赛 React

既上一篇介绍完useState hook後,本篇就来介绍Day6也有用到的useEffect hook,在React官网有提到,如果使用者熟悉React class component的生命周期,那麽可以用下面这个提示来理解useEffect:

如果你熟悉 React class 的生命周期方法,你可以把 useEffect 视为 componentDidMount,componentDidUpdate 和 componentWillUnmount 的组合。

从下面这张React官方提供图可以看出React生命周期的三个阶段,Mounting(创建)、Updating(更新)、Unmounting(销毁),不过要完整地解释这张图会需要比较多的篇幅,这边只要先了解执行顺序为先是componentDidMount,再来是componentDidUpdate,最後是componentWillUnmount,再来就直接看官网的范例即可。

Class 与 useEffect用法差异

要知道用法上的差异,可以从React官网上的范例来理解,这个范例是用React去写一个计数器,并且在React对DOM进行变更後立即更新网页标题,下面就分别用class与hook去写,藉由范例认识其差异。

先将相同功能的程序码比较一下,使用hook的程序码比较短一些,也发现class版本需要用到componentDidMount与componentDidUpdate去实现这样的功能,而hook版本只需要一个useEffect就可以处理,当然并不是程序码短就是好,因此接下来分析一下功能差别。

class版本需要用到两个method的原因

因为我们想要在一进入网页的时候,就让网页的title显示"You clicked 0 times",因此需要在Mounting的时候呼叫一次,之後在点击按钮的时候,count这个state会被更新,为了要将网页的title也更新为"You clicked n times",因此需要呼叫componentDidUpdate。

可能有人会说,难道没有一个method可以让React在mounting及updating都去执行这行程序码吗?可惜React的class component刚好就是没有这个功能,因此就需要写两次。

将上图中的class版本程序码放大如下:

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }
  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

为什麽useEffect只要一行就可以

相对於class版本的程序码,hook版本只写了一行useEffect就达成这个范例的要求,原因是useEffect会在每次重新render之後都执行一次,不管这个render是在mounting阶段或updating阶段所执行的,useEffect都会执行,因此当state变动,自然会重新render,也就启动useEffect内的程序,网页的title也就更新了,真的是非常方便。

然後还有一个小地方是,useEffect需要在component内呼叫,才能去抓到最新的state,这个是javascript closures的特点,而React巧妙地运用了这点。

将上图中的hook版本程序码放大如下:

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

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

回到Day6的Card.js来看看

在Card.js中,因为我希望在以下时间点去触发抓取数据的程序:

  1. 使用者进入网页时,需先载入视窗范围内可看见的图 (Mounting Card Component)
  2. 使用者scroll down时,动态载入新的图 (Mounting Card Component)
  3. 使用者按下更新按钮,需重新抓取数据更新图表 (Updating Card Component)

从上述需求可以发现,抓取数据这个动作,会在Mounting及Updating阶段执行,因此useEffect就符合这样的需求。所以在Card.js内写了一个fetchData函数,并在useEffect内呼叫它,即完成这个功能。

Card.js

const Card = (props) => {
  // 定义state
  const [chartOption, setChartOption] = useState({...});
  
  // 抓取API数据
  const fetchData = (series_id) => {...};
  
  // effect
  useEffect(() => {
    fetchData(props.item.series_id);
  }, []);
  
  return (
    ...
  )
}

无限回圈的陷阱

在上面的程序码可以看到useEffect内有一个空的阵列,它的功能是避免程序陷入无限回圈中,原因如下图,在mount时执行render,完成之後effect就会启动,因此就变更了state,React发现state改变後又重新render,再来又触发effect,因此就形成了一个无限回圈。

这个无限回圈的问题,可以透过useEffect的dependencies来解决,例如官网的范例如下:

如果count没有改变,effect就不会执行。

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes

因此我在Card的useEffect放入一个空阵列当作dependencies,因为它一直都是空的,所以react只会执行一次effect,虽然这不是最正确的方式,不过算是暂时解决这个问题。

小结

useEffect真的是有点复杂的功能,不过还蛮好用的,在这篇提到的无限回圈问题还没结束,下一篇要来研究到底useEffect中的dependencies到底要放一些什麽,同时也会提到useCallback这个hook。


<<:  Day 09 - Type Signature

>>:  Day16 Grafana (Match Making)

用 Google App Script 实现发送认证码的 API

昨天用 Vite 快速打造了输入信箱获取认证码的页面,但必须搭配发送认证码的 API 才能继续完成这...

OpenCV癌细胞医学图片的特徵如何做侦测圈选分类

不知道现在台湾的AI有没有已经做到癌细胞医学图片的特徵侦测圈选跟分类 目前看到云象科技与林口长庚医院...

找LeetCode上简单的题目来撑过30天啦(DAY21)

不知道要打什麽,直接开始 题号:739 标题:Daily Temperatures 难度:Mediu...

Day09 | Dart 非同步 - Future

昨天介绍了在Dart中非同步的基本概念,今天就要来讲到如何简单的控制非同步操作。 Future Fu...