React Hooks 保姆级教程:把组件想象成失忆的打工人,4 个 Hook 带你原地起飞

React Hooks 保姆级教程:把组件想象成失忆的打工人,4 个 Hook 带你原地起飞

💡 本文面向 React 初学者,用生活化比喻讲透 useStateuseEffectuseRefuseContext 和自定义 Hook。读完后你会彻底告别"背了又忘、忘了又背"的死循环。

一、先聊点让人血压升高的往事

还记得 Class 组件的时代吗?一个简单的计数器,你得这么写:

jsx 复制代码
class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
    this.handleClick = this.handleClick.bind(this); // 😭 还得手动 bind
  }

  handleClick() {
    this.setState({ count: this.state.count + 1 });
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        点击了 {this.state.count} 次
      </button>
    );
  }
}

一个计数器,用了 15 行代码,还牵扯到 constructorthisbindrender 一堆概念。对小白来说,this 指向问题就是第一道天堑------你永远不知道哪个 this 会突然"离家出走"。

而 Hooks 的出现,就是来拯救我们的。

同样的计数器,Hooks 版本只有 6 行:

jsx 复制代码
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>
      点击了 {count} 次
    </button>
  );
}

没有 this,没有 bind,没有 class函数组件 + Hooks,就是现代 React 的标准姿势。

那 Hooks 到底是什么?别急,我给你讲一个故事 👇


二、核心比喻:函数组件 = 一个「失忆的打工人」

理解 Hooks 的钥匙,就藏在一句话里:

函数组件每次渲染,都是把整个函数从头到尾重新执行一遍。

想象你是一个记性极差的外卖骑手(函数组件)。老板(React)每次喊你"重新跑一趟"(触发渲染),你就会把上一单的事忘得一干二净,从第一行开始重新执行。

问题来了:上一单送到哪了?客户要什么?你得记下来啊!

于是你需要几个「工具」来对抗失忆,这些工具就是 Hooks:

Hook 比喻 一句话作用
useState 📝 便利贴 记住需要触发更新的数据
useEffect 🧹 下班后的家务 渲染后去做"副作用"
useRef 🔒 私房钱抽屉 存东西,但"不声张"(不触发渲染)
useContext 📢 公司广播喇叭 跨层级共享数据,不用层层传话

下面我们一个个拆开讲。


三、useState:给失忆的组件贴上「便利贴」

1. 为什么需要它

函数组件里,你写的普通变量是"一次性"的:

jsx 复制代码
function BadCounter() {
  let count = 0; // ❌ 每次渲染都会被重置为 0

  return (
    <button onClick={() => {
      count++;        // 虽然改了,但界面根本不会变
      console.log(count);
    }}>
      点击了 {count} 次
    </button>
  );
}

点一下按钮,控制台打印 1,但界面上的数字纹丝不动。为什么?

因为:① 普通变量每次渲染都被重置;② 就算没被重置,React 根本不知道你改了变量,它不会主动重新渲染。

useState 就是来解决这两个问题的:

jsx 复制代码
import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  //     ↑当前值    ↑修改函数      ↑初始值

  return (
    <button onClick={() => setCount(count + 1)}>
      点击了 {count} 次
    </button>
  );
}
  • useState(0):告诉 React"帮我记一个值,初始是 0";
  • count:当前的值(React 帮你保存,下次渲染不会丢);
  • setCount(新值):修改值,并通知 React 重新渲染

2. 一个很关键的坑:更新是"异步"的

先看这段代码,猜猜打印什么?

jsx 复制代码
function Demo() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
    console.log(count); // 猜猜这里是 0 还是 1?
  };

  return <button onClick={handleClick}>点击</button>;
}

答案是 0

因为 setCount 并不会立刻改掉 count,它只是"预约"了一次更新,等到下次渲染时 count 才会变成新值。当前这次函数执行里,count 还是老样子。

所以如果你要"基于上一个值做计算",别用 count,要用函数式更新

jsx 复制代码
// ❌ 连续调两次,结果只加 1(因为两次拿到的 count 都是旧值)
setCount(count + 1);
setCount(count + 1);

// ✅ 函数式更新:连续加 2,正确!
setCount(c => c + 1);
setCount(c => c + 1);

💡 记住一句话:「算新值要用旧值时,传函数」

3. 对象和数组:必须"换新的",不能"改原值"

很多小白会在这一步翻车:

jsx 复制代码
const [user, setUser] = useState({ name: '小明', age: 18 });

// ❌ 错误:直接改原对象,React 检测不到变化
user.age = 19;
setUser(user);

// ✅ 正确:用展开运算符造一个新对象
setUser({ ...user, age: 19 });

// 数组同理
const [list, setList] = useState([]);
// ✅ 不要 push,要返回新数组
setList([...list, newItem]);

原理很简单:React 判断"要不要更新"用的是 引用比较 。你改的是同一个对象的内部属性,引用没变,React 就认为"没变",直接跳过。造一个新的引用,它才会动。


四、useEffect:渲染之后,顺手做的「家务活」

1. 什么是"副作用"

先搞懂这个词,你就懂一半了。

"副作用"(Side Effect)= 组件渲染之外,还要对"外部世界"做的事,比如:

  • 请求接口数据(fetch
  • 操作浏览器 API(改标题、存 localStorage)
  • 订阅/监听(addEventListenersetInterval
  • 手动改 DOM

这些事不能在渲染过程中做(渲染要"纯"),所以 React 给了你一个时机:渲染完成之后 。这个时机,就由 useEffect 提供。

2. 基本用法

jsx 复制代码
import { useState, useEffect } from 'react';

function UserInfo({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // 渲染完成后执行:请求数据
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setUser);
  }, [userId]); // 👈 依赖数组:userId 变化时才重新执行

  if (!user) return <div>加载中...</div>;
  return <div>{user.name}</div>;
}

3. 依赖数组的三种写法(面试必考)

jsx 复制代码
// ① 不传第二个参数:每次渲染都执行
useEffect(() => { ... });

// ② 传空数组 []:只在「首次渲染后」执行一次
useEffect(() => {
  console.log('我只在出生时执行一次');
}, []);

// ③ 传 [a, b]:首次执行 + a 或 b 变化时执行
useEffect(() => {
  console.log('a 或 b 变了');
}, [a, b]);

4. 清理函数:离开前记得"善后"

有些副作用需要"撤销",否则会内存泄漏。比如定时器、事件监听、订阅:

jsx 复制代码
function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const id = setInterval(() => setSeconds(s => s + 1), 1000);
    // ✅ 返回清理函数:组件卸载(或下次执行前)会调用
    return () => clearInterval(id);
  }, []);

  return <div>已经过去 {seconds} 秒</div>;
}

💡 记住:useEffect 里"造"了什么,return 里就"拆"掉什么,一一对应。

5. 两个高频踩坑

坑一:无限循环

jsx 复制代码
// ❌ 危险!每次渲染都会 setUser → 触发重新渲染 → 又执行 effect → 死循环
useEffect(() => {
  setUser({ name: '小明' });
}); // 没写依赖数组

坑二:闭包陷阱(拿不到最新的 state)

jsx 复制代码
const [count, setCount] = useState(0);

useEffect(() => {
  const id = setInterval(() => {
    console.log(count); // 永远打印 0,因为闭包锁住了首次的 count
  }, 1000);
  return () => clearInterval(id);
}, []);

解决办法还是那招------用函数式更新 或把 count 加进依赖数组。


五、useRef:一个「不声张」的私房钱抽屉

useRefuseState 都能"存值",但有一个关键区别,这是面试官最爱问的点:

useState useRef
改了之后触发渲染? ✅ 会 ❌ 不会
渲染后值还在吗? ✅ 在 ✅ 在
典型用途 驱动 UI 变化 存"不需要渲染"的值

一句话总结:

useState 是"把东西贴在脑门上",一改就嚷嚷着要重新渲染;useRef 是"把东西塞进抽屉",改了也默不作声。

1. 用途一:操作 DOM 元素

这是 useRef 最常见的用法------拿到某个 DOM 节点,手动操作它:

jsx 复制代码
import { useRef } from 'react';

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

  return (
    <>
      <input ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>
        点我聚焦输入框
      </button>
    </>
  );
}

inputRef.current 就是那个真实的 <input> DOM 节点,你可以直接调它的 .focus() 等方法。

2. 用途二:存一个"改了但不想渲染"的值

比如记录"上一次的值"、存一个计时器 id、存一个不会变的配置:

jsx 复制代码
function ClickLogger() {
  const clickCount = useRef(0); // 记录点击次数,但不触发渲染

  return (
    <button onClick={() => {
      clickCount.current++;
      console.log('实际点了', clickCount.current, '次');
    }}>
      点我(界面不会重渲染)
    </button>
  );
}

六、useContext:公 司广播喇叭,告别层层传话

1. 痛点:props 的"快递中转站"

组件嵌套一深,传数据就成了噩梦,这叫 Prop Drilling(属性钻取)

jsx 复制代码
<App>                    // 主题 = "dark"
  <Header theme="dark">  // 中转 1
    <Nav theme="dark">   // 中转 2
      <Button theme="dark" />  // 真正要用,却要经过 3 层

中间那些组件根本不用 theme,却被迫当"快递员",又丑又难维护。

2. 解法:Context = 公司全员群

useContext 的思路是:数据放到一个"广播喇叭"(Provider)里,谁需要谁直接"收听"(useContext),不用层层转发。

jsx 复制代码
import { createContext, useContext } from 'react';

// ① 创建一个"广播频道",默认值是 'light'
const ThemeContext = createContext('light');

function App() {
  return (
    // ② Provider = 打开喇叭,广播 value
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

// 中间组件 Toolbar 完全不碰 theme,也无需透传
function Toolbar() {
  return <ThemedButton />;
}

// ③ 真正需要的组件,用 useContext 直接"收听"
function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <button className={theme}>当前主题:{theme}</button>;
}

注意三个步骤:createContext 建频道 → Provider 广播 → useContext 收听,缺一不可。

3. 适用场景

  • 主题、语言、登录用户等"全局状态"
  • 但别滥用:数据频繁变化时,Context 的更新会让所有订阅者重渲染,性能不如专门的全局状态库(如 Zustand、Redux)

七、自定义 Hook:你的「逻辑乐高积木」

学到这,你已经会用内置 Hook 了。但真正让你"进阶"的,是自定义 Hook

1. 它解决什么问题

组件写多了你会发现:很多逻辑是重复的。比如"存 localStorage"这个需求,你可能会在 5 个组件里写 5 遍几乎一样的代码。

自定义 Hook 就是把「一段包含状态的逻辑」打包成一个函数,哪里需要哪里调,就像拼乐高。

2. 唯一铁律:以 use 开头

js 复制代码
function useXxx(...) { ... } // ✅ 合法
function xxx(...) { ... }    // ❌ React 会报错

为什么要 use 开头?因为 React 靠这个名字来识别"这是一个 Hook",从而做规则检查(比如不能在条件语句里调 Hook)。

3. 实战一:useLocalStorage

把"读和写 localStorage"打包:

jsx 复制代码
import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  // 惰性初始化:只在第一次渲染时读 localStorage
  const [value, setValue] = useState(() => {
    const saved = localStorage.getItem(key);
    return saved !== null ? JSON.parse(saved) : initialValue;
  });

  // 值一变,就自动写回 localStorage
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue]; // 对外暴露的接口和 useState 一模一样
}

// 使用:跟 useState 一样顺手,但数据会自动持久化
function App() {
  const [name, setName] = useLocalStorage('name', '小明');
  return (
    <input value={name} onChange={e => setName(e.target.value)} />
  );
}

刷新页面,输入框里的名字还在------因为已经悄悄存进了 localStorage。

4. 实战二:useDebounce(防抖)

搜索框场景几乎必用,输入时别每次都发请求:

jsx 复制代码
import { useState, useEffect } from 'react';

function useDebounce(value, delay = 500) {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer); // 每次变化都清掉上一个定时器
  }, [value, delay]);

  return debounced;
}

// 使用:用户停下输入 500ms 后,debouncedSearch 才是最终值
function SearchBox() {
  const [text, setText] = useState('');
  const debouncedText = useDebounce(text, 500);

  useEffect(() => {
    if (debouncedText) {
      console.log('发起搜索:', debouncedText);
    }
  }, [debouncedText]);

  return <input value={text} onChange={e => setText(e.target.value)} />;
}

5. 实战三:useWindowSize(监听窗口大小)

jsx 复制代码
import { useState, useEffect } from 'react';

function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    const handleResize = () =>
      setSize({ width: window.innerWidth, height: window.innerHeight });

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize); // 记得清理
  }, []);

  return size;
}

八、一张表,收下全部重点

Hook 解决什么问题 核心记忆点
useState 组件要"记住"并更新数据 更新是异步的;对象/数组要造新引用
useEffect 渲染后做副作用 依赖数组三种写法;记得写清理函数
useRef 存不触发渲染的值 / 操作 DOM .current 不会重新渲染
useContext 跨层级共享数据 建频道 → 广播 → 收听
自定义 Hook 复用带状态的逻辑 必须 use 开头;本质就是个函数

九、写在最后

回顾一下这条主线,你会发现 Hooks 一点都不玄:

函数组件是个记性差的打工人useState 给它贴便利贴、useEffect 让它下班后做家务、useRef 给它藏私房钱的抽屉、useContext 是公司的广播喇叭------而自定义 Hook,就是把这几样工具打包成你自己的"百宝箱"

这 4 个内置 Hook 涵盖了日常开发 90% 的场景。先把它们用熟,再去看 useMemouseCallbackuseReducer 这些进阶货,你会轻松很多。


📝 本文基于 React 16.8+ 的核心 Hook,适用于 React 18 / 19。 🔗 官方文档:react.dev/reference/r...

相关推荐
mmsx1 小时前
一个 Bug 修了 12 轮,根因只有一句话:AI 编程时代,日志才是唯一的真相
前端
默_笙1 小时前
😝 5 亿次循环卡死页面?我用 Web Worker 把它扔进后台线程,页面丝滑如初
前端·javascript
CAD老兵1 小时前
一行代码集成 DWG/DXF 图纸查看:测量批注,数据不出站
前端·javascript·github
小林ixn1 小时前
单例模式:从弹窗管理到全局状态,一个模式搞定
前端·javascript·设计模式
AI编程实验室1 小时前
Node.js Agent Handoff 仓库扫描 MVP:忽略规则、include 通配与稳定输出实现
前端·ai编程
猫不易1 小时前
从 Virtual DOM 到 Vapor:Vue 3.6 的另一条路
前端·vue.js
小林ixn1 小时前
在浏览器里跑 DeepSeek-R1:WebGPU + Transformers.js 实战
react.js·llm·浏览器
光影少年1 小时前
react navite性能优化 & 常见坑
前端·react native·掘金·金石计划
八角丶1 小时前
Node.js Cluster 详解
前端·node.js