原文链接:https://medium.com/free-code-camp/learn-the-basics-of-react-hooks-in-10-minutes-b2898287fe5d
2019 年初,React 团队在 React 16.8.0 版本中新增了一个特性—— Hooks(钩子)。如果说 React 是一大碗糖果,那么 Hooks 就是最新款的、耐嚼的超美味糖果。
那么 Hooks 到底是什么呢?为什么会触发前端程序员的 ”真香“ 定律呢?
简介
React Hooks 诞生的初衷是为了提供一种更强大、更具表现力的方式来编写和复用组件中的通用功能。
从长远来看,我们希望 Hooks 成为人们编写 React 组件的主要方式。——React 团队
既然 Hooks 这么重要,为什么不以一种有趣的方式来学习它呢?
你可以借助这个具备实时编辑示例的 Hooks API 学习工具 来快速学习
糖果
想象一下,React 是一碗漂亮的糖果。这碗糖果能够适应世界上很多人的口味,但是糖果的厂家意识到碗内的有几块糖果虽然吃起来很美味,但是吃的过程十分麻烦(想想渲染参数和高阶组件)。于是他们做出了改革,并不是直接扔掉原来的糖果,而是加入了一些新的糖果,名字叫做「Hooks」。
这个糖果存在的目的就是让你更容易做你已经在做的事情,比如更容易的享受美味。这些糖果一点都不特别,如果你仔细品尝,你还能发现一些熟悉的味道—— JavaScript 函数。
状态 Hook
如上一章节谈及的,Hook 本质就是函数。官方发布的钩子函数总计有 10 个,这10 个 API 简化了组件中的功能实现和逻辑复用。首先,我们来看第一个 Hook —— useState
。
在过去很长一段时间里,我们没有办法在函数组件中使用本地状态值,Hooks 的出现解决了该问题。使用 useState
可以方便快捷地在函数组件中声明或更新本地状态,是不是很神奇呢?如下是一个简单的计数器应用:
一般来讲,我们会声明 Couter
组件如下:
import React, { Component } from 'react';
class Counter extends Component {
constructor() {
super();
this.state = {
count: 1
};
}
handleClick = () => {
this.setState(prevState => ({count: prevState.count + 1}))
}
render() {
const { count } = this.state;
return (
<div>
<h3 className="center">
Welcome to the Counter of Life
</h3>
<button className="center-block" onClick={this.handleClick}>
{count}
</button>
</div>
);
}
}
之所以使用类声明的方式来创建组件是为了跟踪组件内的 count
状态。现在,使用 useState
钩子函数,也可以使用函数声明的方式来创建 Count
组件。
两者有什么不同呢?
函数组件的声明,不需要使用 Class extends
关键字,并且不需要一个 render
的内部方法。
function CounterHooks() {
return (
<div>
<h3 className="center">Welcome to the Counter of Life </h3>
<button
className="center-block"
onClick={this.handleClick}> {count} </button>
</div>
);
}
上述代码存在两个问题:
- 不应该在函数组件中使用
this
关键字; -
count
状态尚未在函数体中定义。
针对第一个问题,可以将 handleClick
作为函数体内部的独立函数:
function CounterHooks() {
const handleClick = () => {
}
return (
<div>
<h3 className="center">Welcome to the Counter of Life </h3>
<button
className="center-block"
onClick={handleClick}> {count} </button>
</div>
);
}
在类组件中,count
来自于组件内的 state
对象。而在函数组件中,可以通过调用 useState
方法(钩子)产生。
调用 useState
时,可以传入一个参数,用于初始化某一状态。例如 useState(0)
中,0 就是要追踪的那个状态的初始值。该函数返回一个包含两个元素的数组,第一个元素为状态值,第二个是用于更新状态的函数,可以将两者看做是 state
和 setState
的变异体(并不完全相同)。于是,我们便可以对 Count
组件进行重构了:
function CounterHooks() {
// 🦄
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1)
}
return (
<div>
<h3 className="center">Welcome to the Counter of Life </h3>
<button
className="center-block"
onClick={handleClick}> {count} </button>
</div>
);
}
除了代码比较少之外,还有几个点需要注意:
第一,由于 useState
返回的是一个数组,可以通过“解构赋值”的方法,快速拆分为两个变量:
const [count, setCount] = useState(0);
第二,重构代码中的 handleClick
函数不需要引用 prevState
或类似的过去值,只需要向 setCount
传入新的状态值即可。
const handleClick = () => {
setCount(count + 1)
}
由于钩子函数保证了 count
状态值会在前后两次渲染中正确传递,所以当需要更新 count
值时,仅仅需要调用 setCount
并传入新的值即可,如 setCount(count + 1)
。
使用 Hooks 创建组件是不是很简单呢?虽然这仅仅是一个虚构的组件,但也是一个美好的开始。
注 :状态的更新函数也支持传入一个函数作为参数,比如当新的状态值依赖于旧的状态值并需要做进一步处理时,可以使用
setCount(prevCount => { if(prevCount >= 0) { return prevCount; } else { return -prevCount; } })
多状态场景
在类组件中,我们总是习惯在一个对象(state
)中设置所有的状态值,无论是多个属性还是仅有一个属性。
// 单个属性
state = {
count: 0
}
// 多个属性
state = {
count: 0,
time: '07:00'
}
使用 useState
时,有着细微的差别。在上面的计数器案例中,只使用了一个数值来初始化 count
状态,而非使用一个对象来存储状态。
那么,如果我们需要添加另一个状态呢?是否可以调用多个 useState
?我们使用下面这个组件来分析,该组件与 Count
组件基本一致,但是会使用点击次数来更新时间。
function CounterHooks() {
const [count, setCount] = useState(0);
const [time, setTime] = useState(new Date())
const handleClick = () => {
setCount(count + 1);
setTime(new Date())
}
return (
<div>
<h3 className="center">Welcome to the Counter of Life </h3>
<button
className="center-block"
onClick={handleClick}>{count}</button>
<p className="center">
at: {`${time.getHours()} : ${time.getMinutes()} : ${time.getSeconds()}`}</p>
</div>
);
}
如你所见,组件内钩子的用法基本一致,只是增多了一次 useState
的调用:
const [time, setTime] = useState(new Date())
如此,time
状态便可以直接在 JSX 渲染块中使用,记录点击按钮那一刻的时、分、秒。
但是,是否可以使用「useState
+ 对象」统一管理所有的状态,而不是多次调用 useState
呢?答案是肯定的,但是在更新 state
时,需要注意,与类组件中的 setState
不同,传入 useState
的对象将替代原来的状态对象,而 setState
则会合并两个对象属性。
// 🐢 合并 (setState) vs 替代 (useState)
// 假设初始状态对象为 {name: "Ohans"}
setState({age: "unknown"})
// 新的状态对象将是
// {name: "Ohans", age: "unknown"}
setStateGeneratedByUseState({age: "unknown"})
// 新的状态对象将是
// {age: "unknown"} - 初始状态值被替代了
副作用钩子
在类组件中,我们经常会使用日志打印、数据请求或管理订阅等副作用。这些副作用可以看做是不包含在渲染逻辑中的操作,而 useEffect
钩子就是为此创建的,怎么使用呢?
useEffect
钩子中需要传入一个函数,在该函数中,可以执行一系列的副作用,来看一个简单的例子。
useEffect(() => {
// 🐢 你可以在这里设置副作用
console.log("useEffect first timer here.")
})
在 useEffect
中,我传入了一个匿名函数,并在函数中调用一些副作用方法。
那么下一个问题是:什么时候该使用 useEffect
呢?
在类组件中,会有 componentDidMount
和 componentDidUpdate
等生命周期方法,而函数组件是不具备生命周期函数方法的,故而 useEffect
可以起到一定的补偿效果。因此,在上述例子中,但组件挂载(componentDidMount
)或组件更新(componentDidUpdate
)时,将调用 useEffect
中的方法(也称为「副作用方法」)。
将上面的 useEffect
逻辑加入到我们的 Counter
组件中,得到如下结果:
function CounterHooks() {
const [count, setCount] = useState(0);
const [time, setTime] = useState(new Date())
// 🐢 look here.
useEffect(() => {
console.log("useEffect first timer here.")
}, [count])
const handleClick = () => {
setCount(count + 1);
setTime(new Date())
}
return (
<div>
<h3 className="center">Welcome to the Counter of Life </h3>
<button className="center-block"
onClick={handleClick}>{count}</button>
<p className="center"> at: {`${time.getHours()} : ${time.getMinutes()} : ${time.getSeconds()}`}</p>
</div>
);
}
注意:
useEffect
钩子并不完全等价于componentDidMount
+componentDidUpdate
。可以这么理解,但并不完全等价,二者之间存在一些细微差别。
在上面的案例中,每次组件更新都会打印信息,并不是我们理想中的效果。如果你只是想在组件挂在时打印信息,应该怎么办呢?
看向 useEffect
的第二个参数,这是「副作用方法」的依赖数组,当数组内任一元素发生变化时,都会重新「副作用方法」。故而在上述案例中,「副作用方法」将在组件挂载和 count
状态值发生变化时执行。
如果你传入一个空数组,「副作用方法」将仅在组件挂载时执行,组件更新时将不再触发。
useEffect(() => {
console.log("useEffect first timer here.")
}, [])
订阅
在很多应用中,订阅和取消订阅都是很常用的副作用。那么 useEffect
中又应怎么使用呢?
useEffect(() => {
const clicked = () => console.log('window clicked');
window.addEventListener('click', clicked);
}, [])
在上面的代码中,当组件挂载时,window 上将绑定一个点击事件。那么又该如何取消订阅呢?
useEffect
中考虑了这一需求。如果在「副作用方法」中返回一个函数,那么卸载组件时将会调用该函数。因此,这是取消订阅的最佳位置,如下所示:
useEffect(() => {
const clicked = () => console.log('window clicked');
window.addEventListener('click', clicked);
return () => {
window.removeEventListener('click', clicked)
}
}, [])
使用 useEffect
钩子可以完成很多的行为,如 API 调用等。
自定义 Hooks
上面所有章节讲述的都是从 React 糖果盒中获取并使用糖果,然而 React 也为你提供了一种定制化糖果的方法——自定义 Hooks。
自定义 Hooks 本质上也只是一个普通方法,但是命名上要求以 "use" 开头,如有必要,在其内部,可以调用 React 内置的所有钩子函数。下面是个简单的例子:
// 🐢 自定义 hook - 以 "use" 作为前缀 (useMedium)
useMedium(() => {
const URI = "https://some-path-api";
// 🦄 内部可以使用内置的钩子函数
useEffect(() => {
fetch(URI)
},[])
})
Hooks 规则
在使用 Hooks 时,有两条规则需要遵守:
- 仅在顶层调用钩子,即不可以在条件、循环或嵌套函数中调用;
- 仅在 React 函数中调用钩子,即函数组件和自定义钩子。
ESLint 插件可以更好地协助你遵守以上规则。
译者言
本专题主要分享国外关于 hooks 的一些博客或讨论,主要会分成三个部分:话题引入、基础知识、实际应用。
本文简单介绍了 React Hooks 的部分特性,但还有很多没有提及。在后面的系列文章中,还会详细介绍 Hooks 中的各 API。