Featured image of post React Hooks 完全指南:15 个核心 Hook 一网打尽

React Hooks 完全指南:15 个核心 Hook 一网打尽

useState / useEffect / useContext / useRef / useReducer / useCallback / useMemo / useLayoutEffect / useImperativeHandle / useDebugValue + React 18 的 useId / useTransition / useDeferredValue + React 19 的 useActionState / useFormStatus / useOptimistic——从状态、副作用到性能优化、SSR 一站式梳理。

为什么写这篇:React Hooks 2019-02 在 16.8 正式发布,5 年内彻底改变了 React 的写法。从 useState 到 React 19 的 useActionState,Hooks 已经从"新特性"变成"默认写法"。本文按"开箱即用频次"梳理 15 个核心 Hook,给出核心概念、最佳实践与常见坑。

适用读者:刚学 React Hooks 的同学;想系统梳理 18/19 新 Hook 的中级工程师;从 class 组件迁过来的老 React。

前置知识:理解 JSX、函数组件、props。

目录

  1. Hooks 设计哲学
  2. useState:状态管理基石
  3. useReducer:复杂状态的"action + reducer"模式
  4. useEffect:副作用与清理
  5. useLayoutEffect:DOM 更新同步完成后立即执行
  6. useContext:跨组件共享数据
  7. useRef:DOM 引用与持久化数据
  8. useImperativeHandle:自定义 ref 暴露的接口
  9. useMemo & useCallback:缓存计算结果与函数引用
  10. useDebugValue:自定义 Hook 在 DevTools 中的标签
  11. useId:跨服务端/客户端的唯一 ID
  12. useTransition:标记非紧急任务
  13. React 19 新 Hook 速览
  14. 自定义 Hook:组合已有 Hook 复用逻辑
  15. Hooks 三大规则

1. Hooks 设计哲学

Hooks 解决的核心问题:让函数组件拥有 class 组件的能力(state、生命周期、context),同时让代码更简洁、更好复用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// ❌ class 组件:逻辑散在生命周期里
class UserList extends React.Component {
  state = { users: [] }
  componentDidMount() {
    fetch('/api/users').then(r => r.json()).then(users => this.setState({ users }))
  }
  render() {
    return <ul>{this.state.users.map(u => <li>{u.name}</li>)}</ul>
  }
}

// ✅ 函数组件 + Hook:逻辑聚拢、可复用
function UserList() {
  const [users, setUsers] = useState([])
  useEffect(() => {
    fetch('/api/users').then(r => r.json()).then(setUsers)
  }, [])
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}

2. useState:状态管理基石

1
const [count, setCount] = useState(0)

2.1 初始值是函数(昂贵计算)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function HeavyComputationComponent() {
  const expensiveInitialValue = () => {
    let sum = 0
    for (let i = 0; i < 1_000_000_000; i++) sum += i
    return sum % 100
  }
  // 函数只在首次渲染执行一次
  const [value, setValue] = useState(expensiveInitialValue)
  return <div>{value}</div>
}

2.2 函数式更新

1
2
3
4
function handleClickMultipleUpdates() {
  setCount(prev => prev + 1)
  setCount(prev => prev + 1)   // 最终 count = 2(不是 1)
}

Why:React 18 的 setState 是批处理 + 异步的。连续调 3 次 setCount(c => c+1) 拿到的是"3 次累加后的 count"。

2.3 对象/数组不可变更新

1
2
3
4
5
6
7
const [user, setUser] = useState({ name: '张三', age: 20 })

// ✅ 正确:展开旧对象 + 改一个属性
setUser(prev => ({ ...prev, age: prev.age + 1 }))

const [items, setItems] = useState(['苹果', '香蕉'])
setItems(prev => [...prev, '橙子'])

直接修改原对象不会触发重渲染——React 用引用比较判断要不要更新。


3. useReducer:复杂状态的"action + reducer"模式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
const initialState = { count: 0, step: 1 }

function reducer(state, action) {
  switch (action.type) {
    case 'increment': return { ...state, count: state.count + state.step }
    case 'decrement': return { ...state, count: state.count - state.step }
    case 'setStep':   return { ...state, step: action.payload }
    default: throw new Error('unknown action')
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState)
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'setStep', payload: 5 })}>step=5</button>
    </>
  )
}

vs useState:state 之间有联动、依赖前一个 state、有多种"动作"时用 useReducer 更清晰。


4. useEffect:副作用与清理

1
2
3
4
5
6
7
8
9
useEffect(() => {
  // 副作用逻辑
  console.log('组件已渲染或依赖项已更新')

  return () => {
    // 可选:清理函数
    console.log('清理')
  }
}, [dep1, dep2])

4.1 三个常见用法

依赖项行为
不传每次渲染后都执行(通常不要
[]只在挂载/卸载时执行(一次)
[a, b]ab 变化时执行

4.2 清理函数:避免内存泄漏

1
2
3
4
useEffect(() => {
  const timer = setInterval(() => console.log('tick'), 1000)
  return () => clearInterval(timer)   // 卸载时清理
}, [])

常见需要清理的场景:定时器、事件监听、网络请求(AbortController)、WebSocket、订阅。

4.3 AbortController 取消请求

1
2
3
4
5
6
7
8
useEffect(() => {
  const controller = new AbortController()
  fetch('/api/data', { signal: controller.signal })
    .then(r => r.json())
    .then(setData)
    .catch(e => { if (e.name !== 'AbortError') console.error(e) })
  return () => controller.abort()
}, [])

5. useLayoutEffect:DOM 更新同步完成后立即执行

1
2
3
4
5
useLayoutEffect(() => {
  // 同步执行,浏览器还没绘制
  ref.current.style.opacity = '0'
  // 立即读取布局相关数据
}, [data])

vs useEffect

  • useEffect 异步,浏览器已绘制完才跑
  • useLayoutEffect 同步,DOM 更新完、浏览器重绘前立即跑

适用场景:读 DOM 布局(offsetHeight、scrollTop)、强制同步重排避免视觉抖动。

陷阱:服务端渲染(SSR)会发警告。建议优先用 useEffect只有当出现"渲染闪烁"时才换 useLayoutEffect


6. useContext:跨组件共享数据

解决 “props 透传"问题——避免一层层 <A><B><C>...</C></B></A> 手动传 props。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 1. 创建 Context
const ThemeContext = createContext({ theme: 'light', toggle: () => {} })

// 2. Provider 包裹
function App() {
  const [theme, setTheme] = useState('light')
  return (
    <ThemeContext.Provider value={{ theme, toggle: () => setTheme(t => t === 'light' ? 'dark' : 'light') }}>
      <Page />
    </ThemeContext.Provider>
  )
}

// 3. 子组件消费
function Button() {
  const { theme, toggle } = useContext(ThemeContext)
  return <button onClick={toggle}>{theme === 'light' ? '🌞' : '🌙'}</button>
}

:Context 的 value 每次渲染都是新对象,会让所有消费者重渲染。优化:用 useMemo 包装 value。

1
2
const value = useMemo(() => ({ theme, toggle }), [theme])
return <ThemeContext.Provider value={value}>...</ThemeContext.Provider>

7. useRef:DOM 引用与持久化数据

7.1 获取 DOM 引用

1
2
3
4
5
6
7
8
9
function TextInput() {
  const inputRef = useRef<HTMLInputElement>(null)

  useEffect(() => {
    inputRef.current?.focus()  // 自动聚焦
  }, [])

  return <input ref={inputRef} />
}

7.2 持久化数据(不触发重渲染)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Timer() {
  const [count, setCount] = useState(0)
  const intervalRef = useRef<number | null>(null)

  const start = () => {
    intervalRef.current = window.setInterval(() => {
      setCount(c => c + 1)
    }, 1000)
  }

  const stop = () => {
    if (intervalRef.current) clearInterval(intervalRef.current)
  }

  return (
    <>
      <p>{count}s</p>
      <button onClick={start}>开始</button>
      <button onClick={stop}>停止</button>
    </>
  )
}

vs useState:改 ref.current 不触发 重渲染——适合"我需要存个值,但改了不更新 UI”。


8. useImperativeHandle:自定义 ref 暴露的接口

配合 forwardRef 使用,让父组件通过 ref 访问子组件的特定方法(隐藏内部实现)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
const FancyInput = forwardRef((props, ref) => {
  const inputRef = useRef<HTMLInputElement>(null)

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

  return <input ref={inputRef} {...props} />
})

// 父组件
function Parent() {
  const ref = useRef<{ focus: () => void; clear: () => void }>(null)
  return (
    <>
      <FancyInput ref={ref} />
      <button onClick={() => ref.current?.focus()}>聚焦</button>
      <button onClick={() => ref.current?.clear()}>清空</button>
    </>
  )
}

Why:子组件自己决定暴露什么 API,父组件不需要知道内部 DOM 结构。


9. useMemo & useCallback:缓存计算结果与函数引用

9.1 useMemo:缓存"计算结果"

1
2
3
const expensiveValue = useMemo(() => {
  return data.reduce((sum, item) => sum + item.price * item.qty, 0)
}, [data])  // data 变才重算

9.2 useCallback:缓存"函数引用"

1
2
3
4
5
6
7
8
const handleClick = useCallback((id: string) => {
  setItems(items => items.filter(i => i.id !== id))
}, [])   // 函数永远不重创建

// 子组件配合 React.memo
const Child = memo(function Child({ onClick, value }) {
  return <button onClick={() => onClick(value)}>click</button>
})

三者联动useMemo 计算 + useCallback 传函数 + React.memo 阻止子组件重渲染——三件套只在大列表/复杂组件才有用。

常见误区:每个函数都包 useCallback——会拖慢性能(依赖比较也是开销)。只在该函数被传给 memo 化的子组件时才用。


10. useDebugValue:自定义 Hook 在 DevTools 中的标签

1
2
3
4
5
6
function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null)
  // ... 副作用逻辑
  useDebugValue(isOnline ? 'Online' : 'Offline')
  return isOnline
}

React DevTools → Components 面板里这个 Hook 会显示 Online / Offline,方便调试。

仅在 DevTools 中显示,生产环境不占成本(React 会自动移除)。


11. useId:跨服务端/客户端的唯一 ID

解决 SSR 的水合(hydration)问题——服务端和客户端的 ID 必须一致,否则 React 警告。

1
2
3
4
5
6
7
8
9
function PasswordField() {
  const id = useId()   // :r0:
  return (
    <>
      <label htmlFor={id}>Password</label>
      <input id={id} type="password" />
    </>
  )
}

vs 自增 IDMath.random() / Date.now() 在 SSR 下会不一致。


12. useTransition:标记非紧急任务

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Search({ data }) {
  const [isPending, startTransition] = useTransition()
  const [filter, setFilter] = useState('')

  const handleChange = (e) => {
    // 紧急任务:更新输入框(必须立即响应)
    setFilter(e.target.value)

    // 非紧急任务:过滤大列表(可被打断)
    startTransition(() => {
      setFilteredData(data.filter(d => d.name.includes(e.target.value)))
    })
  }

  return (
    <>
      <input value={filter} onChange={handleChange} />
      {isPending && <Spinner />}
      <List items={filteredData} />
    </>
  )
}

效果:用户输入时,输入框立即响应;过滤计算在后台进行(“transitioning”),避免页面卡顿。


13. React 19 新 Hook 速览

13.1 useActionState(替代 useState 表单提交)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
async function saveAction(prevState, formData) {
  await fetch('/api/save', { method: 'POST', body: formData })
  return { ok: true }
}

function Form() {
  const [state, formAction, isPending] = useActionState(saveAction, { ok: false })
  return (
    <form action={formAction}>
      <input name="title" />
      <button disabled={isPending}>{isPending ? '保存中...' : '保存'}</button>
      {state.ok && <p>已保存</p>}
    </form>
  )
}

省掉手写 useState({ loading, error, data }) 三个状态——React 自动管。

13.2 useFormStatus(读父表单的提交状态)

1
2
3
4
function SubmitButton() {
  const { pending, data, method } = useFormStatus()
  return <button disabled={pending}>{pending ? '...' : '提交'}</button>
}

13.3 useOptimistic(乐观更新)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function Like({ likes, isLiked }) {
  const [optimistic, setOptimistic] = useOptimistic(isLiked)
  return (
    <button onClick={async () => {
      setOptimistic(true)   // UI 立即显示已赞
      await fetch('/api/like', { method: 'POST' })
      // 接口成功后由父组件更新真实状态
    }}>
      {optimistic ? '❤️' : '🤍'} {likes}
    </button>
  )
}

Why:用户点"赞"后接口还没返回时,按钮已经变成红色——体感流畅 5 倍。


14. 自定义 Hook:组合已有 Hook 复用逻辑

自定义 Hook = 一个以 use 开头的函数,内部调用其他 Hook

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// useLocalStorage.ts
function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState<T>(() => {
    const stored = localStorage.getItem(key)
    return stored ? JSON.parse(stored) : initialValue
  })

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value))
  }, [key, value])

  return [value, setValue] as const
}

// 用法
function App() {
  const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light')
  return <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>{theme}</button>
}

常见自定义 HookuseFetch / useDebounce / useThrottle / usePagination / useModal / useWebSocket


15. Hooks 三大规则

  1. 只能在函数组件顶层调用——不能在 if/for/嵌套函数里
  2. 只能在 React 函数中调用——函数组件、自定义 Hook;不能在普通 JS 函数里
  3. useEffect 依赖项必须完整——配合 eslint-plugin-react-hooks 自动检查
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// ❌ 错误:条件调用 Hook
function Bad() {
  if (count > 0) {
    const [v, setV] = useState(0)   // 报错:Hook 数量不固定
  }
}

// ✅ 正确:永远在顶层调用
function Good() {
  const [v, setV] = useState(0)
  if (count > 0) {
    // 用 v 即可
  }
}

Why:React 用"调用顺序"识别每个 Hook(第一次是 useState、第二次是 useEffect…)。条件调用会破坏顺序,state 会错位。


小结

React Hooks 核心 15 个:

Hook用途React 版本
useState状态管理基石16.8+
useReducer复杂状态16.8+
useEffect副作用16.8+
useLayoutEffectDOM 同步副作用16.8+
useContext跨组件共享16.8+
useRefDOM 引用 + 持久化数据16.8+
useImperativeHandle配合 forwardRef 暴露 API16.8+
useMemo / useCallback性能优化16.8+
useDebugValueDevTools 标签16.8+
useIdSSR 唯一 ID18+
useTransition标记非紧急任务18+
useActionState表单状态管理19+
useFormStatus读表单状态19+
useOptimistic乐观更新19+
useDeferredValue延迟更新值18+

下一步:学完 Hooks 之后,进阶性能优化(React.memo / 虚拟列表 / 代码分割)、状态管理(Redux Toolkit / Zustand / Jotai)、数据请求(React Query / SWR)、渲染模式(Suspense / Streaming SSR / Server Components)。

参考资料

使用 Hugo 构建
主题 StackJimmy 设计