前言
什么是 Hooks ?:
Hooks------即钩子 是 React 提供的一组工具函数,让函数组件能够保存和更新数据、操作页面元素,以及在组件之间共享数据。。
一、useState
useState 用来保存会影响页面显示的数据,例如计数或年龄,让页面随数据的更新而变化。
1.1 基本用法
使用时,需要先从 react 导入 useState,再在组件函数中传入初始值,接收它返回的状态和更新函数。
比如先做一个点击后加 1 的按钮:
jsx
import { useState } from 'react'
export default function State() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
)
}
这段代码中:
- useState(0) 中的 0 是初始值。
- useState 返回一个数组,count 接收第一项,表示当前状态;setCount 接收第二项,这是专门用来更新状态的一个函数。
- JSX 中的 {count} 把当前状态显示在按钮上。
- 点击按钮时,setCount(count + 1) 把新值交给 React,让页面随状态更新。
按钮最初显示 0,点击后依次显示 1、2、3。如果需要修改这个状态时,需要调用 setCount,不能直接给 count 赋值,否则组件不会重新渲染。
1.2 状态保存与重新渲染
调用 setCount 将状态更新为不同的值后,React 会安排组件重新渲染,即组件代码全部重新执行。
但当再次执行到 useState(0) 时,React 会返回为当前组件保存的状态。例如,第一次点击后,count 得到的是 1,不会重新变回初始值 0。
useState 这一行仍会执行,但状态不会重新初始化。 可以把它理解为 React 为组件保存了一份数据,这份数据会在组件保持挂载期间的多次渲染之间保留下来。
1.3 用函数计算初始值
如果初始值需要计算,可以给 useState 传入一个箭头函数,比如:
jsx
const [count, setCount] = useState(() => {
let a = 1
let b = 2
return a + b
})
React 初始化状态时调用这个函数,将返回值 3 保存为 count,所以按钮最初显示 3。这个函数叫作初始化函数。
1.4 注意:初始化函数不能等待异步结果
上面的计算立即就能得到结果。那如果初始值需要等一秒才能拿到,把 useState 调用改成下面这样行不行呢?
jsx
const [count, setCount] = useState(async () => {
const a = await new Promise((resolve) => {
setTimeout(() => resolve(1), 1000)
})
return a + 2
})
这种写法不能实现"等异步完成后,再用 3 初始化状态"。
原因是 async 函数返回 Promise。React 调用初始化函数后,会直接保存它的返回值,不会等待 Promise 完成。因此,这里的 count 得到的是 Promise,而不是最终的数字 3。
所以,函数形式的初始值需要同步返回所需的状态。如果数据来自异步操作,应先给状态一个初始值,等结果拿到后再调用 setCount 更新。
二、useEffect
useEffect 用来处理组件渲染后的副作用,例如获取数据、打印日志或操作 DOM。
2.1 基本用法
使用 useEffect 时,把需要执行的操作写进一个函数,再把这个函数传给 useEffect。React 会在组件的渲染结果提交后调用它。
下面的组件有两个按钮,分别修改 age 和 data。我们用一条日志观察副作用何时执行:
jsx
import { useEffect, useState } from 'react'
export default function Effect() {
const [age, setAge] = useState(18)
const [data, setData] = useState(0)
useEffect(() => {
console.log('副作用执行了')
})
return (
<div>
<button onClick={() => setAge(age + 1)}>年龄:{age}</button>
<button onClick={() => setData(data + 1)}>数据:{data}</button>
</div>
)
}
组件第一次出现在页面上叫作挂载。挂载后,控制台打印"副作用执行了"。点击任意一个按钮,状态改变,组件重新渲染,之后又会打印这条日志。
这里传给 useEffect 的箭头函数,就是副作用函数。它由 React 安排执行,不需要我们手动调用,也不需要像 useState 那样接收返回值。
不过,有些操作只需要在挂载时执行,有些需要在特定数据变化后执行。这时就要用到 useEffect 的第二个参数:依赖数组。
2.2 用依赖数组控制执行时机
下面仍然使用这两个按钮,只改变 useEffect 的写法。
2.2.1 不传依赖数组
前面的完整示例没有传第二个参数,所以每次渲染提交后,副作用函数都会执行。
因此,初次挂载会打印日志;点击"年龄"或"数据"按钮,也都会再次打印。
2.2.2 传入空数组
给刚才的 useEffect 加上第二个参数 \[\]:
jsx
useEffect(() => {
console.log('副作用执行了')
}, [])
这次,初次挂载后会打印日志,但点击任何一个按钮,都不会再次打印。
按钮上的数字仍然会变化,组件也确实重新渲染了。只是空数组表示没有需要跟踪的依赖,React 不需要再次执行这个副作用函数。但如果组件被卸载后重新挂载,副作用函数仍然会执行。
2.2.3 传入需要跟踪的变量
如果要在 age 变化后打印它的最新值,就把 age 放进依赖数组:
jsx
useEffect(() => {
console.log('当前年龄:', age)
}, [age])
初次挂载时,输出 18。之后:
- 点击"年龄"按钮,age 变成 19,依赖发生变化,副作用函数再次执行,输出 19。
- 点击"数据"按钮,data 变了,但 age 没变,因此不会再次输出。
age 是组件的状态,会随着更新而变化,属于响应式变量。把它列为依赖,就是让 React 在每次渲染后比较它是否发生变化;有变化,才重新执行副作用。
三种写法的区别如下:
| 操作 | 不传依赖数组 | 空数组 \[\] | age |
|---|---|---|---|
| 初次挂载 | 执行 | 执行 | 执行 |
| 点击"年龄"按钮 | 执行 | 不执行 | 执行 |
| 点击"数据"按钮 | 执行 | 不执行 | 不执行 |
2.3 返回清理函数
除了执行副作用,有时还需要在组件卸载时做收尾工作。可以在副作用函数中,再返回一个函数:
jsx
useEffect(() => {
console.log('开始副作用')
return () => {
console.log('清理副作用')
}
}, [])
这里有两个函数,各自负责不同的事情:
- 外层函数执行副作用,挂载后打印"开始副作用"。
- return 返回的内层函数负责清理,React 会保存它,等需要清理时再调用。
当父组件不再渲染这个组件时,组件被卸载,React 调用保存的清理函数,打印"清理副作用"。
三、useRef
useRef 用来保存需要在组件多次渲染之间保留的值,既可以存 DOM 节点,也可以存普通数据。
3.1 基本用法
从 react 导入 useRef 后,在组件函数中传入初始值,并接收返回的 ref 对象:
jsx
const numRef = useRef(0)
这里的 0 是初始值。numRef 是一个对象,保存的数据放在它的 current 属性里,因此读取 numRef.current 就能得到 0。
需要修改时,可以在点击事件中给 numRef.current 赋新值;需要获取 DOM 时,则把这个 ref 交给元素的 ref 属性。下面分别看这两种用法。
3.2 获取 DOM
先让输入框在挂载后自动获得焦点:
jsx
import { useRef, useEffect } from 'react'
export default function Ref() {
const inputRef = useRef(null)
useEffect(() => {
inputRef.current.focus()
}, [])
return <input type="text" ref={inputRef} />
}
这段代码分三步完成:
- useRef(null) 创建一个 ref,此时 current 还没有指向 DOM。
- input 的 ref 属性把这个 ref 与输入框关联。React 创建输入框后,将 DOM 节点放进 current。
- 挂载后的 Effect 执行,通过 inputRef.current 调用 focus,输入框获得焦点。
因此,获取 DOM 的关键是先关联 ref,再通过 current 访问节点。
3.3 缓存一个值
ref 不只能存 DOM,也可以存数字。下面分别用两个按钮修改 ref 和触发重新渲染:
jsx
import { useRef, useState } from 'react'
export default function Ref() {
const numRef = useRef(0)
const [renderCount, forceRender] = useState(0)
return (
<div>
<button onClick={() => {
numRef.current++
console.log('保存的数字:', numRef.current)
}}>数字加 1</button>
<button onClick={() => forceRender((prev) => prev + 1)}>
触发更新:{renderCount}
</button>
</div>
)
}
依次操作:
- 点击两次"数字加 1",控制台先后输出 1、2,页面上的"触发更新"仍显示 0。
- 点击"触发更新",状态变化,组件重新渲染,按钮显示 1。
- 再点击"数字加 1",控制台输出 3,而不是重新从 1 开始。
这说明 ref 中的值在重新渲染后仍然保留。
3.4 注意:修改 ref 不会更新页面
修改 current 本身不会触发重新渲染。 上面的"数字加 1"按钮只修改保存的数值;"触发更新"按钮调用了状态更新函数,才会让组件重新渲染。
需要改变页面显示的数据,用 useState;只需要保存下来、供后续读取的数据,可以用 useRef。
四、useContext
useContext 用来读取上层组件提供的共享数据,让不同层级的后代组件使用同一份数据。
4.1 基本用法
使用上下文分三步:用 createContext 创建上下文对象,在上层组件中用 Provider 提供值,最后在后代组件中调用 useContext 读取值。
useContext 的参数是上下文对象,返回值是上层 Provider 提供的数据。先传一个固定的数字 100:
jsx
import { createContext, useContext } from 'react'
const countContext = createContext()
export default function Context() {
return (
<countContext.Provider value={100}>
<Child1 />
</countContext.Provider>
)
}
function Child1() {
const count = useContext(countContext)
return <div>ContextChild1 -- {count}</div>
}
页面显示"ContextChild1 -- 100",过程如下:
- createContext 创建上下文对象 countContext。
- 上层通过 Provider 的 value 提供 100。
- Child1 把同一个 countContext 传给 useContext,读取到 100。
后代即使隔着多层组件,也可以这样读取上层提供的值。
4.2 让后代修改共享状态
固定值只能读取。要让后代修改它,可以把值改为上层的 state,并将更新函数一起提供出去:
jsx
import { createContext, useContext, useState } from 'react'
const countContext = createContext()
export default function Context() {
const [num, setNum] = useState(100)
return (
<countContext.Provider value={[num, setNum]}>
<Child1 />
<Child2 />
</countContext.Provider>
)
}
function Child1() {
const [count, setCount] = useContext(countContext)
return (
<button onClick={() => setCount(count + 1)}>
ContextChild1 -- {count}
</button>
)
}
function Child2() {
const [count] = useContext(countContext)
return <div>ContextChild2 -- {count}</div>
}
这次 value 提供的是一个数组:第一项是 num,第二项是 setNum。Child1 两项都接收,Child2 只读取第一项。
两个组件最初都显示 100。点击 Child1 的按钮后:
- Child1 调用 setCount,它就是上层传下来的 setNum,只是接收时换了名字。
- 上层的 num 更新为 101,Provider 随之提供新值。
- Child1 和 Child2 都读取了这个上下文,因此都会重新渲染,显示 101。
Child2 没有调用更新函数,也能得到新值,因为两个组件读取的是上层管理的同一份状态。
4.3 注意:使用同一个上下文对象
组件拆开存放时,需要导出创建好的上下文对象,再在使用它的组件中引入。提供和读取必须使用同一个对象。