React Hooks 完整指南

系统整理 React 常用 Hooks 的用法与实战示例,涵盖状态管理、副作用处理、性能优化、DOM 操作等维度,代码与注释完整保留。


一、状态管理

1. useState

useState 是 React 中最基础的状态管理 Hook,用于在函数组件中添加状态。

核心要点:

  • 普通形式const [state, setState] = useState(initialValue)
  • 函数形式const [state, setState] = useState(() => initialValue),适用于初始值需要计算的场景
  • 批量更新 :React 18 中所有更新皆为异步批量处理,多次 setState 会合并
  • 函数式更新setState(prev => prev + 1),基于上一次状态计算新值,不会被合并
tsx 复制代码
import { Button, Flex } from 'antd';
import React, { useState } from 'react';
import { flushSync } from 'react-dom';

const UseStateComponent: React.FC = () => {
  const [count, setCount] = useState<number>(0); // 普通

  // set 属于异步更新 - 即在函数中多次使用 set 会合并操作;
  // const [count, setCount] = useState<number>(() => 0) // 函数式

  // + 1 在 react18之前合成事件为异步更新,原生事件是同步更新
  const handleClick = () => {
    setCount(count + 1);
    setCount(count + 1);
  };

  // + 1 但在 react18 皆为异步更新
  const handleTimeoutClick = () => {
    setTimeout(() => setCount(count + 1));
    setTimeout(() => setCount(count + 1));
  };

  // +2 可以使用函数形式;函数无法合并,即每次返回上一次周期的值;
  const handleFucClick = () => {
    setCount((count) => count + 1);
    setCount((count) => count + 1);
  };

  // +2 强制同步更新
  const handleSyncClick = () => {
    flushSync(() => setCount((count) => count + 1));
    flushSync(() => setCount((count) => count + 1));
  };

  return (
    <Flex gap={5} vertical>
      <p>{count}</p>
      <Flex gap={5}>
        <Button onClick={handleClick}>+ 1</Button>
        <Button onClick={handleTimeoutClick}>setTimeout + 1</Button>
        <Button onClick={handleFucClick}>func + 2</Button>
        <Button onClick={handleSyncClick}>flushSync + 2</Button>
      </Flex>
    </Flex>
  );
};
export default UseStateComponent;

四种更新方式对比:

方式 行为 结果
handleClick 两次 setCount(count + 1) +1(合并)
handleTimeoutClick setTimeout 中两次 set React 18 仍为 +1(合并)
handleFucClick 函数式更新 +2(不合并)
handleSyncClick flushSync 强制同步 +2(立即执行)

2. useReducer

useReduceruseState 的加强版,适用于状态逻辑复杂 、或下一个状态依赖前一个状态的场景。

核心要点:

  • 使用 reducer 函数集中管理状态变更逻辑
  • 通过 dispatch 派发 action 来触发状态更新
  • 代码结构更清晰,便于测试和维护
tsx 复制代码
import { Button, Typography } from 'antd'
import { useReducer } from 'react'

const initState = { count: 0 }
// 相当于 useState 加大版,用于处理更庞大且复杂的数据
const reducer = (state = initState, action: any) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 }
    case 'decrement':
      return { count: state.count + -1 }
    default:
      return state
  }
}

const UseReducerComponent = () => {
  const [state, dispatch] = useReducer(reducer, initState)
  return (
    <Typography>
      <Typography.Text>{state.count}</Typography.Text>
      <Button onClick={() => dispatch({ type: 'increment' })}>+</Button>
      <Button onClick={() => dispatch({ type: 'decrement' })}>-</Button>
    </Typography>
  )
}

export default UseReducerComponent

适用场景:

  • 状态对象包含多个子属性
  • 状态更新逻辑复杂(不止简单的 setXxx
  • 需要复用状态逻辑到多个组件

useState vs useReducer:

维度 useState useReducer
复杂度 简单状态 复杂状态逻辑
可读性 简单场景清晰 复杂场景更清晰
复用性 需自定义 Hook reducer 可独立复用
调试 直接查看 state 通过 action 追溯变更

二、副作用处理

3. useEffect

useEffect 是 React 中最常用的副作用 Hook,在组件渲染到屏幕之后执行。

核心要点:

  • 无依赖数组 :每次渲染后都执行(DidMount + DidUpdate
  • 空依赖数组 [] :只在组件挂载时执行(模拟 componentDidMount
  • 有依赖数组 [dep] :挂载时 + 依赖变化时执行(模拟 DidMount + 关联 DidUpdate
  • 返回清理函数 :组件卸载时执行(模拟 componentWillUnmount,但不完全等同)
tsx 复制代码
import { Button } from 'antd'
import React, { useEffect, useState } from 'react'

const UseEffectComponent: React.FC = () => {
  const [count, setCount] = useState(0)

  // 没有依赖时: DidMount + DidUpdate
  // 组件初始化和 props 更新都会执行;
  useEffect(() => {
    console.log('没有任何依赖')
  })

  // 依赖为空值相当于 DidMount;
  useEffect(() => {
    console.log('依赖为空值')
  }, [])

  // 存在依赖时,相当于 DidMount + 关联依赖 DidUpdate ;
  useEffect(() => {
    console.log('存在依赖')
  }, [count])

  // return 相当于模拟willUnMount但不等于;
  useEffect(() => {
    console.log('开始')
    return () => {
      console.log('结束')
    }
  }, [])

  return <Button onClick={() => setCount(count + 1)}>点击</Button>
}
export default UseEffectComponent

四种依赖模式对比:

依赖形式 执行时机 用途
[] 每次渲染后 监听所有变化
[] 仅挂载时 初始化操作(如请求数据)
[dep] 挂载 + 依赖变化 依赖特定状态/属性
return fn 卸载时(或依赖变化前) 清理副作用(取消订阅、清除定时器等)

4. useLayoutEffect

useLayoutEffectuseEffect 签名相同,但执行时机不同:在浏览器绘制之前同步执行

核心要点:

  • 执行时机:DOM 变更后、浏览器绘制前同步执行
  • 适用场景:需要读取/修改 DOM 布局(如测量元素尺寸、位置)
  • 注意:同步执行会阻塞浏览器绘制,谨慎使用,避免性能问题
tsx 复制代码
import React, { useLayoutEffect, useRef, useState } from 'react';

/**
 * useLayoutEffect 会在浏览器绘制前同步执行,适合在这里订阅布局相关的外部系统(如 ResizeObserver)。
 * 在 ResizeObserver 回调里 setState,属于「外部系统通知再更新」,避免在 effect 主体内同步 setState。
 */
const UseLayoutEffectComponent: React.FC = () => {
  const textRef = useRef<HTMLDivElement>(null);
  const [width, setWidth] = useState(0);

  useLayoutEffect(() => {
    const el = textRef.current;
    if (!el) return;

    const update = () => {
      setWidth(Math.round(el.getBoundingClientRect().width));
    };

    const ro = new ResizeObserver(update);
    ro.observe(el);

    return () => {
      ro.disconnect();
    };
  }, []);

  return (
    <div>
      <div ref={textRef} style={{ display: 'inline-block', fontWeight: 600 }}>
        useLayoutEffect 读取真实宽度
      </div>
      <p>当前宽度:{width}px</p>
    </div>
  );
};

export default UseLayoutEffectComponent;

useEffect vs useLayoutEffect:

维度 useEffect useLayoutEffect
执行时机 绘制后异步执行 绘制前同步执行
阻塞渲染 是(可能导致闪烁减少但性能下降)
适用场景 大多数副作用(请求、事件订阅等) DOM 测量、布局调整
视觉闪烁 可能出现 避免布局闪烁

💡 建议 :优先使用 useEffect,只有当出现布局闪烁问题时再考虑 useLayoutEffect


三、性能优化

5. useMemo

useMemo 用于缓存计算结果 ,只有当依赖项发生变化时才重新计算。类似于 Vue 中的 computed

核心要点:

  • 缓存昂贵的计算结果,避免每次渲染都重新计算
  • 第二个参数为依赖数组,只有依赖变化时才重新执行
  • 不要滥用,简单计算无需使用
tsx 复制代码
import { Button } from 'antd'
import React, { useMemo, useState } from 'react'
const list = [1, 3, 5, 7, 9]
/**
 * 当组件中的某一个值发生改变时,如果存在需要计算的值会重新进行计算;
 * 而 useMemo 可以缓存计算结果(类似 vue computed),只有在依赖项发生变化时重新计算
 */
const UseMemoComponent: React.FC = () => {
  const [count, setCount] = useState(0)

  // 每次点击按钮(setCount)会重新渲染此函数
  const totalNormal = () => {
    console.log('totalNormal')
    return list.reduce((prev, cur) => prev + cur)
  }

  // 类似 computed,有缓存,[] 关联属性才会重新执行
  const total = useMemo(() => {
    console.log('useMemo')
    return list.reduce((prev, cur) => prev + cur)
  }, [])

  return (
    <div>
      <p>{count}</p>
      <p>normal:{totalNormal()}</p>
      <p>useMemo:{total}</p>
      <Button onClick={() => setCount(count + 1)} type="primary">
        点击
      </Button>
    </div>
  )
}
export default UseMemoComponent

对比:

方式 行为 输出
totalNormal() 每次渲染都重新计算 25,但每次都执行 console.log
useMemo 依赖不变时返回缓存值 25,只在依赖变化时执行计算

6. useCallback

useCallback 用于缓存函数引用 ,配合 React.memo 使用可以避免子组件不必要的重渲染。

核心要点:

  • 缓存函数本身(而非执行结果)
  • 父组件传入子组件的函数 props 时,使用 useCallback 避免子组件因函数引用变化而重渲染
  • 需配合 memo 包裹子组件才能生效
  • 注意闭包陷阱:依赖未正确声明时,函数内可能捕获旧值
tsx 复制代码
import { Button, Card, Col, Row, Typography } from 'antd'
import React, { memo, useCallback, useEffect, useState } from 'react'
import type { ChangeEvent } from 'react'

interface ChildProps {
  onchange: (e: ChangeEvent<HTMLInputElement>) => void
}

/**
 * 左侧:useCallback + memo 缓存函数示例
 * 父组件传入子组件函数;当父组件发生变化,子组件也会重新渲染,将函数缓存配合 memo 使用;
 * 使用 memo 进行包裹缓存组件,相当于 PureComponent
 */
const MemoCacheDemo: React.FC = () => {
  const [count, setCount] = useState(0)

  // 点击子组件不会重新渲染
  const onchange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value)
  }, [])

  return (
    <Card title="useCallback + memo 缓存函数" bordered={false}>
      <Typography.Title level={4}>{count}</Typography.Title>
      <Button onClick={() => setCount(count + 1)}>+1</Button>
      <div style={{ marginTop: 16 }}>
        <Child onchange={onchange} />
      </div>
    </Card>
  )
}

const Child = memo<ChildProps>(({ onchange }) => {
  console.log('子组件渲染~~')
  return <input type="text" onChange={onchange} />
})

/**
 * 右侧:闭包陷阱示例
 * 如果版本是 v17 这是一个有问题的自定义钩子
 */
const ClosureTrapDemo: React.FC = () => {
  const [count, setCount] = useState(0)

  const handleClick = useCallback(() => {
    console.log(count) // 第一次是 0 如果 count 改变,handleClick 会保留旧值
  }, [count])

  useEffect(() => {
    setTimeout(() => {
      setCount((prev) => prev + 1)
    }, 1000)
  }, [])

  return (
    <Card title="闭包陷阱" bordered={false}>
      <Typography.Title level={4}>当前计数:{count}</Typography.Title>
      <Button onClick={handleClick}>点击计数(查看控制台)</Button>
    </Card>
  )
}

const UseCallbackComponent: React.FC = () => {
  return (
    <Row gutter={24}>
      <Col span={12}>
        <MemoCacheDemo />
      </Col>
      <Col span={12}>
        <ClosureTrapDemo />
      </Col>
    </Row>
  )
}

export default UseCallbackComponent

两个示例说明:

示例 说明
MemoCacheDemo useCallback + memo 配合,父组件状态更新不会触发子组件重新渲染
ClosureTrapDemo 闭包陷阱演示,useCallback 的依赖数组未正确维护时,函数内捕获的是旧值

7. useTransition

useTransition 用于将低优先级状态更新标记为"过渡",让高优先级更新(如用户输入)优先响应,避免界面卡顿。

核心要点:

  • startTransition:将状态更新包裹为过渡更新
  • isPending:布尔值,表示过渡是否进行中,可用于展示 loading 状态
  • 适用于:大量数据过滤、列表渲染等耗时操作
tsx 复制代码
import { Input, Spin } from 'antd'
import React, { useState, useTransition } from 'react'

/**
 * 用于在处理异步操作时提供平滑的过渡效果;
 * 可以帮助你在组件的状态变化时,以动画的方式过渡到新的状态。
 */
const UseTransitionComponent: React.FC = () => {
  const [list, setList] = useState<Array<number | string>>([])
  const [isPending, startTransition] = useTransition() // 更新过程

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const arr = new Array(5).fill(e.target.value)
    startTransition(() => {
      setList([...list, ...arr])
    })
  }
  return (
    <>
      <Input type="text" onChange={handleChange} />
      <>
        {isPending ? (
          <Spin>loading...</Spin>
        ) : (
          list.map((item, i) => {
            return <b key={i}>{item}</b>
          })
        )}
      </>
    </>
  )
}
export default UseTransitionComponent

8. useDeferredValue

useDeferredValue 用于将某个状态值延迟更新,让 React 优先渲染紧急更新(如用户输入),再处理基于该值的昂贵计算/渲染。

核心要点:

  • 接收一个值,返回该值的"延迟版本"
  • 当紧急更新(如输入)发生时,先使用旧值渲染,稍后再用新值重新渲染
  • 适用于:搜索过滤、大数据列表等场景
  • useTransition 的区别:useDeferredValue 作用于useTransition 作用于状态更新函数
tsx 复制代码
import { Input } from 'antd'
import React, { useDeferredValue, useMemo, useState } from 'react'

const mockData = Array.from({ length: 800 }, (_, i) => `React Hook Item ${i + 1}`)

/**
 * useDeferredValue:把高开销渲染延后,优先保证输入框流畅。
 */
const UseDeferredValueComponent: React.FC = () => {
  const [keyword, setKeyword] = useState('')
  const deferredKeyword = useDeferredValue(keyword)

  const result = useMemo(() => {
    const lowCaseKeyword = deferredKeyword.trim().toLowerCase()
    if (!lowCaseKeyword) return mockData.slice(0, 30)
    return mockData.filter((item) => item.toLowerCase().includes(lowCaseKeyword)).slice(0, 50)
  }, [deferredKeyword])

  return (
    <div>
      <Input
        placeholder="输入关键字试试(输入仍然会比较顺滑)"
        value={keyword}
        onChange={(e) => setKeyword(e.target.value)}
        style={{ maxWidth: 320, marginBottom: 12 }}
      />
      <div>命中数量:{result.length}</div>
      <div style={{ maxHeight: 200, overflow: 'auto', marginTop: 8 }}>
        {result.map((item) => (
          <div key={item}>{item}</div>
        ))}
      </div>
    </div>
  )
}

export default UseDeferredValueComponent

useTransition vs useDeferredValue:

维度 useTransition useDeferredValue
操作对象 状态更新函数 状态值
使用方式 startTransition(() => setXxx(...)) const deferred = useDeferredValue(value)
适用场景 主动控制哪些更新是过渡 将某个 props/state 延迟传递给子组件
获取 pending 状态 isPending 无(可用 deferred !== value 判断)

性能优化 Hooks 对比总结:

Hook 缓存对象 解决什么问题
useMemo 计算结果 避免昂贵计算重复执行
useCallback 函数引用 避免子组件因函数引用变化而重渲染
useTransition 状态更新 让紧急更新优先,避免界面卡顿
useDeferredValue 状态值 延迟非紧急渲染,保证输入流畅

四、Refs 与 DOM 操作

9. useRef

useRef 用于在函数组件中获取对 DOM 元素或任意可变值的引用,其 .current 属性在组件重新渲染时保持不变。

核心要点:

  • 返回一个可变的 ref 对象,.current 属性被初始化为传入的参数
  • ref 的变更不会触发组件重新渲染
  • 常见用途:获取 DOM 元素、保存上一次状态/属性、保存定时器 ID 等
tsx 复制代码
import React, { useEffect, useRef } from 'react'

// 获取真实 dom 元素
const UseRefComponent: React.FC = () => {
  const userRef = useRef<HTMLInputElement>(null)
  useEffect(() => {
    console.log(userRef.current)
    userRef.current?.focus() // 获取焦点
  }, [])
  return <input type="text" ref={userRef} />
}
export default UseRefComponent

常见用途:

用途 示例
获取 DOM const ref = useRef<HTMLInputElement>(null)
自动聚焦 ref.current?.focus()
保存上一次的值 const prevValue = useRef(value)
保存定时器/订阅 ID const timerRef = useRef<NodeJS.Timeout>()

10. useImperativeHandle

useImperativeHandle 用于自定义通过 ref 暴露给父组件的实例值。通常与 forwardRef 配合使用。

核心要点:

  • 配合 forwardRef 将子组件的 DOM 或自定义方法暴露给父组件
  • 父组件通过 ref 调用子组件的方法(如 focus()clear()
  • 避免了直接暴露整个 DOM 节点,只暴露必要的 API
tsx 复制代码
import { Button } from 'antd'
import React, { forwardRef, useImperativeHandle, useRef } from 'react'

interface FocusInputRef {
  focus: () => void
  clear: () => void
}

const FocusInput = forwardRef<FocusInputRef>((_, ref) => {
  const innerRef = useRef<HTMLInputElement>(null)

  useImperativeHandle(ref, () => ({
    focus: () => innerRef.current?.focus(),
    clear: () => {
      if (innerRef.current) innerRef.current.value = ''
    },
  }))

  return (
    <input
      ref={innerRef}
      placeholder="父组件通过 ref 调用 focus/clear"
      style={{ width: 320, height: 32, padding: '4px 8px' }}
    />
  )
})

FocusInput.displayName = 'FocusInput'

const UseImperativeHandleComponent: React.FC = () => {
  const focusInputRef = useRef<FocusInputRef>(null)

  return (
    <div>
      <FocusInput ref={focusInputRef} />
      <div style={{ marginTop: 12, display: 'flex', gap: 8 }}>
        <Button type="primary" onClick={() => focusInputRef.current?.focus()}>
          聚焦输入框
        </Button>
        <Button onClick={() => focusInputRef.current?.clear()}>清空内容</Button>
      </div>
    </div>
  )
}

export default UseImperativeHandleComponent

工作流程:

scss 复制代码
父组件                          子组件 (FocusInput)
  │                                  │
  ├─── ref={focusInputRef} ─────────>│
  │                                  ├── forwardRef 接收 ref
  │                                  ├── useImperativeHandle 定义暴露的 API
  │                                  │   ├── focus()
  │                                  │   └── clear()
  │                                  └── innerRef 绑定真实 <input>
  │                                  │
  ├─── ref.current.focus() ────────>│ 调用暴露的方法
  │                                  └── 实际操作 innerRef.current

注意事项:

  • 必须配合 forwardRef 使用,否则父组件无法传递 ref
  • 建议给 forwarded 组件设置 displayName,便于调试
  • 优先使用 props 进行数据流传递,useImperativeHandle 是命令式操作的补充

useRef vs useImperativeHandle:

维度 useRef useImperativeHandle
作用对象 当前组件内的 DOM/值 子组件暴露的 API
配合使用 独立使用 必须配合 forwardRef
数据流 单向(自己用) 父子组件命令式通信
典型场景 聚焦、测量 DOM 封装表单组件、封装播放器组件

五、其他

11. useId

useId 用于生成稳定且唯一 的 id,主要用于无障碍访问(a11y)场景中 label 与表单元素的关联。

核心要点:

  • 生成在服务端和客户端之间保持稳定的唯一 ID
  • 避免手写 id 可能导致的 SSR 水合(hydration)不匹配问题
  • 适用于:<label htmlFor><input id> 的绑定
tsx 复制代码
import { Input } from 'antd'
import React, { useId } from 'react'

/**
 * useId:生成稳定且唯一的 id,适合表单标签关联。
 */
const UseIdComponent: React.FC = () => {
  const inputId = useId()

  return (
    <div>
      <label htmlFor={inputId} style={{ display: 'block', marginBottom: 8 }}>
        用户名(label 与 input 绑定)
      </label>
      <Input id={inputId} placeholder="请输入用户名" style={{ maxWidth: 280 }} />
      <p style={{ marginTop: 8, color: '#666' }}>当前 id: {inputId}</p>
    </div>
  )
}

export default UseIdComponent

为什么不用 Math.random() 或自增 ID?

方式 问题
Math.random() SSR 时服务端和客户端生成的 id 不一致,导致水合失败
全局自增计数器 多组件实例、并发渲染下可能冲突
useId React 内部保证唯一性和跨端一致性 ✅

生成的 id 格式示例:

makefile 复制代码
:rc1:
:rc2:
:rc3:

格式可能随 React 版本变化,不要依赖其具体形式,只需保证唯一性即可。


整理自 @src/pages/react-hooks/,代码未删减,完整保留原始注释与实现。

相关推荐
假如让我当三天老蒯3 小时前
State和Props区别和左右(自学用)
前端·react.js
夜雪闻竹4 小时前
React Query + REST API 最佳实践
前端·react.js·前端框架
戈德斯文5 小时前
我做了一面互联网摸鱼墙:从无限 Canvas 到本地生产环境
react.js·canvas·next.js
vim怎么退出1 天前
Dive into React——Hooks 原理
react.js·源码阅读
光影少年1 天前
react的useMemo 如何优化?
前端·react.js·掘金·金石计划
YFF菲菲兔1 天前
React 核心流程总述
react.js
光影少年1 天前
react状态管理
前端·react.js·前端框架
珎珎啊1 天前
React 和 Vue 3的区别
前端·vue.js·react.js
Bigger1 天前
mini-cc 终端 UI:用 React 写 CLI 是什么体验
前端·react.js·ai编程