咱們研發開源了一款基於 Git 進行技術實戰教程寫做的工具,咱們圖雀社區的全部教程都是用這款工具寫做而成,歡迎 Star 哦javascript
若是你想快速瞭解如何使用,歡迎閱讀咱們的 教程文檔 哦css
若是您以爲咱們寫得還不錯,記得 點贊 + 關注 + 評論 三連,鼓勵咱們寫出更好的教程💪前端
自從 React 16.8 發佈以後,它帶來的 React Hooks 在前端圈引發了一場沒法逆轉的風暴。React Hooks 爲函數式組件提供了無限的功能,解決了類組件不少的固有缺陷。這篇教程將帶你快速熟悉並掌握最經常使用的兩個 Hook:useState
和 useEffect
。在瞭解如何使用的同時,還能管窺背後的原理,順便實現一個 COVID-19(新冠肺炎)可視化應用。java
在閱讀這篇教程以前,但願你已經作了以下準備:git
在 Hooks 出現以前,類組件和函數組件的分工通常是這樣的:github
有些團隊還制定了這樣的 React 組件開發約定:npm
有狀態的組件沒有渲染,有渲染的組件沒有狀態。編程
那麼 Hooks 的出現又是爲了解決什麼問題呢?咱們能夠試圖總結一下類組件頗具表明性的痛點:
this
管理,容易引入難以追蹤的 BugsetInterval
和 clearInterval
這種具備強關聯的邏輯被拆分在不一樣的生命週期方法中沒錯,隨着 Hooks 的推出,這些痛點都成爲了歷史!
如何快速學習並掌握 React Hooks 一直是困擾不少新手或者老玩家的一個問題,而筆者在平常的學習和開發中也發現瞭如下頭疼之處:
若是你也有一樣的困惑,但願這一系列文章能幫助你撥開雲霧,讓 Hooks 成爲你的稱手兵器。咱們將經過一個完整的 COVID-19 數據可視化項目,結合 Hooks 的動畫原理講解,讓你真正地精通 React Hooks!
說實話,Hooks 的知識點至關分散,就像遊樂園的遊玩項目同樣,選擇一條完美的路線很難。可是無論怎麼樣,但願在接下來的旅程中,你能玩得開心😊!
首先,經過 Create React App(如下簡稱 CRA) 初始化項目:
npx create-react-app covid-19-with-hooks
複製代碼
在少量等待以後,進入項目。
提示
咱們全部的數據源自 NovelCOVID 19 API,能夠點擊訪問其所有的 API 文檔。
一切就緒,讓咱們出發吧!
首先,讓咱們從最最最經常使用的兩個 Hooks 提及:useState
和 useEffect
。頗有可能,你在平時的學習和開發中已經接觸並使用過了(固然若是你剛開始學也不要緊啦)。不過在此以前,咱們先熟悉一下 React 函數式組件的運行過程。
咱們知道,Hooks 只能用於 React 函數式組件。所以理解函數式組件的運行過程對掌握 Hooks 中許多重要的特性很關鍵,請看下圖:
能夠看到,函數式組件嚴格遵循 UI = render(data)
的模式。當咱們第一次調用組件函數時,觸發初次渲染;而後隨着 props
的改變,便會從新調用該組件函數,觸發重渲染。
你也許會納悶,動畫裏面爲啥要並排畫三個同樣的組件呢?由於我想經過這種方式直觀地闡述函數式組件的一個重要思想:
每一次渲染都是徹底獨立的。
後面咱們將沿用這樣的風格,並一步步地介紹 Hook 在函數式組件中扮演怎樣的角色。
首先咱們來簡單地瞭解一下 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
也爲 3setTimeout
結束,輸出當時記住的結果:3這道理就像,你翻開十年前的日記本,雖然是如今翻開的,但記錄的仍然是十年前的時光。或者說,日記本 Capture 了那一段美好的回憶。
你可能已經據說 useEffect
相似類組件中的生命週期方法。可是在開始學習 useEffect
以前,建議你暫時忘記生命週期模型,畢竟函數組件和類組件是不一樣的世界。官方文檔介紹 useEffect
的使用方法以下:
useEffect(effectFn, deps)
複製代碼
effectFn
是一個執行某些可能具備反作用的 Effect 函數(例如數據獲取、設置/銷燬定時器等),它能夠返回一個清理函數(Cleanup),例如你們所熟悉的 setInterval
和 clearInterval
:
useEffect(() => { const intervalId = setInterval(doSomething(), 1000); return () => clearInterval(intervalId); }); 複製代碼
能夠看到,咱們在 Effect 函數體內經過 setInterval
啓動了一個定時器,隨後又返回了一個 Cleanup 函數,用於銷燬剛剛建立的定時器。
OK,聽上去仍是很抽象,再來看看下面的動畫吧:
動畫中有如下須要注意的點:
提示
將 Effect 推遲到渲染完成以後執行是出於性能的考慮,若是你想在渲染以前執行某些邏輯(不惜犧牲渲染性能),那麼可以使用
useLayoutEffect
鉤子,使用方法與useEffect
徹底一致,只是執行的時機不一樣。
再來看看 useEffect
的第二個參數:deps
(依賴數組)。從上面的演示動畫中能夠看出,React 會在每次渲染後都運行 Effect。而依賴數組就是用來控制是否應該觸發 Effect,從而可以減小沒必要要的計算,從而優化了性能。具體而言,只要依賴數組中的每一項與上一次渲染相比都沒有改變,那麼就跳過本次 Effect 的執行。
仔細一想,咱們發現 useEffect
鉤子與以前類組件的生命週期相比,有兩個顯著的特色:
componentDidMount
)、重渲染(componentDidUpdate
)和銷燬(componentDidUnmount
)三個階段的邏輯用一個統一的 API 去解決setInterval
和 clearInterval
),更突出邏輯的內聚性在最極端的狀況下,咱們能夠指定 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/v2"; 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 數據,其中有如下須要注意的點:
fetchGlobalStats
異步函數並進行調用從而獲取數據,而不是直接把這個 async 函數做爲 useEffect
的第一個參數;此外,第二個參數(依賴數組)爲空數組,所以整個 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 進行了一次數據獲取!
在上一步驟中,咱們在 App
組件中定義了一個 State 和 Effect,可是實際應用不可能這麼簡單,通常都須要多個 State 和 Effect,這時候又該怎麼去理解和使用呢?
在上一節的動畫中,咱們看到每一次渲染組件時,咱們都能經過一個神奇的鉤子把狀態」鉤「過來,不過這些鉤子從何而來咱們打了一個問號。如今,是時候解開謎團了。
注意
如下動畫演示並不徹底對應 React Hooks 的源碼實現,可是它能很好地幫助你理解其工做原理。固然,也能幫助你去啃真正的源碼。
咱們先來看看當組件初次渲染(掛載)時,狀況究竟是什麼樣的:
注意如下要點:
useState
定義了多個狀態;useState
,都會在組件以外生成一條 Hook 記錄,同時包括狀態值(用 useState
給定的初始值初始化)和修改狀態的 Setter 函數;useState
生成的 Hook 記錄造成了一條鏈表;onClick
回調函數,調用 setS2
函數修改 s2
的狀態,不只修改了 Hook 記錄中的狀態值,還即將觸發重渲染。OK,重渲染的時候到了,動畫以下:
能夠看到,在初次渲染結束以後、重渲染以前,Hook 記錄鏈表依然存在。當咱們逐個調用 useState
的時候,useState
便返回了 Hook 鏈表中存儲的狀態,以及修改狀態的 Setter。
提示
當你充分理解上面兩個動畫以後,其實就能理解爲何這個 Hook 叫
useState
而不是createState
了——之因此叫use
,是由於沒有的時候才建立(初次渲染的時候),有的時候就直接讀取(重渲染的時候)。
經過以上的分析,咱們不難發現 useState
在設計方面的精巧(摘自張立理:對 React Hooks 的一些思考):
useState
(和其餘鉤子),函數組件依然是實現渲染邏輯的「純」組件,對狀態的管理被 Hooks 所封裝了起來在對 useState
進行一波深挖以後,咱們再來揭開 useEffect
神祕的面紗。實際上,你可能已經猜到了——一樣是經過一個鏈表記錄全部的 Hook,請看下面的演示:
注意其中一些細節:
useState
和 useEffect
在每次調用時都被添加到 Hook 鏈表中;useEffect
還會額外地在一個隊列中添加一個等待執行的 Effect 函數;至此,上一節的動畫中那兩個「問號」的身世也就揭曉了——只不過是鏈表罷了!回過頭來,咱們想起來 React 官方文檔 Rules of Hooks 中強調過一點:
Only call hooks at the top level. 只在最頂層使用 Hook。
具體地說,不要在循環、嵌套、條件語句中使用 Hook——由於這些動態的語句頗有可能會致使每次執行組件函數時調用 Hook 的順序不能徹底一致,致使 Hook 鏈表記錄的數據失效。具體的場景就不畫動畫啦,自行腦補吧~
useEffect
(包括其餘相似的 useCallback
和 useMemo
等)都有個依賴數組(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/v2"; 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; 複製代碼
能夠看到:
countries
(全部國家的數據)和 key
(數據排序的指標,就是上面的五個);useEffect
鉤子進行數據獲取,和以前獲取全球數據相似,只不過注意咱們這邊第二個參數(依賴數組)是 [key]
,也就是隻有當 key
狀態改變的時候,纔會調用 useEffect
裏面的函數。把項目跑起來,能夠看到直方圖顯示了前十個國家的數據,而且能夠修改排序的指標(好比能夠從默認的累積確診 cases
切換成死亡人數 deaths
):
看上去挺不錯的!
到這裏,本系列第一篇也就講完啦,但願你真正理解了 useState
和 useEffect
——最最最經常使用的兩個 Hook。在下一篇教程中,咱們將繼續講解自定義 Hook 和 useCallback
。
想要學習更多精彩的實戰技術教程?來圖雀社區逛逛吧。