咱们研发开源了一款基于 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
。
想要学习更多精彩的实战技术教程?来图雀社区逛逛吧。