常用的Hooks

3 阅读8分钟

常用的Hooks

1. 状态管理:useState/useReducer

useState

  • 在函数组件中引入状态,React会在组件渲染时记住当前状态值,触发更新时重新渲染组件
  • 状态更新时异步的,多个setState会被批量处理
const [count, setCount] = useState(0)
// 直接更新
setCount(count + 1)
// 函数式更新(基于前一个状态)
setCount(pre => pre + 1)

useReducer

  • useState的替代方案,适合复杂状态逻辑或含多子值
// 表单状态机(状态转换复杂)
const formReducer = (state, action) => {
    switch(action.type) {
        case 'field_change':
            return { ...state, [action.field]: action.value, dirty: true}
        case 'validate':
            return { ...state, errors: validate(state), valid: !hasErrors};
        case 'submit':
            return state.valid ? { ...state, submitting: true } : state;
        default:
            return state;
    }
}
const [formState, dispatch] = useReducer(formReducer, initialState);
// dispatch({type: 'field_change', field:'email', value:'a@b.com'})

2. 副作用处理类

useEffect/useLayoutEffect

  • 组件首次渲染后,执行副作用函数
  • 组件卸载时,执行最后一次的清理函数
  • 工作原理
    • 保存上一次的依赖项,React在内部保存当前的依赖项数组
    • 浅比较(Shallow Comparison)使用Object.is()算法逐项比较Object.is(oldUserId, newUserId)
    • 任何一项变化执行effect
    • 基本类型值比较,引用类型按引用比较,浅比较不检查对象内部属性
    • Object.is()与===相似
    • NAN = NAN false; +0 === -0 true
    • object.is(NaN, NaN) true, object.is(+0, -0) false
  • 不提供依赖项,每次渲染后都执行
    • 可能导致不必要的性能开销
    useEffect(() => {
        console.log('组件已更新')
    })
    
  • 空依赖项数组[]:仅在初始渲染后执行一次
    • 初始化操作,一次性设置
    useEffect(() => {
        fetchData() // 从服务器获取初始数据
    })
    
  • 包含依赖项的数组
    • 响应特定数据变化
    useEffect(() ={
        fetchUserDate(userId).then(data => setUser(data))
    }, [userId]) // 依赖于userId
    

useEffect

  • 渲染阶段(Render) --> Commit(DOM更新) --> 浏览器绘制(Paint) -> useEffect执行(异步执行),不阻塞视觉
  • useEffect(() => {}, []) --> 仅对应componentDidMount(挂载时执行一次)
  • useEffect(()=>{},[dep]) --> 对应componentDidMount + componentDidUpdate(挂载执行 + 依赖变化更新)
  • 清理函数 --> 对应componentWillUnmount(卸载) + 依赖更新前的清理(类组件无直接对应)
// useEffect:异步副作用(数据请求)
useEffect(() => {
    const fetchData = async () => {
        const res = await fetch('/api/data');
        setData(await res.json())
    }
    fetchData()
    return () => { /* 清理逻辑(如取消请求)*/}
}, [])

useLayoutEffect

  • 渲染阶段(Render) -> 提交阶段(Commit,更新DOM) -> useLayoutEffect同步执行(阻塞浏览器绘制) -> 浏览器绘制(Paint),阻塞视觉
  • 使用DOM测量,布局调整,避免闪烁
const [num, setNum] = useState(1)
useLayoutEffect(() => {
    // 定义可复用的函数引用(解决事件监听移除无效问题)
    const handleFn = () { console.log('自定义事件fn触发') }
    document.body.addEventListener('fn', handleFn)
    console.log('useLayoutEffect, 打印顺序在useEffect之前')
    return () => {
        document.body.removeEventListener('fn', handleFn)
    }
}, [num])
// useLayoutEffect: 同步DOM操作(避免闪烁)
useLayoutEffect(() => {
    const dom = document.getElementById('box')
    dom.style.top = `${dom.offsetHeight}px`; // 同步调整位置
}, [])

3. 性能优化类

useMemo/React.memo/useCallback

  • React组件默认“父组件重渲染 -> 所有子组件重渲染”,这类API核心是缓存,减少无意义的计算/渲染

useMemo

  • 让组件中的函数跟随状态更新,缓存一个计算结果(值),只有当依赖项变化时才会重新计算
  • 依赖项不变时,直接返回缓存值,不重新执行计算函数
// useMemo: 缓存计算结果
const filteredList = useMemo(() => {
    return list.filter(item => item.age > 18); // 仅list变化时重新计算
}, [list])

React.memo

  • 高阶组件(HOC),包装函数组件后,对传入的props做浅比较,只有props变化时组件才会渲染,否则复用上次的渲染结果
  • 函数props引用变化,导致memo失效
  • 最佳配合:当子组件memo包装,且父组件给子组件传递函数props时,必须有个useCallback缓存该函数,才能让memo真正生效
import React, { memo, useState } from 'react;
// 子组件:用memo包装,期望props不变时不重渲染
const Child = memo(({onClick, name}) => {
    console.log('Child组件重渲染了')
    return <button onClick={onClick}>{name}</button>
})
// 子组件props 只有基本类型,memo单独用就够了
const Child1 = memo(({age}) => {
    console.log('Child重渲染')
    return <div>年龄:{age}</div>
})
// 父组件
const Parent =() => {
    const [count, setCount] = useState(0)
    const age = 20; // 基本类型,引用不变
    // 问题:每次父组件渲染,都会创建新的handleClick函数(引用变了)
    const handleClick = () => {
        console.log('点击了')
    }
    return (
        <div>
            <button onClick={() => setCount(count + 1)}>计数:{count} </button>
            {/* 即使name不变,handleClick引用变了 -> memo 认为props变了 -> 子组件重渲染 */}
            <Child onClick={handleClick} name="按钮" />
            <Child1 age={age} /> {/* age不变 -> 子组件不重渲染*/}
        </div>
    )
}
// 对上面进行局部改造可解决问题
// 关键:用useCallback缓存函数,依赖为空-> 永远返回统一个引用
const handleClick = useCallback(() => {
    console.log('点击了');
},[]) // 依赖项数组:无依赖 -> 函数引用永不变化
{/*onClick 引用不变,name不变 -> memo 生效,子组件不重渲染 */}
<Child onClick={handleClick} name='按钮' />

useCallback

  • 跟随状态更新函数,缓存函数引用,即函数本身,而不是函数的执行结果
  • 依赖项不变时,返回同一个函数地址,而非每次渲染创建新函数
  • useMemo(() => fn, deps)相当于useCallback(fn, deps)
  • 在使用方法上,useCallback与useMemo相同,useMemo返回的是一个值,useCallback返回的是个函数
  • 给子组件传值时候用useCallback比较好
const [num, setNum] = useState(1)
const getDoubleNum = useCallback(() => {
    return num * 2
}, [num])
return (<div>
    { getDoubleNum()}
    <Child callback={getDoubleNum}></Child>
</div>)
function Child(props) {
    useEffect(() => {
        console.log('callback更新了')
    }, [props.callback])
}

4. 引用/通信类:useRef/useContext

useRef

  • 一个持久化的Ref对象(Fiber节点的Ref属性),ref.current不受组件重渲染影响,且修改它不会触发组件重渲染(因为不进入状态队列)
  • 获取DOM时,React会在DOM挂载后将元素赋值给ref.current
  • 三大应用场景:DOM操作、跨渲染状态、缓存上一次的值
// useRef: 存DOM/跨渲染变量
const inputRef = useRef(null)
useEffect(() => {
    inputRef.current.focus(); // 获取DOM并聚焦
}, []) ;
<Input ref={inputRef} />

useContext

  • React维护了一个Context上下文栈,组件渲染时会向上遍历栈,找到最近的Context.Provider,读取value并缓存,当Provider的value变化时,所有使用useContext获取该Context的组件都会重渲染
  • Context.Provider来确定数据共享范围
  • 通过value来分发内容
  • 在子组件中通过useContext来获取数据
const Context = createContext(null)
function StateFun() {
    const [num, setNum] = useState(1)
    return (
        <div>
            这是一个函数组件{ num }
            <Context.Provider value={num}>
                <Item1/>
                <Item2/>
            </Context.Provider>
        </div>
    )
}
function Item1(){
    const num = useContext(Context)
    return <div>子组件1{num}</div>
}
function Item2(){
    const num = useContext(Context)
    return <div>子组件2{num}</div>
}

5. 渲染优化类

useTransition/useDerredValue

  • React的优先级调度机制:将更新分为“紧急更新”(如输入框、点击)和非紧急更新(如大数据渲染)

useTransition

  • 标记一个状态更新为非紧急,React会优先处理紧急更新,等主线程空闲后再执行非紧急更新,期间页面保持响应
  • const [ispending, startTransition] = useTransition()
  • ispending: 布尔值,指示过渡状态是否进行中
  • startTransition:函数,用于包裹优先级状态更新
  • 高优先级:直接调用状态更新函数
  • 低优先级:用startTransition包裹状态更新
  • 可控制状态更新
    • 能够修改setState调用
    • 事件处理函数在当前组件内
  • 需要加载状态
    • ispending提供明确的加载反馈
    • 适合显示Loading指示器
  • 复杂更新逻辑
    • 多个状态需要同时更新
    • 需要精确控制更新时机
// 示例
import { useState, useTransition } form 'react'
function SearchPage() {
    const handleInputChange = (e) => {
        setInputValue(e.target.value)
    }
}

useDeferredValue

  • 基于useTransitin实现,对一个值创建延迟版本,紧急更新时先用旧值渲染,空闲后再用新值渲染,本质是值的优先级降级
  • const deferredValue = useEdfeeredValue(value)
  • 无法控制状态更新
    • 值来自props或第三方hooks
    • 父组件控住状态更新逻辑
  • 简单值延迟
    • 只需要延迟某个值的使用
    • 不需要复杂的状态管理
  • 渐进式优化
    • 最小代码改的
    • 快速应用到现有组件
// 示例
import { useState, useDeferredValue } from 'react'
function SearchPage(){
    const [query, setQuery] = useSState('')
    // 将query包装成延迟值
    const defeeredQuery = useDeferredValue(query)
    const handleInputChange = (e) => {
        setQuery(e.target.value)
    }
    // 判断是否处于过时状态
    const isState = query !== defeeredQuery
    return (
        <div>
            <input type='text' value={query} onChange={handleInputChange} />
            <div style={{ opacity: isStatle ? 0.5 : 1 }}>
                <SearchResultList query={defeeredQuery} />
            </div>
        </div>
    )
}

6. 工具类:AbortController

  • 是浏览器原生API(非React专属),核心是发布-订阅模式
  • 创建控制器生成一个AbortSignal信号
  • 将信号绑定到异步操作(如fetch),异步操作会监听信号状态
  • 调用controller.abort()时,信号触发中止事件,异步操作会立即中止并抛出AbortError
useEffect(() => {
    const controller = new AbotController()
    const fetchData = async () => {
        try{
            const reponse = await fetch(url, {signal: controller.signal})
        } catch (error) {
            if(error.name !== 'AbortError'){
                // 处理错误逻辑
            }
        }
    }
    fetchData()
    return () => controller.abort() // 中止操作
}, [dependencies])

7. 自定义Hooks

  • 自定义Hooks不是React提供的内置API,而是基于内置Hooks封装的复用逻辑
  • 本质是遵循命名规则的普通函数(必须用use开头)
  • 内部可以调用任意内置Hooks(React会通过调用栈识别Hooks归属的组件)
  • 每次调用自定义Hooks,其中的内置Hooks都会独立创建(状态隔离)
// 自定义hook:封装请求逻辑
function useRequest(url) {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        const controller = new AbortController();
        fetch(url, { signal: controller.signal })
        .then(res => res.json())
        .then(data => {
            setData(data);
            setLoading(false);
        });
        return () => controller.abort();
    }, [url]);
    
    return { data, loading };
}

// 组件中使用
const { data, loading } = useRequest('/api/data');

核心区别总览表

类别API核心定位关键特性
状态管理useState简单局部状态异步更新、语法简洁
状态管理useReducer复杂状态逻辑集中更新、纯函数reducer
副作用useEffect异步副作用绘制后执行、不阻塞
副作用useLayoutEffect异步副作用绘制前执行、阻塞
性能优化useMemo缓存计算值依赖变化才重新计算
性能优化useCallback缓存函数引用避免子组件无意义重渲染
性能优化React.memo缓存组件渲染结果浅比较props
引用/通信useRef持久化引用(DOM/变量)修改不触发重渲染
引用/通信useContext跨组件传智依赖Provider传递至
渲染优化useTransition标记非紧急更新优先级调度、不阻塞紧急操作
渲染优化useDeferredValue延迟值更新基于useTransition实现
工具AbortController取消异步操作浏览器原生、监听中止信号
逻辑复用自定义Hooks封装复用逻辑以use开头、可调用内置Hooks