輕鬆學會 React 鉤子:以 useEffect() 為例

阮一峰發表於2020-09-15

我本來不想碰它們了,覺得框架一直在升級,教程寫出來就會過時。

但是,最近我逐漸體會到 React 鉤子(hooks)非常好用,重新認識了 React 這個框架,覺得應該補上關於鉤子的部分。

下面就來談談,怎樣正確理解鉤子,並且深入剖析最重要的鉤子之一的useEffect()。內容會盡量通俗,讓不熟悉 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 useEffect: 4 Tips Every Developer Should Know, Helder Esteves
  • Using the Effect Hook, React
  • How to fetch data with React Hooks?, Robin Wieruch

(正文完)

React 系統影片

對於每個想進大廠的前端開發者來說,React 是繞不過的坎,面試肯定會問到,業務也很可能會用。不懂一點 React 技術棧,大大降低了個人競爭力。

退一步說,即使你用不到 React,但是它的很多思想已經影響到了整個業界,比如虛擬 DOM、JSX、函數語言程式設計、immutable 的狀態、單向資料流等等。懂了 React,面對其他輪子時,你也能得心應手。

但是,大家都知道 React 學習曲線比較陡峭,不少人抱怨:苦苦學了1個多月卻進展緩慢怎麼辦?

彆著急,這裡有一份開課吧的 《React 原理剖析 + 元件化》 系統影片。不僅講解了原理,還包括了綜合性的實戰專案,裡面用到了 react-router、redux、react-redux、antd 等 React 全家桶。

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

(完)

相關文章