React Hooks 的关键不是记住更多 API,而是把问题分成四类:组件自己的简单状态用 useState;复杂状态转换用 useReducer;与 React 外部系统同步用 useEffect;跨越多层组件读取共享值用 useContext。重复逻辑再提取成自定义 Hook。
本文合并了本站原有的 useState、useEffect、useReducer、useContext、状态与副作用、自定义 Hooks 等零散文章。
目录
- Hooks 的两条基本规则
- useState:组件自己的简单状态
- useEffect:只处理外部同步
- useReducer:集中管理复杂状态转换
- useContext:传递共享值,不等于状态管理器
- 自定义 Hook:复用逻辑,不复用状态实例
- 如何选择 Hook
- 发布前检查
- 参考资料
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 应表达具体能力,例如 useOnlineStatus、useDebouncedValue。不要仅仅为了缩短组件,把一段只用一次且语义模糊的代码包装成 Hook。
如何选择 Hook
| 问题 | 首选 |
|---|---|
| 输入框、开关、当前选中项 | useState |
| 多字段状态有清晰动作和转换规则 | useReducer |
| 连接浏览器 API、订阅或第三方系统 | useEffect |
| 跨越多层组件提供主题、语言、用户 | useContext |
| 保存不参与渲染的可变值或 DOM 引用 | useRef |
| 多个组件重复相同的 Hook 组合逻辑 | 自定义 Hook |
状态驱动动画时,React 管理“现在是什么状态”,Motion 管理“两个视觉状态如何过渡”。可以继续阅读 Motion for React 完整指南。
发布前检查
- 是否把可计算数据重复存进 state?
- Effect 是否真的在同步外部系统?
- 每个订阅、监听器、定时器和请求是否有清理逻辑?
- Reducer 是否纯净,是否修改了原 state?
- Context 是否过大、变化是否过于频繁?
- 自定义 Hook 的名称能否直接说明用途?
参考资料
继续阅读
理解 React Portal:让你的组件跳出层级限制
在 React 中,组件默认是渲染在父组件的 DOM 树中的。比如你有一个父组件,它包含了多个子组件,每个子组件都渲染在父组件的 HTML 结构里。这种渲染方式是 React 的常规操作。然而,有时候我们需要让某些组件脱离父组件的 DOM 结构,单独渲染到其他位置。比如当我们使用
11 分钟React 组件设计:组合、抽象与复用边界
从函数组件、props 和组合模式出发,说明什么时候值得抽象组件、如何避免过度封装,并给出受控组件、children、状态提升和自定义 Hook 的选择方法。
10 分钟React Hydration Failed:原因、定位与修复
系统排查服务器 HTML 与客户端首次渲染不一致的问题,覆盖时间、随机数、浏览器 API、无效 HTML、客户端存储、第三方扩展和 suppressHydrationWarning 的边界。
订阅 FreeMac
每周精选:免费 Mac 软件评测、可信来源更新、替代方案和少折腾指南。