用動畫和實戰打開 React Hooks(一):useState 和 useEffect

咱們研發開源了一款基於 Git 進行技術實戰教程寫做的工具,咱們圖雀社區的全部教程都是用這款工具寫做而成,歡迎 Starjavascript

若是你想快速瞭解如何使用,歡迎閱讀咱們的 教程文檔css

本文由圖雀社區成員 mRc 寫做而成,歡迎加入圖雀社區,一塊兒創做精彩的免費技術教程,予力編程行業發展。html

若是您以爲咱們寫得還不錯,記得 點贊 + 關注 + 評論 三連,鼓勵咱們寫出更好的教程💪前端

自從 React 16.8 發佈以後,它帶來的 React Hooks 在前端圈引發了一場沒法逆轉的風暴。React Hooks 爲函數式組件提供了無限的功能,解決了類組件不少的固有缺陷。這篇教程將帶你快速熟悉並掌握最經常使用的兩個 Hook:useStateuseEffect。在瞭解如何使用的同時,還能管窺背後的原理,順便實現一個 COVID-19(新冠肺炎)可視化應用。java

歡迎訪問本項目的 GitHub 倉庫Gitee 倉庫react

起步

前提條件

在閱讀這篇教程以前,但願你已經作了以下準備:git

爲何會有 Hooks?

在 Hooks 出現以前,類組件和函數組件的分工通常是這樣的:github

  • 類組件提供了完整的狀態管理和生命週期控制,一般用來承接複雜的業務邏輯,被稱爲「聰明組件
  • 函數組件則是純粹的從數據到視圖的映射,對狀態毫無感知,所以一般被稱爲「傻瓜組件

有些團隊還制定了這樣的 React 組件開發約定:npm

有狀態的組件沒有渲染,有渲染的組件沒有狀態。編程

那麼 Hooks 的出現又是爲了解決什麼問題呢?咱們能夠試圖總結一下類組件頗具表明性的痛點

  1. 使人頭疼的 this 管理,容易引入難以追蹤的 Bug
  2. 生命週期的劃分並不符合「內聚性」原則,例如 setIntervalclearInterval 這種具備強關聯的邏輯被拆分在不一樣的生命週期方法中
  3. 組件複用(數據共享或功能複用)的困局,從早期的 Mixin,到高階組件(HOC),再到 Render Props,始終沒有一個清晰直觀又便於維護的組件複用方案

沒錯,隨着 Hooks 的推出,這些痛點都成爲了歷史!

爲何要寫這一系列 Hooks 教程?

如何快速學習並掌握 React Hooks 一直是困擾不少新手或者老玩家的一個問題,而筆者在平常的學習和開發中也發現瞭如下頭疼之處:

  • React 官方文檔對 Hooks 的講解偏應用,對原理的闡述一筆帶過
  • 講 React Hooks 的優秀文章不少,但大多專一於講解一兩個 Hook,要想一網打盡有難度
  • 看了不少使用方法甚至源碼分析,可是無法和具體的使用場景對應起來,不瞭解怎麼在實際開發中靈活運用

若是你也有一樣的困惑,但願這一系列文章能幫助你撥開雲霧,讓 Hooks 成爲你的稱手兵器。咱們將經過一個完整的 COVID-19 數據可視化項目,結合 Hooks 的動畫原理講解,讓你真正地精通 React Hooks!

說實話,Hooks 的知識點至關分散,就像遊樂園的遊玩項目同樣,選擇一條完美的路線很難。可是無論怎麼樣,但願在接下來的旅程中,你能玩得開心😊!

初始化項目

首先,經過 Create React App(如下簡稱 CRA) 初始化項目:

npx create-react-app covid-19-with-hooks
複製代碼

在少量等待以後,進入項目。

提示

咱們全部的數據源自 NovelCOVID 19 API,能夠點擊訪問其所有的 API 文檔。

一切就緒,讓咱們出發吧!

useState + useEffect:初來乍到

首先,讓咱們從最最最經常使用的兩個 Hooks 提及:useStateuseEffect 。頗有可能,你在平時的學習和開發中已經接觸並使用過了(固然若是你剛開始學也不要緊啦)。不過在此以前,咱們先熟悉一下 React 函數式組件的運行過程。

理解函數式組件的運行過程

咱們知道,Hooks 只能用於 React 函數式組件。所以理解函數式組件的運行過程對掌握 Hooks 中許多重要的特性很關鍵,請看下圖:

能夠看到,函數式組件嚴格遵循 UI = render(data) 的模式。當咱們第一次調用組件函數時,觸發初次渲染;而後隨着 props 的改變,便會從新調用該組件函數,觸發重渲染

你也許會納悶,動畫裏面爲啥要並排畫三個同樣的組件呢?由於我想經過這種方式直觀地闡述函數式組件的一個重要思想:

每一次渲染都是徹底獨立的。

後面咱們將沿用這樣的風格,並一步步地介紹 Hook 在函數式組件中扮演怎樣的角色。

useState 使用淺析

首先咱們來簡單地瞭解一下 useState 鉤子的使用,官方文檔介紹的使用方法以下:

const [state, setState] = useState(initialValue);
複製代碼

其中 state 就是一個狀態變量,setState 是一個用於修改狀態的 Setter 函數,而 initialValue 則是狀態的初始值。

光看代碼可能有點抽象,請看下面的動畫:

與以前的純函數式組件相比,咱們引入了 useState 這個鉤子,瞬間就打破了以前 UI = render(data) 的安靜畫面——函數組件竟然能夠從組件以外把狀態和修改狀態的函數「鉤」過來!而且仔細看上面的動畫,經過調用 Setter 函數,竟然還能夠直接觸發組件的重渲染

提示

你也許注意到了全部的「鉤子」都指向了一個綠色的問號,咱們會在下面詳細地分析那是什麼,如今就暫時把它看做是組件以外能夠訪問的一個「神祕領域」。

結合上面的動畫,咱們能夠得出一個重要的推論:每次渲染具備獨立的狀態值(畢竟每次渲染都是徹底獨立的嘛)。也就是說,每一個函數中的 state 變量只是一個簡單的常量,每次渲染時從鉤子中獲取到的常量,並無附着數據綁定之類的神奇魔法。

這也就是老生常談的 Capture Value 特性。能夠看下面這段經典的計數器代碼(來自 Dan 的這篇精彩的文章):

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

  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + count);
    }, 3000);
  }

  return (
    <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> <button onClick={handleAlertClick}> Show alert </button> </div>
  );
}
複製代碼

實現了上面這個計數器後(也能夠直接經過這個 Sandbox 進行體驗),按以下步驟操做:1)點擊 Click me 按鈕,把數字增長到 3;2)點擊 Show alert 按鈕;3)在 setTimeout 觸發以前點擊 Click me,把數字增長到 5。

結果是 Alert 顯示 3!

若是你以爲這個結果很正常,恭喜你已經理解了 Capture Value 的思想!若是你以爲匪夷所思嘛……來簡單解釋一下:

  • 每次渲染相互獨立,所以每次渲染時組件中的狀態、事件處理函數等等都是獨立的,或者說只屬於所在的那一次渲染
  • 咱們在 count 爲 3 的時候觸發了 handleAlertClick 函數,這個函數所記住的 count 也爲 3
  • 三秒種後,剛纔函數的 setTimeout 結束,輸出當時記住的結果:3

這道理就像,你翻開十年前的日記本,雖然是如今翻開的,但記錄的仍然是十年前的時光。或者說,日記本 Capture 了那一段美好的回憶。

useEffect 使用淺析

你可能已經據說 useEffect 相似類組件中的生命週期方法。可是在開始學習 useEffect 以前,建議你暫時忘記生命週期模型,畢竟函數組件和類組件是不一樣的世界。官方文檔介紹 useEffect 的使用方法以下:

useEffect(effectFn, deps)
複製代碼

effectFn 是一個執行某些可能具備反作用的 Effect 函數(例如數據獲取、設置/銷燬定時器等),它能夠返回一個清理函數(Cleanup),例如你們所熟悉的 setIntervalclearInterval

useEffect(() => {
  const intervalId = setInterval(doSomething(), 1000);
  return () => clearInterval(intervalId);
});
複製代碼

能夠看到,咱們在 Effect 函數體內經過 setInterval 啓動了一個定時器,隨後又返回了一個 Cleanup 函數,用於銷燬剛剛建立的定時器。

OK,聽上去仍是很抽象,再來看看下面的動畫吧:

動畫中有如下須要注意的點:

  • 每一個 Effect 必然在渲染以後執行,所以不會阻塞渲染,提升了性能
  • 在運行每一個 Effect 以前,運行前一次渲染的 Effect Cleanup 函數(若是有的話)
  • 當組件銷燬時,運行最後一次 Effect 的 Cleanup 函數

提示

將 Effect 推遲到渲染完成以後執行是出於性能的考慮,若是你想在渲染以前執行某些邏輯(不惜犧牲渲染性能),那麼可以使用 useLayoutEffect 鉤子,使用方法與 useEffect 徹底一致,只是執行的時機不一樣。

再來看看 useEffect 的第二個參數:deps (依賴數組)。從上面的演示動畫中能夠看出,React 會在每次渲染後都運行 Effect。而依賴數組就是用來控制是否應該觸發 Effect,從而可以減小沒必要要的計算,從而優化了性能。具體而言,只要依賴數組中的每一項與上一次渲染相比都沒有改變,那麼就跳過本次 Effect 的執行。

仔細一想,咱們發現 useEffect 鉤子與以前類組件的生命週期相比,有兩個顯著的特色:

  • 將初次渲染(componentDidMount)、重渲染(componentDidUpdate)和銷燬(componentDidUnmount)三個階段的邏輯用一個統一的 API 去解決
  • 把相關的邏輯都放到一個 Effect 裏面(例如 setIntervalclearInterval),更突出邏輯的內聚性

在最極端的狀況下,咱們能夠指定 deps 爲空數組 [] ,這樣能夠確保 Effect 只會在組件初次渲染後執行。實際效果動畫以下:

能夠看到,後面的全部重渲染都不會觸發 Effect 的執行;在組件銷燬時,運行 Effect Cleanup 函數。

注意

若是你熟悉 React 的重渲染機制,那麼應該能夠猜到 deps 數組在判斷元素是否發生改變時一樣也使用了 Object.is 進行比較。所以一個隱患即是,當 deps 中某一元素爲非原始類型時(例如函數、對象等),每次渲染都會發生改變,從而失去了 deps 自己的意義(條件式地觸發 Effect)。咱們會在接下來說解如何規避這個困境。

實戰環節

OK,到了實戰環節,咱們先實現獲取全球數據概況(每 5 秒從新獲取一次)。建立 src/components/GlobalStats.js 組件,用於展現全球數據概況,代碼以下:

import React from "react";

function Stat({ number, color }) {
  return <span style={{ color: color, fontWeight: "bold" }}>{number}</span>;
}

function GlobalStats({ stats }) {
  const { cases, deaths, recovered, active, updated } = stats;

  return (
    <div className='global-stats'>
      <small>Updated on {new Date(updated).toLocaleString()}</small>
      <table>
        <tr>
          <td>
            Cases: <Stat number={cases} color='red' />
          </td>
          <td>
            Deaths: <Stat number={deaths} color='gray' />
          </td>
          <td>
            Recovered: <Stat number={recovered} color='green' />
          </td>
          <td>
            Active: <Stat number={active} color='orange' />
          </td>
        </tr>
      </table>
    </div>
  );
}

export default GlobalStats;
複製代碼

能夠看到,GlobalStats 就是一個簡單的函數式組件,沒有任何鉤子。

而後修改 src/App.js ,引入剛剛建立的 GlobalStats 組件,代碼以下:

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

import "./App.css";
import GlobalStats from "./components/GlobalStats";

const BASE_URL = "https://corona.lmao.ninja";

function App() {
  const [globalStats, setGlobalStats] = useState({});

  useEffect(() => {
    const fetchGlobalStats = async () => {
      const response = await fetch(`${BASE_URL}/all`);
      const data = await response.json();
      setGlobalStats(data);
    };

    fetchGlobalStats();
    const intervalId = setInterval(fetchGlobalStats, 5000);

    return () => clearInterval(intervalId);
  }, []);

  return (
    <div className='App'> <h1>COVID-19</h1> <GlobalStats stats={globalStats} /> </div> ); } export default App; 複製代碼

能夠看到,咱們在 App 組件中,首先經過 useState 鉤子引入了 globalStats 狀態變量,以及修改該狀態的函數。而後經過 useEffect 鉤子獲取 API 數據,其中有如下須要注意的點:

  1. 咱們經過定義了一個 fetchGlobalStats 異步函數並進行調用從而獲取數據,而不是直接把這個 async 函數做爲 useEffect 的第一個參數;
  2. 建立了一個 Interval,用於每 5 秒鐘從新獲取一次數據;
  3. 返回一個函數,用於銷燬以前建立的 Interval。

此外,第二個參數(依賴數組)爲空數組,所以整個 Effect 函數只會運行一次。

注意

有時候,你也許會不經意間把 Effect 寫成一個 async 函數:

useEffect(async () => {
  const response = await fetch('...');
  // ...
}, []);
複製代碼

這樣能夠嗎?強烈建議你不要這樣作useEffect 約定 Effect 函數要麼沒有返回值,要麼返回一個 Cleanup 函數。而這裏 async 函數會隱式地返回一個 Promise,直接違反了這一約定,會形成不可預測的結果。

最後附上應用的全局 CSS 文件,代碼以下(直接複製粘貼便可):

.App {
  width: 1200px;
  margin: auto;
  text-align: center;
}

.history-group {
  display: flex;
  justify-content: center;
  width: 1200px;
  margin: auto;
}

table,
th,
td {
  border: 1px solid #ccc;
  border-collapse: collapse;
}

th,
td {
  padding: 5px;
  text-align: left;
}

.global-stats > table {
  margin: auto;
  margin-top: 0.5rem;
  margin-bottom: 1rem;
}
複製代碼

經過 npm start 開啓項目:

此外,你能夠檢查一下控制檯的 Network 選項卡,應該能看到咱們的應用每五秒就會發起一次請求查詢最新的數據。恭喜你,成功地用 Hooks 進行了一次數據獲取!

useState + useEffect:漸入佳境

在上一步驟中,咱們在 App 組件中定義了一個 State 和 Effect,可是實際應用不可能這麼簡單,通常都須要多個 State 和 Effect,這時候又該怎麼去理解和使用呢?

深刻 useState 的本質

在上一節的動畫中,咱們看到每一次渲染組件時,咱們都能經過一個神奇的鉤子把狀態」鉤「過來,不過這些鉤子從何而來咱們打了一個問號。如今,是時候解開謎團了。

注意

如下動畫演示並不徹底對應 React Hooks 的源碼實現,可是它能很好地幫助你理解其工做原理。固然,也能幫助你去啃真正的源碼。

咱們先來看看當組件初次渲染(掛載)時,狀況究竟是什麼樣的:

注意如下要點:

  1. 在初次渲染時,咱們經過 useState 定義了多個狀態;
  2. 每調用一次 useState ,都會在組件以外生成一條 Hook 記錄,同時包括狀態值(用 useState 給定的初始值初始化)和修改狀態的 Setter 函數;
  3. 屢次調用 useState 生成的 Hook 記錄造成了一條鏈表
  4. 觸發 onClick 回調函數,調用 setS2 函數修改 s2 的狀態,不只修改了 Hook 記錄中的狀態值,還即將觸發重渲染

OK,重渲染的時候到了,動畫以下:

能夠看到,在初次渲染結束以後、重渲染以前,Hook 記錄鏈表依然存在。當咱們逐個調用 useState 的時候,useState 便返回了 Hook 鏈表中存儲的狀態,以及修改狀態的 Setter。

提示

當你充分理解上面兩個動畫以後,其實就能理解爲何這個 Hook 叫 useState 而不是 createState 了——之因此叫 use ,是由於沒有的時候才建立(初次渲染的時候),有的時候就直接讀取(重渲染的時候)。

經過以上的分析,咱們不難發現 useState 在設計方面的精巧(摘自張立理:對 React Hooks 的一些思考):

  • 狀態和修改狀態的 Setter 函數兩兩配對,而且後者必定影響前者,前者只被後者影響,做爲一個總體它們徹底不受外界的影響
  • 鼓勵細粒度和扁平化的狀態定義和控制,對於代碼行爲的可預測性和可測試性大有幫助
  • 除了 useState (和其餘鉤子),函數組件依然是實現渲染邏輯的「純」組件,對狀態的管理被 Hooks 所封裝了起來

深刻 useEffect 的本質

在對 useState 進行一波深挖以後,咱們再來揭開 useEffect 神祕的面紗。實際上,你可能已經猜到了——一樣是經過一個鏈表記錄全部的 Hook,請看下面的演示:

注意其中一些細節:

  1. useStateuseEffect 在每次調用時都被添加到 Hook 鏈表中;
  2. useEffect 還會額外地在一個隊列中添加一個等待執行的 Effect 函數;
  3. 在渲染完成後,依次調用 Effect 隊列中的每個 Effect 函數。

至此,上一節的動畫中那兩個「問號」的身世也就揭曉了——只不過是鏈表罷了!回過頭來,咱們想起來 React 官方文檔 Rules of Hooks 中強調過一點:

Only call hooks at the top level. 只在最頂層使用 Hook。

具體地說,不要在循環、嵌套、條件語句中使用 Hook——由於這些動態的語句頗有可能會致使每次執行組件函數時調用 Hook 的順序不能徹底一致,致使 Hook 鏈表記錄的數據失效。具體的場景就不畫動畫啦,自行腦補吧~

不要撒謊:關於 deps 的那些事

useEffect (包括其餘相似的 useCallbackuseMemo 等)都有個依賴數組(deps)參數,這個參數比較有趣的一點是:指定依賴的決定權徹底在你手裏。你固然能夠選擇「撒謊」,無論什麼狀況都給一個空的 deps 數組,彷彿在說「這個 Effect 函數什麼依賴都沒有,相信我」。

然而,這種有點偷懶的作法顯然會引來各類 Bug。通常來講,所使用到的 prop 或者 state 都應該被添加到 deps 數組裏面去。而且,React 官方還推出了一個專門的 ESLint 插件,能夠幫你自動修復 deps 數組(說實話,這個插件的自動修復有時候仍是挺鬧心的……)。

實戰環節

從這一步開始,咱們將使用 Recharts 做爲可視化應用的圖表庫,它提供了出色的 D3 和 React 的綁定層。經過以下命令添加 recharts 依賴:

npm install recharts
複製代碼

建立 src/components/CountriesChart.js ,用於展現多個國家的相關數據直方圖,代碼以下:

import React from "react";
import {
  BarChart,
  CartesianGrid,
  XAxis,
  YAxis,
  Tooltip,
  Legend,
  Bar,
} from "recharts";

function CountriesChart({ data, dataKey }) {
  return (
    <BarChart
      width={1200}
      height={250}
      style={{ margin: "auto" }}
      margin={{ top: 30, left: 20, right: 30 }}
      data={data}
    >
      <CartesianGrid strokeDasharray='3 3' />
      <XAxis dataKey='country' />
      <YAxis />
      <Tooltip />
      <Legend />
      <Bar dataKey={dataKey} fill='#8884d8' />
    </BarChart>
  );
}

export default CountriesChart;
複製代碼

建立 src/components/SelectDataKey.js ,用於選擇須要展現的關鍵指標,代碼以下:

import React from "react";

function SelectDataKey({ onChange }) {
  return (
    <> <label htmlFor='key-select'>Select a key for sorting: </label> <select id='key-select' onChange={onChange}> <option value='cases'>Cases</option> <option value='todayCases'>Today Cases</option> <option value='deaths'>Death</option> <option value='recovered'>Recovered</option> <option value='active'>Active</option> </select> </> ); } export default SelectDataKey; 複製代碼

SelectDataKey 用於讓用戶選擇如下關鍵指標:

  • cases :累積確診病例
  • todayCases :今日確診病例
  • deaths :累積死亡病例
  • recovered :治癒人數
  • active :現存確診人數

最後咱們在根組件 src/App.js 中引入上面建立的兩個組件,代碼以下:

// ...
import GlobalStats from "./components/GlobalStats";
import CountriesChart from "./components/CountriesChart";
import SelectDataKey from "./components/SelectDataKey";

const BASE_URL = "https://corona.lmao.ninja";

function App() {
  const [globalStats, setGlobalStats] = useState({});
  const [countries, setCountries] = useState([]);
  const [key, setKey] = useState("cases");

  useEffect(() => {
    // ...
  }, []);

  useEffect(() => {
    const fetchCountries = async () => {
      const response = await fetch(`${BASE_URL}/countries?sort=${key}`);
      const data = await response.json();
      setCountries(data.slice(0, 10));
    };

    fetchCountries();
  }, [key]);

  return (
    <div className='App'>
      <h1>COVID-19</h1>
      <GlobalStats stats={globalStats} />
      <SelectDataKey onChange={(e) => setKey(e.target.value)} />
      <CountriesChart data={countries} dataKey={key} />
    </div>
  );
}

export default App;
複製代碼

能夠看到:

  1. 咱們建立了兩個新的狀態 countries (全部國家的數據)和 key (數據排序的指標,就是上面的五個);
  2. 咱們又經過一個 useEffect 鉤子進行數據獲取,和以前獲取全球數據相似,只不過注意咱們這邊第二個參數(依賴數組)是 [key] ,也就是隻有當 key 狀態改變的時候,纔會調用 useEffect 裏面的函數。
  3. 最後使用以前建立的兩個子組件,傳入相應的數據和回調函數。

把項目跑起來,能夠看到直方圖顯示了前十個國家的數據,而且能夠修改排序的指標(好比能夠從默認的累積確診 cases 切換成死亡人數 deaths ):

看上去挺不錯的!

到這裏,本系列第一篇也就講完啦,但願你真正理解了 useStateuseEffect ——最最最經常使用的兩個 Hook。在下一篇教程中,咱們將繼續講解自定義 Hook 和 useCallback ,敬請期待。

參考資料

想要學習更多精彩的實戰技術教程?來圖雀社區逛逛吧。

相關文章
相關標籤/搜索