Skip to content
第 9 章 前端 ⏱ 10 分钟阅读

第 9 章:Hooks 基础 ​

学习目标 ​

  • 理解 Hooks 是什么、解决了什么问题
  • 掌握 Hooks 的 4 条使用规则
  • 熟悉 React 18 内置的所有常用 Hooks
  • 避开 Hooks 的 3 个常见坑

一、Hooks 是什么? ​

Hooks 是 React 16.8 引入的特性,让你在函数组件里"挂钩"状态和生命周期。以前只有 class 组件能用 state 和生命周期。

tsx
// class 写法(旧)
class Counter extends React.Component {
  state = { count: 0 };
  render() {
    return <button onClick={() => this.setState({ count: this.state.count + 1 })}>{this.state.count}</button>;
  }
}

// hooks 写法(新)
function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

Hooks 优势:

  • 没有 this 烦恼
  • 逻辑复用方便(自定义 Hook)
  • 组件更扁平
  • 函数组件 + Hooks 是当前主流

二、React 内置 Hooks 一览 ​

Hook作用
useState组件 state
useEffect副作用(数据获取、订阅、DOM 操作)
useContext跨组件共享数据
useReducer复杂 state 管理
useRef保留值/DOM 引用
useMemo缓存计算结果
useCallback缓存函数引用
useLayoutEffect同步版 useEffect
useImperativeHandle自定义 ref 暴露的方法
useDebugValueDevTools 显示调试信息
useId生成稳定唯一 ID
useTransition标记非紧急更新(并发)
useDeferredValue延迟更新值
useSyncExternalStore订阅外部 store

三、Hooks 的 4 条铁律 ​

  1. 只在顶层调用(不要在 if/for/函数里调用)
  2. 只在 React 函数里调用(组件 / 自定义 Hook)
  3. 命名以 use 开头(自定义 Hook)
  4. 依赖数组要写全(配合 ESLint 检查)
tsx
// ❌ 错误:if 里调用 hook
function Bad({ cond }: { cond: boolean }) {
  if (cond) {
    const [count, setCount] = useState(0);   // ⚠️ 报错!
  }
}

// ✅ 正确:无条件调用
function Good({ cond }: { cond: boolean }) {
  const [count, setCount] = useState(0);     // 永远执行
  if (!cond) return null;
  return <div>{count}</div>;
}

⚠️ 坑 1:条件调用 hook 会导致 hooks 数量不稳定,React 内部按调用顺序存储 state,数量变了 state 全乱套。

四、为什么要顶层调用? ​

React 内部用链表按调用顺序存 state:

tsx
function Comp() {
  const [a, setA] = useState('A');   // 第 1 个 hook
  const [b, setB] = useState('B');   // 第 2 个 hook
  return ...;
}

如果某次渲染跳过了 useState('A'),那 setB 实际拿到的是 A 的 setter,数据全错乱。

五、为什么只能在 React 函数里? ​

Hooks 依赖 React 的内部机制(当前的 Fiber 节点、调用栈),普通 JS 函数调用 hook 会找不到上下文而报错。

tsx
// ❌ 普通工具函数里不能 hook
function myUtil() {
  const [x, setX] = useState(0);   // ⚠️ 报错!
}

// ✅ 自定义 Hook 里调用是合法的
function useMyState(initial: number) {
  const [x, setX] = useState(initial);   // OK
  return [x, setX];
}

⚠️ 坑 2:ESLint 配置要开 react-hooks/rules-of-hooks,强制顶层/函数组件调用。

六、自定义 Hook 是什么? ​

以 use 开头的函数,内部调用其他 Hook,封装可复用逻辑:

tsx
function useLocalStorage<T>(key: string, initial: T) {
  const [value, setValue] = useState<T>(() => {
    const saved = localStorage.getItem(key);
    return saved ? JSON.parse(saved) : initial;
  });

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

  return [value, setValue] as const;
}

// 使用
function Comp() {
  const [name, setName] = useLocalStorage('name', '游客');
  return <input value={name} onChange={e => setName(e.target.value)} />;
}

⚠️ 坑 3:自定义 Hook 本质是普通函数,React 不识别它,靠约定(命名 + 调用其他 Hook)让它"成为"Hook。

七、React 18 新 Hooks 速览 ​

useTransition ​

React 18 的"优先级调度"Hook——把更新拆成紧急和不紧急,不紧急的可以让步给紧急的(用户输入不卡)。

核心问题:大列表搜索卡顿

tsx
function Search() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])

  // 1 万条数据 filter,可能要 200ms
  function handleChange(e) {
    setQuery(e.target.value)
    setResults(filterList(e.target.value))  // ← 这一步卡 200ms
  }
}

后果:输入框要等 200ms 才更新,用户以为"没打上",越打越多。

用 useTransition 解决:

tsx
const [isPending, startTransition] = useTransition()

function handleChange(e: ChangeEvent<HTMLInputElement>) {
  setQuery(e.target.value)              // 紧急:输入框立即更新
  startTransition(() => {
    setResults(filterList(e.target.value)) // 非紧急:可中断、可延迟
  })
}

结果:

  • 输入框立即响应(紧急优先)
  • 列表慢慢算,不算完也不卡 UI
  • isPending === true 期间可以显示 loading
概念含义
setQuery紧急更新——必须立即反映
startTransition非紧急更新——可中断、可延迟、可让步
isPending非紧急更新还没算完就是 true

实战用法:加 loading 提示

tsx
function Search() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])
  const [isPending, startTransition] = useTransition()

  function handleChange(e) {
    setQuery(e.target.value)
    startTransition(() => {
      setResults(filterList(e.target.value))
    })
  }

  return (
    <div>
      <input value={query} onChange={handleChange} />
      {isPending && <Spinner />}
      <List items={results} />
    </div>
  )
}

useTransition vs useDeferredValue ​

tsx
// useTransition:包更新动作(主动控制)
startTransition(() => {
  setResults(filterList(e.target.value))
})

// useDeferredValue:包值(值延迟一下)
const deferredQuery = useDeferredValue(query)
const results = filterList(deferredQuery)
useTransitionuseDeferredValue
包啥更新动作(setX)值(const d = useDeferredValue(v))
主动权在你在 React 自动延迟
适用场景搜索/过滤大列表、tab 切换input 实时派生数据

啥时候用 ​

场景用啥
搜索/过滤大列表useTransition
切换 tab 渲染重组件useTransition
实时搜索(input + 派生值)useDeferredValue
普通小数据更新不用,直接 setState

💡 useTransition = "紧急/不紧急"开关——setX 直接更新 = 紧急;startTransition(() => setX(...)) = 不紧急,可以让步。大列表/重渲染才用,小数据别瞎包。

useDeferredValue ​

tsx
const [query, setQuery] = useState('');
const deferred = useDeferredValue(query);            // 延迟一帧更新

// 用 deferred 做重型计算,输入仍流畅
const filtered = bigList.filter(x => x.includes(deferred));

为啥 filter 不用 query?——query 同步跑 filter,输入框跟着阻塞;deferred 让 filter 慢一拍,输入框立即响应。

query    → 输入框(必须用最新的,不能延迟)
deferred → 重计算(用旧值,让 filter 慢慢跑)

为啥 deferred 能用"旧值"?——中间会短暂"旧结果",但只持续一帧(16ms 左右),用户基本看不出来:

时刻querydeferred列表内容
0 ms""""filter("") = [] ✅
输入 "a""a"""filter("") = [] ⚠️ 短暂旧
66 ms"a""a"filter("a") = [...] ✅ 跟上

取舍:输入永远流畅 > 列表短暂旧 1 帧——用户眼睛盯着输入框,没盯着列表,延迟 1 帧无感。

啥时候不能用 deferred(数据必须实时准):

tsx
// ❌ 金额/状态/计数 → 必须用 query,延迟会算错钱
const total = orders.filter(o => o.includes(query)).reduce((s, o) => s + o.amount, 0)

// ✅ 搜索/过滤/排序 → 可以用 deferred,旧 1 帧无所谓
const filtered = bigList.filter(x => x.includes(deferred))

判断:用 query !== deferred 判断"deferred 是否落后",显示 loading:

tsx
const isStale = query !== deferred
{isStale && <Spinner />}

💡 deferred 短暂旧 1 帧,换取输入永远流畅——搜索/过滤/排序随便用;金额/状态/计数别用。

useId ​

tsx
const id = useId();
<label htmlFor={id}>姓名</label>
<input id={id} />

八、本章小结 ​

要点关键
Hooks 价值函数组件也能用 state/lifecycle
铁律顶层调用 / 函数组件或自定义 Hook
自定义 Hookuse 开头,封装可复用逻辑
React 18 新useTransition / useDeferredValue / useId
顺序敏感内部用顺序存 state,不能条件调用
配套工具ESLint react-hooks 插件

动手练习 ​

  1. 阅读列表:列出你能记得的所有内置 Hook,作用一句话
  2. 坏代码改造:把 if 里调 hook 的代码改成正确写法
  3. useId:用 useId 给表单字段配 label
  4. useTransition:写一个搜索框,输入时用 useTransition 包过滤逻辑

下一章:第 10 章:useState →

本站基于 VitePress 构建 · 由 StackHub 团队维护