慣性聚合 高效追蹤和閱讀你感興趣的部落格、新聞、科技資訊
閱讀原文 在慣性聚合中打開

推薦訂閱源

博客园 - 司徒正美
V
V2EX
T
Tailwind CSS Blog
有赞技术团队
有赞技术团队
aimingoo的专栏
aimingoo的专栏
Apple Machine Learning Research
Apple Machine Learning Research
IT之家
IT之家
Blog — PlanetScale
Blog — PlanetScale
A
About on SuperTechFans
月光博客
月光博客
T
The Blog of Author Tim Ferriss
宝玉的分享
宝玉的分享
Martin Fowler
Martin Fowler
博客园 - 聂微东
The GitHub Blog
The GitHub Blog
V
Visual Studio Blog
WordPress大学
WordPress大学
酷 壳 – CoolShell
酷 壳 – CoolShell
Engineering at Meta
Engineering at Meta
GbyAI
GbyAI

阮一峰的网络日志

科技爱好者周刊(第 396 期):互联网通信的替代方案 科技爱好者周刊(第 396 期):互联网通信的替代方案 - 阮一峰的网络日志 科技爱好者周刊(第 395 期):软件开发的第三种方式 科技爱好者周刊(第 395 期):软件开发的第三种方式 - 阮一峰的网络日志 科技爱好者周刊(第 393 期):脑腐状态 科技爱好者周刊(第 392 期):axios 投毒与好莱坞式骗术 科技爱好者周刊(第 391 期):AI 的贫富分化 科技爱好者周刊(第 390 期):没有语料,大模型就是智障 套壳中国大模型撑起500亿美元估值?扒一扒 Cursor 的"套壳"疑云 科技爱好者周刊(第 389 期):未来如何招聘程序员 科技爱好者周刊(第 388 期):测试是新的护城河 零安装的"云养虾":ArkClaw 使用指南 科技爱好者周刊(第 387 期):你是领先的 科技爱好者周刊(第 386 期):当外卖员接入 AI 字节全家桶 Seed 2.0 + TRAE 玩转 Skill 科技爱好者周刊(第 385 期):马斯克害怕中国车企吗? 智谱旗舰 GLM-5 实测:对比 Opus 4.6 和 GPT-5.3-Codex 科技爱好者周刊(第 384 期):为什么软件股下跌 科技爱好者周刊(第 383 期):你是第几级 AI 编程 Kimi 的一体化,Manus 的分层 科技爱好者周刊(第 382 期):独立软件的黄昏 AI native Workspace 也许是智能体的下一阶段 科技爱好者周刊(第 381 期):中国 AI 大模型领导者在想什么 科技爱好者周刊(第 380 期):为什么人们拥抱"不对称收益" 科技爱好者周刊(第 379 期):《硅谷钢铁侠》摘录 我如何用 AI 处理历史遗留代码:MiniMax M2.1 升级体验 科技爱好者周刊(第 378 期):预测是新的互联网热点 科技爱好者周刊(第 377 期):14万美元的贫困线 科技爱好者周刊(第 376 期):太空数据中心的争议 科技爱好者周刊(第 375 期):一扇门的 Bug 终于有人做了 Subagent,TRAE 国内版 SOLO 模式来了 科技爱好者周刊(第 374 期):6GHz 的问题 VS Code 使用国产大模型 MiniMax M2 教程 科技爱好者周刊(第 373 期):数据模型是新产品的核心 国产大模型接入 Claude Code 教程:以 Doubao-Seed-Code 为例 科技爱好者周刊(第 372 期):软件界面如何设计 大模型比拼:MiniMax M2 vs GLM 4.6 vs Claude Sonnet 4.5 科技爱好者周刊(第 371 期):一个乐观主义者的专访 科技爱好者周刊(第 370 期):正确的代码高亮 错误处理:异常好于状态码 科技爱好者周刊(第 369 期):Tim 与罗永浩的对谈 科技爱好者周刊(第 368 期):不要这样管理软件团队 一天之内,智谱和 Anthropic 都发了最强编程模型 科技爱好者周刊(第 367 期):Nano Banana 的几个妙用 科技爱好者周刊(第 366 期):旧金山疯狂的 AI 广告 科技爱好者周刊(第 365 期):流量变现正在崩塌 科技爱好者周刊(第 364 期):最难还原的魔方 科技爱好者周刊(第 363 期):最好懂的神经网络解释 科技爱好者周刊(第 362 期):GitHub 工程师谈系统设计 科技爱好者周刊(第 361 期):暗网 Tor 安全吗?
輕鬆學會 React 鉤子:以 useEffect() 為例
阮一峰 · 2020-09-15 · via 阮一峰的网络日志

五年多前,我寫過 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 全家桶。

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

(完)