ReactHooksTypeScript前端

React Hooks 完整指南:状态、副作用与 Context

用一篇文章理清 useState、useEffect、useReducer、useContext 和自定义 Hook 的职责、选择方法与常见错误,包含可验证的 TypeScript 示例。

·更新于 ·阅读约 16 分钟·计算中...
React Hooks 完整指南:状态、副作用与 Context

React Hooks 的关键不是记住更多 API,而是把问题分成四类:组件自己的简单状态用 useState;复杂状态转换用 useReducer;与 React 外部系统同步用 useEffect;跨越多层组件读取共享值用 useContext。重复逻辑再提取成自定义 Hook。

本文合并了本站原有的 useState、useEffect、useReducer、useContext、状态与副作用、自定义 Hooks 等零散文章。

目录

Hooks 的两条基本规则

Hooks 只能在 React 函数组件或自定义 Hook 的顶层调用:

function SearchPanel({ enabled }: { enabled: boolean }) {
  // 正确:每次渲染的 Hook 调用顺序一致
  const [query, setQuery] = useState("")

  if (!enabled) return null
  return <input value={query} onChange={(event) => setQuery(event.target.value)} />
}

不要在条件、循环、事件处理器或普通工具函数中调用 Hook。React 依靠稳定的调用顺序关联每个 Hook 的状态,项目中应启用 eslint-plugin-react-hooks 检查规则。

useState:组件自己的简单状态

适合布尔开关、输入值、选中项等相对独立的状态:

import { useState } from "react"

export function Counter() {
  const [count, setCount] = useState(0)

  return (
    <button onClick={() => setCount((current) => current + 1)}>
      点击 {count}    </button>
  )
}

新值依赖旧值时使用函数式更新。对象和数组不能直接修改,要创建新值:

setProfile((profile) => ({ ...profile, name: "Steven" }))
setTools((tools) => [...tools, "Raycast"])

以下数据通常不需要 state:

  • 能从 props 或其他 state 直接计算出的值。
  • 只在事件处理器中使用的临时变量。
  • 不参与渲染、修改后不需要更新界面的值;这类值可能更适合 useRef

useEffect:只处理外部同步

useEffect 用来让组件与 React 外部系统同步,例如浏览器 API、网络连接、定时器或第三方组件。它不是“组件渲染后执行任意代码”的通用容器。

import { useEffect, useState } from "react"

export function OnlineStatus() {
  const [online, setOnline] = useState(navigator.onLine)

  useEffect(() => {
    const update = () => setOnline(navigator.onLine)

    window.addEventListener("online", update)
    window.addEventListener("offline", update)

    return () => {
      window.removeEventListener("online", update)
      window.removeEventListener("offline", update)
    }
  }, [])

  return <span>{online ? "在线" : "离线"}</span>
}

Effect 的思考顺序是:需要同步哪个外部系统?如何开始同步?依赖变化或组件卸载时如何停止?如果找不到外部系统,往往不需要 Effect。

请求数据时防止竞态

useEffect(() => {
  const controller = new AbortController()

  async function load() {
    const response = await fetch(`/api/tools?q=${query}`, {
      signal: controller.signal,
    })
    const result = await response.json()
    setTools(result)
  }

  load().catch((error) => {
    if (error.name !== "AbortError") console.error(error)
  })

  return () => controller.abort()
}, [query])

在 Next.js 中,首屏数据优先在服务器组件或框架的数据层获取。客户端 Effect 更适合用户交互后请求、浏览器专属 API 和实时订阅。

常见 useEffect 错误

  • 漏写依赖,导致闭包读取旧值。
  • 为了绕过 ESLint 随意删除依赖。
  • 在 Effect 中计算本可在渲染阶段直接得到的数据。
  • 创建定时器、监听器或连接后没有清理。
  • Effect 更新某个依赖,又因为该依赖变化不断重复执行。

useReducer:集中管理复杂状态转换

当一个状态包含多个相关字段、更新动作很多,或需要明确记录“发生了什么”时,使用 reducer:

import { useReducer } from "react"

type State = { status: "idle" | "saving" | "saved"; error?: string }
type Action =
  | { type: "save_started" }
  | { type: "save_succeeded" }
  | { type: "save_failed"; message: string }

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "save_started":
      return { status: "saving" }
    case "save_succeeded":
      return { status: "saved" }
    case "save_failed":
      return { status: "idle", error: action.message }
    default:
      return state
  }
}

export function SaveButton() {
  const [state, dispatch] = useReducer(reducer, { status: "idle" })
  return (
    <button
      disabled={state.status === "saving"}
      onClick={() => dispatch({ type: "save_started" })}
    >
      {state.status === "saving" ? "保存中…" : "保存"}
    </button>
  )
}

Reducer 必须保持纯函数:不请求网络、不写 localStorage、不修改原对象。副作用仍在事件处理器、框架数据层或必要的 Effect 中完成。

useContext:传递共享值,不等于状态管理器

Context 解决的是逐层传递 props 的问题。主题、登录用户、语言和依赖注入通常适合 Context:

import { createContext, useContext, useState } from "react"

type Theme = "light" | "dark"
const ThemeContext = createContext<Theme | null>(null)

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme] = useState<Theme>("dark")
  return <ThemeContext value={theme}>{children}</ThemeContext>
}

export function useTheme() {
  const value = useContext(ThemeContext)
  if (value === null) throw new Error("useTheme 必须在 ThemeProvider 内使用")
  return value
}

Provider 的 value 改变时,读取该 Context 的组件会重新渲染。高频变化的大对象不要全部塞进一个 Context;可以拆分 Context,或在确有性能问题时稳定对象和函数引用。React 官方也强调,memo 不能阻止组件接收新的 Context 值。

自定义 Hook:复用逻辑,不复用状态实例

自定义 Hook 的名称以 use 开头,可组合其他 Hooks。每次调用都会获得独立状态,它复用的是状态逻辑:

import { useEffect, useState } from "react"

export function useDebouncedValue<T>(value: T, delay = 300) {
  const [debounced, setDebounced] = useState(value)

  useEffect(() => {
    const timer = window.setTimeout(() => setDebounced(value), delay)
    return () => window.clearTimeout(timer)
  }, [value, delay])

  return debounced
}

好的自定义 Hook 应表达具体能力,例如 useOnlineStatususeDebouncedValue。不要仅仅为了缩短组件,把一段只用一次且语义模糊的代码包装成 Hook。

如何选择 Hook

问题 首选
输入框、开关、当前选中项 useState
多字段状态有清晰动作和转换规则 useReducer
连接浏览器 API、订阅或第三方系统 useEffect
跨越多层组件提供主题、语言、用户 useContext
保存不参与渲染的可变值或 DOM 引用 useRef
多个组件重复相同的 Hook 组合逻辑 自定义 Hook

状态驱动动画时,React 管理“现在是什么状态”,Motion 管理“两个视觉状态如何过渡”。可以继续阅读 Motion for React 完整指南

发布前检查

  1. 是否把可计算数据重复存进 state?
  2. Effect 是否真的在同步外部系统?
  3. 每个订阅、监听器、定时器和请求是否有清理逻辑?
  4. Reducer 是否纯净,是否修改了原 state?
  5. Context 是否过大、变化是否过于频繁?
  6. 自定义 Hook 的名称能否直接说明用途?

参考资料

5 分钟

理解 React Portal:让你的组件跳出层级限制

在 React 中,组件默认是渲染在父组件的 DOM 树中的。比如你有一个父组件,它包含了多个子组件,每个子组件都渲染在父组件的 HTML 结构里。这种渲染方式是 React 的常规操作。然而,有时候我们需要让某些组件脱离父组件的 DOM 结构,单独渲染到其他位置。比如当我们使用

11 分钟

React 组件设计:组合、抽象与复用边界

从函数组件、props 和组合模式出发,说明什么时候值得抽象组件、如何避免过度封装,并给出受控组件、children、状态提升和自定义 Hook 的选择方法。

10 分钟

React Hydration Failed:原因、定位与修复

系统排查服务器 HTML 与客户端首次渲染不一致的问题,覆盖时间、随机数、浏览器 API、无效 HTML、客户端存储、第三方扩展和 suppressHydrationWarning 的边界。

订阅 FreeMac

每周精选:免费 Mac 软件评测、可信来源更新、替代方案和少折腾指南。