輕鬆學會 React 鉤子:以 useEffect-- 爲例

作者: 阮一峯

五年多前,我寫過 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 爲許多常見的操作(副效應),都提供了專用的鉤子。

上面這些鉤子,都是引入某種特定的副效應,而 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()引入。它的常見用途有下面幾種。

下面是從遠程服務器獲取數據的例子。(查看運行結果

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 全家桶。

訪問這個鏈接,或者微信掃描下面的二維碼,就可以免費領取。

(完)

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://www.ruanyifeng.com/blog/2020/09/react-hooks-useeffect-tutorial.html