常用的Hooks

常用的Hooks

1. 状态管理:useState/useReducer

useState

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

useReducer

  • useState的替代方案,适合复杂状态逻辑或含多子值
go 复制代码
// 表单状态机(状态转换复杂)
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
  • 不提供依赖项,每次渲染后都执行

    • 可能导致不必要的性能开销
    javascript 复制代码
    useEffect(() => {
        console.log('组件已更新')
    })
  • 空依赖项数组[]:仅在初始渲染后执行一次

    • 初始化操作,一次性设置
    scss 复制代码
    useEffect(() => {
        fetchData() // 从服务器获取初始数据
    })
  • 包含依赖项的数组

    • 响应特定数据变化
    scss 复制代码
    useEffect(() ={
        fetchUserDate(userId).then(data => setUser(data))
    }, [userId]) // 依赖于userId

useEffect

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

useLayoutEffect

  • 渲染阶段(Render) -> 提交阶段(Commit,更新DOM) -> useLayoutEffect同步执行(阻塞浏览器绘制) -> 浏览器绘制(Paint),阻塞视觉
  • 使用DOM测量,布局调整,避免闪烁
javascript 复制代码
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

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

React.memo

  • 高阶组件(HOC),包装函数组件后,对传入的props做浅比较,只有props变化时组件才会渲染,否则复用上次的渲染结果
  • 函数props引用变化,导致memo失效
  • 最佳配合:当子组件memo包装,且父组件给子组件传递函数props时,必须有个useCallback缓存该函数,才能让memo真正生效
ini 复制代码
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比较好
javascript 复制代码
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操作、跨渲染状态、缓存上一次的值
scss 复制代码
// 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来获取数据
javascript 复制代码
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指示器
  • 复杂更新逻辑
    • 多个状态需要同时更新
    • 需要精确控制更新时机
javascript 复制代码
// 示例
import { useState, useTransition } form 'react'
function SearchPage() {
    const handleInputChange = (e) => {
        setInputValue(e.target.value)
    }
}

useDeferredValue

  • 基于useTransitin实现,对一个值创建延迟版本,紧急更新时先用旧值渲染,空闲后再用新值渲染,本质是值的优先级降级
  • const deferredValue = useEdfeeredValue(value)
  • 无法控制状态更新
    • 值来自props或第三方hooks
    • 父组件控住状态更新逻辑
  • 简单值延迟
    • 只需要延迟某个值的使用
    • 不需要复杂的状态管理
  • 渐进式优化
    • 最小代码改的
    • 快速应用到现有组件
javascript 复制代码
// 示例
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
scss 复制代码
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都会独立创建(状态隔离)
scss 复制代码
// 自定义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
相关推荐
天才熊猫君1 小时前
Vue Fragment 锚点机制
前端
米丘1 小时前
Git 常用操作命令
前端
星_离1 小时前
SSE—实时信息推送
前端
wuhen_n2 小时前
响应式探秘:ref vs reactive,我该选谁?
前端·javascript·vue.js
wuhen_n2 小时前
setup 的艺术:如何组织我们的组合式函数?
前端·javascript·vue.js
三翼鸟数字化技术团队2 小时前
前端架构演进与模块化设计实践
前端·架构
Moment2 小时前
Cursor 的 5 种指令方法比较,你最喜欢哪一种?
前端·后端·github
IT_陈寒2 小时前
Vite快得离谱?揭秘它比Webpack快10倍的5个核心原理
前端·人工智能·后端
明月_清风3 小时前
性能级目录同步:IntersectionObserver 实战
前端·javascript