React Hooks 必学:useState、useEffect、useRef 实战详解

1 阅读4分钟

一、Hooks 规则(必须遵守)

Hooks(useState/useEffect/useRef…)是 React 16.8 引入的函数,让函数组件拥有状态与副作用,替代类组件。三条铁律:

规则说明
只在顶层调用不能在条件、循环、嵌套函数中调用(每次渲染顺序必须一致)
只在函数组件/自定义 Hook 中调用不能在普通函数或类组件中调用
命名以 use 开头自定义 Hook 命名规范
// ❌ 错误:条件里调用 Hook
if (isLogin) {
  useState(0);
}

// ✅ 正确:始终在最顶层
const [count, setCount] = useState(0);
if (isLogin) { /* 普通逻辑 */ }

二、useState:状态管理

import { useState } from "react";

function App() {
  const [count, setCount] = useState(0);

  // 函数式更新:连续更新基于最新值
  const addTwice = () => {
    setCount((prev) => prev + 1);
    setCount((prev) => prev + 1);
  };

  return (
    <div>
      <p>{count}</p>
      <button onClick={addTwice}>+2</button>
    </div>
  );
}

要点回顾:useState 返回 [值, 更新函数]状态不可变(创建新值替换);更新触发组件重渲染。


三、useEffect:副作用(生命周期替代者)

3.1 三种用法对应生命周期

import { useEffect, useState } from "react";

function App() {
  const [id, setId] = useState(1);

  // ① 空依赖:模拟挂载 + 卸载
  useEffect(() => {
    console.log("组件挂载(mounted)");
    return () => console.log("组件卸载(beforeUnmount)");   // 清理函数
  }, []);

  // ② 依赖数组:模拟更新(id 变化时执行)
  useEffect(() => {
    fetchData(id);
  }, [id]);

  // ③ 无依赖数组:每次渲染都执行(慎用,通常不需要)
  useEffect(() => {
    console.log("每次渲染都执行");
  });
}

3.2 生命周期对照表

类组件钩子Hooks 实现
componentDidMountuseEffect(() => {...}, [])
componentDidUpdateuseEffect(() => {...}, [依赖])
componentWillUnmountuseEffect 的清理函数
三者合并useEffect(() => { ...; return cleanup; }, [依赖])

3.3 经典业务:请求数据 + 定时器

function UserList() {
  const [list, setList] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let ignore = false;                      // 防竞态:取消过期响应
    fetch("/api/users")
      .then((res) => res.json())
      .then((data) => {
        if (!ignore) setList(data);
        setLoading(false);
      });
    return () => { ignore = true; };         // 卸载/依赖变化时执行
  }, []);

  return loading ? <p>加载中…</p> : <ul>{list.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}
// 定时器必须在卸载时清理,否则内存泄漏
useEffect(() => {
  const timer = setInterval(() => setTime(new Date().toLocaleTimeString()), 1000);
  return () => clearInterval(timer);
}, []);

四、useRef:获取 DOM、保存可变变量

4.1 获取 DOM

import { useRef, useEffect } from "react";

function SearchInput() {
  const inputRef = useRef(null);

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

  return <input ref={inputRef} placeholder="自动聚焦" />;
}

4.2 保存可变变量(不触发重渲染)

function Timer() {
  const timerRef = useRef(null);        // 存定时器 id
  const countRef = useRef(0);           // 存计数(不渲染)

  const start = () => {
    timerRef.current = setInterval(() => {
      countRef.current += 1;
    }, 1000);
  };

  const stop = () => clearInterval(timerRef.current);
  return <><button onClick={start}>开始</button><button onClick={stop}>停止</button></>;
}

4.3 ref 与 useState 的区别

对比项useRefuseState
变化是否触发渲染❌ 不触发✅ 触发
读取.current直接值
适用DOM、定时器 id、跨渲染可变值需要驱动视图的数据

五、常见业务场景

场景方案
自动聚焦输入框useRef + useEffect
定时器/轮询useEffect + setInterval + 清理函数
请求数据useEffect + 空依赖 + 防竞态标志
防抖搜索useEffect + setTimeout 清理
保存上一次值useRef 记录 + useEffect 更新
组件挂载标识useRef(false) 首次渲染判断
// 防抖搜索示例
function Search() {
  const [keyword, setKeyword] = useState("");
  const [results, setResults] = useState([]);

  useEffect(() => {
    const timer = setTimeout(() => {
      setResults([`${keyword} 的结果 1`, `${keyword} 的结果 2`]);
    }, 300);
    return () => clearTimeout(timer);     // 每次输入先清掉上一次定时器
  }, [keyword]);

  return (
    <>
      <input value={keyword} onChange={(e) => setKeyword(e.target.value)} />
      <ul>{results.map((r, i) => <li key={i}>{r}</li>)}</ul>
    </>
  );
}

六、Hooks 高频踩坑

现象解决
条件里调用 Hook"Rendered fewer hooks than expected" 报错Hook 提到顶层,条件放内部
依赖数组漏写不更新(漏依赖)或无限循环(依赖对象每次新引用)依赖写全;对象依赖用具体字段或 useMemo 稳定引用
闭包陷阱effect 里读到的 state 是旧值依赖数组加上该值,或函数式更新
直接改 state 对象不触发渲染展开创建新对象
定时器/订阅未清理内存泄漏、重复触发effect 返回清理函数
渲染中读取 ref.current拿到旧值/不一致只在事件或 effect 中读取
// 闭包陷阱示例:定时器读到的 count 永远是初始值
useEffect(() => {
  const timer = setInterval(() => console.log(count), 1000);   // ❌ 闭包旧值
  return () => clearInterval(timer);
}, []);   // count 不在依赖里

// ✅ 解决:count 加入依赖(会重建定时器),或用函数式更新

七、总结

  • 规则:顶层调用、函数组件内调用、use 开头命名;
  • useState:状态 + 不可变更新;
  • useEffect:空依赖=挂载/卸载、依赖数组=更新、返回清理函数防泄漏;
  • useRef:DOM 引用 + 不触发渲染的可变变量;
  • 场景:请求、定时器、防抖、聚焦一套组合拳;
  • 踩坑:条件调用、依赖缺失、闭包旧值是三大高频问题。