Hook 是 react 16.8 推出的新特性,具有如下优点:
- Hook 使你在无需修改组件结构的情况下复用状态逻辑。——自定义 hook
- Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)——Effect
- Hook 使你在非 class 的情况下可以使用更多的 React 特性。——拥抱函数式组件
1. 简介
Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:
- 只能在函数最外层调用 Hook,也即Hook 需要在我们组件的最顶层调用。不要在循环、条件判断或者子函数中调用,这会破坏更新时 hook 顺序的一致性,造成数据读取错误。
- 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。(还有一个地方可以调用 Hook —— 就是自定义的 Hook 中)
可以通过 ESLint 配置来提醒自己遵循 Hook 开发规则:
安装插件
npm install eslint-plugin-react-hooks --save-dev
配置
package.json
中的eslint-config:
// 你的 ESLint 配置
"eslintConfig": {
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则
"react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖
}
}
2. React Hook 的基础 API (附实例)
具体 API 介绍可以查阅官网 Hook API Reference
2.1 组件中的状态——useState
在使用 class 定义组件时,可以通过 this.state
定义组件内的状态属性,在 render 时使用 this.state[key]
获取状态值,使用 this.setState
来修改状态。Hook 中提供了 useState
方法,用于快速定义函数组件内的状态和对应的更改函数。
使用方法:const [state, setState] = useState(defaultValue)
实例:
import React, { useState } from 'react'
function Counter() {
// 初始化 count = 0
const [count, setCount] = useState(0)
return (
你点击了 {count} 次
{/* 直接调用 setCount,传入新的值,赋值给 count */}
)
}
2.2 组件中的生命周期——useEffect
函数式组件在发生更新时,都会顺序执行函数主体,相当于类组件中的 render 函数。而在 render 过程中,是不允许执行改变 DOM、添加订阅、设置定时器、记录日志等包含副作用的操作,因此在类组件中,我们通常在生命周期函数中执行必要的包含副作用操作。在函数式组件中,useEffect
提供了执行副作用操作的支持,当 React 渲染组件时,会保存已使用的 effect,并在更新完 DOM 后执行它。useEffect ≈ componentWillMount + componentDidUpdate + componentWillUnmount,其内部可以访问到组件的 props
和 state
。
使用方法:useEffect( didUpdateFn )
实例:
import React, { useState, useEffect } from 'react'
function Counter() {
// 初始化 count = 0
const [count, setCount] = useState(0)
// 如果第二个参数为空,则只有在组件被销毁时才解绑,也就是 副作用 等价于 componentDidMount,解绑 等价于 componentWillUnMount
useEffect(() => {
console.log('useEffect => 组件挂载')
// 返回一个清除函数,当副作用中有定时器或监听事件时清除
return () => {
console.log('useEffect => 组件被销毁')
};
}, [])
// 第二个参数,每次当 count 发生变化就执行解绑原来的数据并重新执行副作用
useEffect(() => {
console.log('useEffect => count 数据挂载')
return () => {
console.log('useEffect => count 数据解绑')
};
}, [count])
return (
你点击了 {count} 次
)
}
React 会在调用一个新的 effect 之前对前一个 effect 进行清理。在上述程序中,当 count 值更新时,会先输出 ’useEffect => count 数据挂载‘,再输出 ’useEffect => count 数据挂载‘。因此当我们在 useEffect
中设置定时器/事件时,通过返回一个清除函数,使得在下一次依赖发生更新时,能够清除上一次的定时器/事件,以此避免内存泄露。否则每次依赖更新时,都会增加一个定时器/事件。
2.3 跨组件通信——useContext
在跨组件通信时,可以借助 context 实现组件间的传值。useContext
用于快速获取组件上层最近的 contextObj.Provider
所提供的 value 值,等价于 contextObj.Consumer
。
使用方法:useContext(ContextObj)
,contextObj
是 React.createContext
返回的 context 对象
实例:
import React, { useState, useEffect, useContext, createContext } from 'react'
const ColorContext = createContext()
function Container() {
const [color, setColor] = useState('#ffff00')
const toggleColor = () => {
const saturation = () => Math.random() * 255
setColor(`rgb(${saturation()}, ${saturation()}, ${saturation()})`)
}
// 1. 使用 ColorContext.Provider 将 color 值传给内部的组件
// 3. 当按钮点击切换背景色时, color 值发生变化,将通知到 Counter 组件中
return (
)
}
function Counter() {
// ...
// 2. 使用 useContext 返回最近的 Provider 提供的 value 值,本例中也即 color 值,并订阅 color 值的变化
const color = useContext(ColorContext)
return (
你点击了 {count} 次
)
}
2.4 复杂状态管理——useReducer
在 redux 状态管理中,使用 reducer 根据 action 的不同,对 state 执行不同的操作。在 hook 中,useState
支持直接修改 state,但是当修改逻辑较为复杂时,可以改用 useReducer
来定义不同的更改行为。通过传入一个形如 (state, action) => {}
的 reducer,返回状态及其 dispatch 函数。还可以使用后面的两个参数对 state 执行初始化操作,initialArg
将作为 init
函数的参数传入。
使用方法:const [state, dispatch] = useReducer(reducer, initialArg, init)
实例:
import React, { useState, useEffect, useContext, createContext, useReducer } from 'react'
function Container() {
// ...
return (
)
}
function Counter() {
// ...
const init = initialCount => ({count: initialCount})
const [state, dispatch] = useReducer((state, action) => {
switch(action.type) {
case 'add':
return {count: state.count + 1}
case 'sub':
return {count: state.count - 1}
case 'reset':
return init(action.payload)
default:
return state
}
}, initialCount, init)
return (
你点击了 {state.count} 次
)
}
2.5 组件性能优化——useCallback / useMemo
在组件生命周期的应用中,常常有利用 shouldCompnentUpdate
判断参数/状态的相等性,避免不必要的组件渲染。 useCallback
也是一种类似的组件优化手段,其返回一个 memoized 函数,仅在依赖项发生变化时,函数体才会更新。useCallback(fn, deps)
相当于 useMemo(() => fn, deps)
,不同的是 useMemo
返回的是一个 memoized 值,当依赖项发生变化时,fn
才会执行,该值才会发生更新。传入 useMemo
的函数会在渲染期间执行,因此在 useMemo
内部,不要执行与渲染无关的操作。依赖项并不会作为参数传入回调函数中,但内部执行函数可以直接使用依赖项,如 fn(deps)
。
使用方法:useCallback(() => { doSomething() }, depsArr)
/ useMemo(() => doSomething(), depsArr)
实例:
import React, { useState, useEffect, useContext, createContext, useReducer, useMemo } from 'react'
// ...
function Counter() {
// ...
const [asyncName, setName] = useState('')
function sayName(name) {
setTimeout(() => {
// 模拟异步请求,并根据请求结果设置状态值
console.log(`${name} 正在操作`)
setName(`user_${name}`)
}, 1000)
}
// 直接调用 sayName 的话,每次 state.count 发生变化时,虽然 asyncName 并不会变化,但 sayName 每次都会被执行。如果是一个比较耗时的异步请求,将降低组件的性能
// sayName(name)
// 对比直接调用,使用 useMemo,能够避免 count 变化时 sayName 的频繁调用,从而优化组件性能
// 仅在依赖项 name 值发生变化时,sayName 方法才会被执行
useMemo(() => sayName(name), [name])
return (
用户名:{asyncName}
{/* ... */}
)
}
效果:
- 直接调用
sayName
- 使用
useMemo
2.6 组件内值的保存——useRef
ref
是一种访问 DOM 的方式,useRef
返回一个“盒子”,可以在其 current
属性中保存一个任何类型的可变值,如 DOM 元素、定时器、订阅器等。useRef
在每次渲染时返回同一个 ref 对象,当 ref 对象内容发生变化时,useRef
并不会通知你。变更 .current
属性不会引发组件重新渲染。
使用方法:const oRef = useRef(initialValue)
实例:
import React, {useRef, useEffect} from 'react'
function InputItem() {
const inputEle = useRef(null)
const timerId = useRef(null)
const [time, setTime] = useState(0)
useEffect(() => {
const id = setInterval(() => {
// 使用函数更新的方式,避免依赖项
setTime(t => t + 1)
}, 1000)
timerId.current = id
return () => clearInterval(id)
}, [])
const focusBtnClick = () => {
// inputEle.current 已经挂载到 DOM 中的文本输入框元素上
inputEle.current.focus()
}
const clearBtnClick = () => {
// timerId.current 已经被写入了定时器的 id,可以在 click 事件中中止定时器
clearInterval(timerId.current)
}
return (
定时器数值为:{time}
)
}
效果:
2.7 其它
以下三个 hook 都是极少使用的方法,简单介绍其应用,有必要时可以查阅官方文档
-
useImperativeHandle
:用于自定义暴露给父组件的子组件内部某一 ref 实例值,与forwardRef
(将内部某一 ref 实例值全部暴露给父组件) 配合使用。 -
useLayoutEffect
:作用同useEffect
,不同的是useEffect
是在 DOM 元素渲染完成后执行,而useLayoutEffect
是与 DOM 更新同步执行。 -
useDebugValue
:用于在 React 开发者工具中显示自定义 hook 的标签。
3. 自定义 Hook
在函数化开发时,我们常常将多个函数间共用的逻辑抽离为某一功能函数,增强代码的复用性。而在组件化开发过程中,两个组件之间也可能存在同样的功能逻辑,比如需求列表和详情页都需要获取需求项的状态(规划中/进行中/已完成),此时可以把 “查询需求项状态” 这一功能用自定义 hook 抽离出来,不仅能够提高代码复用性和可读性,还能方便测试。自定义 hook 命名需要以 use 开头,以方便 react 自动检查是否违反了 hook 规则。目前,也有很多第三方 hook 实现:https://github.com/streamich/react-use
实例:
import React, { useState, useEffect } from 'react'
// 自定义 hook : 根据需求项的 id 值查询状态
function useStatus(demandId) {
// 需求项状态,0 - 规划中, 1 - 进行中, 2 - 已完成
const [status, setStatus] = useState(0)
useEffect(() => {
// 模拟一下异步请求
const getStatus = setTimeout(() => {
setStatus(demandId % 3)
}, 200)
return () => {
clearTimeout(getStatus)
}
}, [demandId])
const description = ['规划中', '进行中', '已完成']
return {code: status, status: description[status]}
}
// 需求列表组件
function DemandList() {
const [list, setList] = useState([])
const [selectedId, setSelectedId] = useState(null)
useEffect(() => {
// 模拟一下数据
let mockList = []
for(let i = 0; i <= 9; i++) {
const id = i + Math.round(Math.random() * 100)
mockList.push({
id,
name: `需求${id}`
})
}
setList(mockList)
}, [])
return (
{list.map( demand => (
{demand.name}
) )}
{/* 这里为了偷懒,就没有用路由,而是直接显示在下面 */}
{ selectedId && }
)
}
// 需求项组件
function DemandItem({demandId, selectedMethod, children}) {
// 调用自定义 hook 获取需求项状态
const {code, status} = useStatus(demandId)
const color = ['#F4A460', '#FFD700', '#32CD32']
return (
{children}
{selectedMethod(demandId)}}
>{status}
)
}
// 需求详情页组件
function DemandDetail({demandId}) {
// 调用自定义 hook 获取需求项状态
const {status} = useStatus(demandId)
const [info, setInfo] = useState({})
useEffect(() => {
// 根据 id 查询需求的详细信息
setInfo({
name: `需求${demandId}`,
detail: '这是需求详情信息呀~这是需求详情信息呀~这是需求详情信息呀~这是需求详情信息呀~这是需求详情信息呀~'
})
}, [demandId])
return (
项目名称为:{info.name}({status})
{info.detail}
)
}
export default DemandList
效果: