文章目录
Hook简介
概述
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
在我们继续之前,请记住 Hook 是:
- 完全可选的。 你无需重写任何已有代码就可以在一些组件中尝试 Hook。但是如果你不想,你不必现在就去学习或使用 Hook。
- 100% 向后兼容的。 Hook 不包含任何破坏性改动。
- 现在可用。 Hook 已发布于 v16.8.0。
没有计划从 React 中移除 class。
class组件的不足
- 组件之间难以复用状态逻辑
- 复杂组件难以理解
- 使用class导致学习成本变高
Hook文档:https://zh-hans.legacy.reactjs.org/docs/hooks-intro.html
什么是 Hook?
Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。Hook 不能在 class 组件中使用 —— 这使得你不使用 class 也能使用 React。
什么时候我会用 Hook? 如果你在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其它转化为 class。现在你可以在现有的函数组件中使用 Hook。
Hook 使用规则
Hook 本质就是 JavaScript 函数,但是使用它们会有额外的规则:
- Hook不能在class 组件中使用
- 只能在函数组件最外层调用 Hook,不要在循环、条件判断或者子函数中调用
- 只能在React的函数组件中调用Hook,不要在其他JavaScript函数中调用
- 在 React 的函数组件中调用 Hook
- 在自定义 Hook 中调用其他 Hook
state的研究(useState)
State:组件的记忆(响应式数据)
组件通常需要根据交互更改屏幕上显示的内容。输入表单应该更新输入字段,单击轮播图上的“下一个”应该更改显示的图片,单击“购买”应该将商品放入购物车。组件需要“记住”某些东西:当前输入值、当前图片、购物车。在 React 中,这种组件特有的记忆被称为 state。
当普通的变量无法满足时
以下是一个渲染点击次数的组件。点击按钮应该显示点击次数并将 count
更改为 1
,再次点击又更改为 2
,以此类推。但这个组件现在不起作用(你可以试一试!):
export default function App () {
let count = 0;
function handleClick() {
count++;
console.log('count: ', count);
}
return (
<div>
<h1>App 点击次数:{count}</h1>
<button onClick={handleClick}>按钮</button>
</div>
);
}
handleClick()
事件处理函数正在更新局部变量 count
。但存在两个原因使得变化不可见:
- 局部变量无法在多次渲染中持久保存。 当 React 再次渲染这个组件时,它会从头开始渲染——不会考虑之前对局部变量的任何更改。
- 更改局部变量不会触发渲染。 React 没有意识到它需要使用新数据再次渲染组件。
要使用新数据更新组件,需要做两件事:
- 保留 渲染之间的数据。
- 触发 React 使用新数据渲染组件(重新渲染)。
useState
Hook 提供了这两个功能:
- State 变量 用于保存渲染间的数据。
- State setter 函数 更新变量并触发 React 再次渲染组件。
添加一个 state 变量
要添加 state 变量,先从文件顶部的 React 中导入 useState
:
import { useState } from 'react';
然后,替换这一行:
let count = 0;
将其修改为一个 state 变量:
const [count, setCount] = useState(0);
这里的
[
和]
语法称为数组解构,它允许你从数组中读取值。useState
返回的数组总是正好有两项。
你将从 useState
中获得两样东西:当前的 state(count
)变量,以及用于更新它的setter函数(setCount
)。你可以给它们起任何名字,但按照惯例,需要像这样 [something, setSomething]
为它们命名。
import { useState } from 'react';
export default function App () {
const [count, setCount] = useState(0);
function handleClick() {
// 改变count
setCount(count + 1);
}
return (
<div>
<h1>App 点击次数:{count}</h1>
<button onClick={handleClick}>按钮</button>
</div>
);
}
第一次显示时,count
的值为 0
,因为你把 0
传给了 useState()
。当你想改变 state 时,调用 setCount()
并将新的值传递给它。点击该按钮计数器将递增。React 将再次调用你的组件函数。这次,count
会变成 1
。接着,变成 2
。以此类推。
遇见你的第一个 Hook
在 React 中,以 use
开头的函数被称为 Hook。useState
是 React 提供的一个内置 Hook。你可以在 React API 参考 中找到其他内置的 Hook。你也可以通过组合现有的 Hook 来编写属于你自己的 Hook。
Hook 是特殊的函数,只在 React 渲染时有效。它们能让你 “hook” 到不同的 React 特性中去。
Hook 比普通函数更为严格。你只能在你的组件(或其他 Hook)的 顶层 调用 Hook。如果你想在一个条件或循环中使用 useState
,请提取一个新的组件并在组件内部使用它。
陷阱
Hooks ——以
use
开头的函数——只能在组件或自定义 Hook 的最顶层调用。 你不能在条件语句、循环语句或其他嵌套函数内调用 Hook。Hook 是函数,但将它们视为关于组件需求的无条件声明会很有帮助。在组件顶部 “use” React 特性,类似于在文件顶部“导入”模块。
剖析 useState
当你调用 useState
时,你是在告诉 React 你想让这个组件记住一些东西:
const [count, setCount] = useState(0);
在这个例子里,你希望 React 记住 count
。
注意
惯例是将这对返回值命名为
const [thing, setThing]
。你也可以将其命名为任何你喜欢的名称,但遵照约定俗成能使跨项目合作更易理解。
useState
的唯一参数是 state 变量的初始值。在这个例子中,count
的初始值被useState(0)
设置为 0
。
每次你的组件渲染时,useState
都会给你一个包含两个值的数组:
- state 变量 (
count
) 会保存上次渲染的值。 - state setter 函数 (
setCount
) 可以更新 state 变量并触发React 重新渲染组件。
执行
setThing
函数会重新渲染组件,就相当于组件函数重新被调用一次
以下是实际发生的情况:
const [count, setCount] = useState(0);
- 组件进行第一次渲染。 因为你将
0
作为count
的初始值传递给useState
,它将返回[0, setCount]
。 React 记住0
是最新的 state 值。 - 你更新了 state。当用户点击按钮时,它会调用
setCount(count+ 1)
。count
是0
,所以它是setCount(1)
。这告诉 React 现在记住count
是1
并触发下一次渲染。 - 组件进行第二次渲染。React 仍然看到
useState(0)
,但是因为 React 记住 了你将count
设置为了1
,它将返回[1, setCount]
。 - 以此类推!
赋予一个组件多个 state 变量
你可以在一个组件中拥有任意多种类型的 state 变量。该组件有两个 state 变量,一个数字 count
和一个布尔值 showMore
,点击 “Show Details” 会改变 showMore
的值:
import { useState } from 'react';
export default function App() {
const [count, setCount] = useState(0);
const [showMore, setShowMore] = useState(false);
function handleClick() {
setCount(count + 1);
console.log('count: ', count);
}
function handleMoreClick() {
setShowMore(!showMore);
}
return (
<div>
<h1>App 点击次数:{count}</h1>
{showMore && <p>记录次数使用的是React的响应式状态</p>}
<button onClick={handleClick}>按钮</button>
<button onClick={handleMoreClick}>{showMore ? 'Hide' : 'Show'} details</button>
</div>
);
}
如果它们不相关,那么存在多个 state 变量是一个好主意,例如本例中的 count
和 showMore
。但是,如果你发现经常同时更改两个 state 变量,那么最好将它们合并为一个。例如,如果你有一个包含多个字段的表单,那么有一个值为对象的 state 变量比每个字段对应一个 state 变量更方便。 选择 state 结构在这方面有更多提示。
State 是隔离且私有的
State 是屏幕上组件实例内部的状态。换句话说,如果你渲染同一个组件两次,每个副本都会有完全隔离的 state!改变其中一个不会影响另一个。
在这个例子中, Count
组件以同样的逻辑被渲染了两次。试着点击每个Count
组件的按钮。你会注意到它们的 state 是相互独立的:
import { useState } from 'react';
export default function App() {
return (
<div>
<h1>App </h1>
<Count></Count>
<Count></Count>
</div>
);
}
function Count() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>点击次数:{count}</button>
</div>
);
}
这就是 state 与声明在模块顶部的普通变量不同的原因。 State 不依赖于特定的函数调用或在代码中的位置,它的作用域“只限于”屏幕上的某块特定区域。你渲染了两个 <Count/>
组件,所以它们的 state 是分别存储的。
还要注意 App
组件“不知道”关于 Count
state 的任何信息,甚至不知道它是否有任何 state。与 props 不同,state 完全私有于声明它的组件。父组件无法更改它。这使你可以向任何组件添加或删除 state,而不会影响其他组件。
如果你希望两个Count
保持其 states 同步怎么办?在 React 中执行此操作的正确方法是从子组件中删除 state 并将其添加到离它们最近的共享父组件中。
渲染和提交
组件显示到屏幕之前,其必须被 React 渲染。理解这些处理步骤将帮助您思考代码的执行过程并能解释其行为。
想象一下,您的组件是厨房里的厨师,把食材烹制成美味的菜肴。在这种场景下,React 就是一名服务员,他会帮客户们下单并为他们送来所点的菜品。这种请求和提供 UI 的过程总共包括三个步骤:
- 触发 一次渲染(把客人的点单分发到厨房)
- 渲染 组件(在厨房准备订单)
- 提交 到 DOM(将菜品放在桌子上)
![]() |
![]() |
![]() |
![]() |
![]() |
---|---|---|---|---|
触发 | 渲染 | 提交 |
步骤 1: 触发一次渲染
有两种原因会导致组件的渲染:
- 组件的 初次渲染。
- 组件(或者其祖先之一)的 状态发生了改变。
初次渲染
当应用启动时,会触发初次渲染。框架和沙箱有时会隐藏这部分代码,但它是通过调用目标 DOM 节点的 createRoot
,然后用你的组件调用 render
函数完成的:
// Image.jsx
export default function Image() {
return (
<img
src="https://i.imgur.com/ZF6s192.jpg"
alt="'Floralis Genérica' by Eduardo Catalano: a gigantic metallic flower sculpture with reflective petals"
/>
);
}
// App.jsx
import Image from './Image.js';
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'))
root.render(<Image />);
试着注释掉 root.render()
,然后您将会看到组件消失。
状态更新时重新渲染
一旦组件被初次渲染,您就可以通过使用 set
函数 更新其状态来触发之后的渲染。更新组件的状态会自动将一次渲染送入队列。(您可以想象这种情况成餐厅客人在第一次下单之后又点了茶、点心和各种东西,具体取决于他们的胃口。)
![]() |
![]() |
![]() |
![]() |
![]() |
---|---|---|---|---|
状态更新… | …触发… | …渲染 |
步骤 2: React 渲染您的组件
在您触发渲染后,React 会调用您的组件来确定要在屏幕上显示的内容。“渲染中” 即 React 在调用您的组件。
- 在进行初次渲染时, React 会调用根组件。
- 对于后续的渲染, React 会调用内部状态更新触发了渲染的函数组件。
这个过程是递归的:如果更新后的组件会返回某个另外的组件,那么 React 接下来就会渲染 那个 组件,而如果那个组件又返回了某个组件,那么 React 接下来就会渲染 那个 组件,以此类推。这个过程会持续下去,直到没有更多的嵌套组件并且 React 确切知道哪些东西应该显示到屏幕上为止。
在接下来的例子中,React 将会调用 Gallery()
和 Image()
若干次:
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'))
root.render(<Gallery />);
function Gallery() {
return (
<section>
<h1>鼓舞人心的雕塑</h1>
<Image />
<Image />
<Image />
</section>
);
}
function Image() {
return (
<img
src="https://i.imgur.com/ZF6s192.jpg"
alt="'Floralis Genérica' by Eduardo Catalano: a gigantic metallic flower sculpture with reflective petals"
/>
);
}
- 在初次渲染中, React 将会为
<section>
、<h1>
和三个<img>
标签 创建 DOM 节点。 - 在一次重渲染过程中, React 将计算它们的哪些属性