阅读视图

发现新文章,点击刷新页面。

React 常用 Hooks 函数及使用方法完全指南(useState / useEffect / useRef / useContext / useCallback / useMemo / useReducer)

前言

React Hooks 自 React 16.8 引入以来,已经彻底改变了我们编写 React 组件的方式。Hooks 让我们在函数组件中使用状态和生命周期能力,告别了类组件的繁琐写法。本文将从最常用的几个 Hook 入手,详细介绍它们的使用方法、最佳实践和常见陷阱。

如果你刚开始接触 React Hooks,或者想系统地梳理一遍常用 Hook 的用法,这篇文章应该能帮到你。


一、useState — 组件状态管理

useState 是最基础、最常用的 Hook,用于在函数组件中声明和管理状态。

基本用法

import { useState } from 'react';

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

  return (
    <div>
      <p>计数:{count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

useState 接收一个初始值,返回一个长度为 2 的数组:当前状态更新状态的函数

更新状态的两种方式

方式一:直接传入新值

setCount(count + 1);

方式二:传入更新函数(推荐,当新值依赖旧值时)

setCount(prev => prev + 1);

第二种方式可以避免闭包陷阱。如果你的 setCount 在异步回调中调用,使用函数式更新能确保拿到最新的状态值。

状态更新的异步性

很多人刚接触时会被这个问题困扰:setState 之后立刻读取状态,发现值没变。

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

setCount(1);
console.log(count); // 还是 0,不是 1

这是因为 React 的状态更新是异步且批量的。实际的重新渲染会在当前事件循环结束后进行。

复杂状态:多个字段

如果状态是一个对象,更新时要手动合并:

const [form, setForm] = useState({ name: '', age: 0 });

// 错误:会丢失 name 字段
setForm({ age: 18 });

// 正确:需要手动合并
setForm(prev => ({ ...prev, age: 18 }));

💡 如果你的状态逻辑比较复杂(多个子字段、相互依赖),考虑用 useReducer 替代。


二、useEffect — 副作用处理

useEffect 用于处理组件中的副作用:数据请求、DOM 操作、订阅、计时器等。

基本用法

import { useEffect } from 'react';

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

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => setUser(data));
  }, [userId]); // 依赖项

  return <div>{user?.name}</div>;
}

理解依赖数组

依赖数组是 useEffect 的灵魂,它决定了 effect 何时执行:

依赖数组 执行时机
undefined (不传) 每次渲染后执行
[] (空数组) 只在挂载后执行一次
[a, b] 当 a 或 b 发生变化时执行

清理函数

如果 effect 产生了订阅、计时器等需要清理的资源,返回一个清理函数:

useEffect(() => {
  const timer = setInterval(() => {
    console.log('tick');
  }, 1000);

  return () => {
    clearInterval(timer); // 组件卸载时清理
  };
}, []);

这里的 return () => clearInterval(timer) 就是清理函数,会在组件卸载时执行,防止内存泄漏。

常见陷阱

陷阱一:忘记添加依赖

useEffect(() => {
  fetch(`/api/users/${userId}`).then(...)
}, []); // ❌ userId 变了也不会重新请求

陷阱二:不必要的依赖导致死循环

useEffect(() => {
  setCount(count + 1); // ❌ count 变化 → 重新渲染 → effect 触发 → count 变化 → 死循环
}, [count]);

陷阱三:在 effect 中使用旧值(闭包问题)

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

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1); // ❌ count 永远是 0
    }, 1000);
    return () => clearInterval(id);
  }, []); // 没有依赖 count
}

解决方案:使用函数式更新 setCount(c => c + 1),或者把 count 加入依赖数组并清理/重建定时器。

useEffect 与 useLayoutEffect 的区别

  • useEffect异步执行,在浏览器绘制之后触发。适合数据请求、事件绑定等不需要阻塞视觉更新的操作。
  • useLayoutEffect同步执行,在 DOM 更新后、浏览器绘制前触发。适合需要读取 DOM 布局的场景(如测量元素尺寸)。

绝大多数情况下用 useEffect 就够了,只有当你遇到闪烁(flicker)问题时才考虑使用 useLayoutEffect


三、useRef — 引用 DOM 和可变值

useRef 有两个主要用途:引用 DOM 元素存储可变值(不触发重新渲染)。

引用 DOM 元素

import { useRef, useEffect } from 'react';

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

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

  return <input ref={inputRef} />;
}

存储可变值(改变不触发重渲染)

function Timer() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

  // 始终保持 ref 与 state 同步
  countRef.current = count;

  useEffect(() => {
    const id = setInterval(() => {
      console.log('当前 count:', countRef.current); // 能拿到最新值
    }, 1000);
    return () => clearInterval(id);
  }, []); // 空依赖,定时器只创建一次
}

这个模式常用于解决闭包陷阱——ref.current 永远指向最新的值,因为它是一个可变对象。

useRef vs useState 的关键区别

特性 useState useRef
修改触发重渲染 ✅ 是 ❌ 否
跨渲染周期保存数据 ✅ 是 ✅ 是
在异步回调中获取最新值 ❌ 闭包问题 ✅ 始终最新
修改方式 setState(newVal) ref.current = newVal

四、useContext — 跨组件数据共享

useContext 让你在不使用 props 层层传递的情况下,在组件树中共享数据。

三步使用法

第一步:创建 Context

import { createContext } from 'react';

const ThemeContext = createContext('light');

第二步:使用 Provider 提供数据

function App() {
  const [theme, setTheme] = useState('light');

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <ThemedComponent />
    </ThemeContext.Provider>
  );
}

第三步:子组件消费数据

import { useContext } from 'react';

function ThemedComponent() {
  const { theme, setTheme } = useContext(ThemeContext);

  return (
    <div className={theme}>
      当前主题:{theme}
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        切换主题
      </button>
    </div>
  );
}

性能注意点

当 Provider 的 value 发生变化时,所有使用 useContext 的子组件都会重新渲染。如果 value 是一个对象,每次父组件渲染都会创建新引用,导致所有消费者重渲染。

解决方案:用 useMemo 包裹 value,或者将 Context 拆分为多个(读写分离)。

// 分离读和写,避免不必要的重渲染
const ThemeContext = createContext('light');
const ThemeUpdateContext = createContext(() => {});

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const toggleTheme = useCallback(
    () => setTheme(t => (t === 'light' ? 'dark' : 'light')),
    []
  );

  return (
    <ThemeContext.Provider value={theme}>
      <ThemeUpdateContext.Provider value={toggleTheme}>
        {children}
      </ThemeUpdateContext.Provider>
    </ThemeContext.Provider>
  );
}

五、useCallback — 缓存函数引用

useCallback 用于缓存函数的引用,避免因函数重新创建导致子组件不必要的重新渲染。

import { useCallback } from 'react';

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

  // 每次 Parent 渲染都会创建新的函数引用
  const handleClick = () => setCount(c => c + 1);

  // useCallback 缓存函数,只有依赖变化时才重建
  const handleClickCached = useCallback(
    () => setCount(c => c + 1),
    []
  );

  return <ExpensiveChild onClick={handleClickCached} />;
}

什么时候用 useCallback

不是所有函数都需要包裹 useCallback。过度使用反而会降低可读性和性能(因为 useCallback 本身也有开销)。

适合的场景:

  • 函数作为 props 传给使用了 React.memo 的子组件
  • 函数作为其他 Hook 的依赖项(比如 useEffect 的依赖)
  • 函数在自定义 Hook 中返回给外部使用

💡 一句话法则:当你确定不缓存会导致不必要的性能问题时再用。初期先正常写函数,遇到性能瓶颈再优化。


六、useMemo — 缓存计算结果

useMemo 用于缓存复杂计算的结果,避免每次渲染都重复执行。

import { useMemo } from 'react';

function Dashboard({ transactions }) {
  // 复杂计算:过滤 + 聚合
  const summary = useMemo(() => {
    return transactions
      .filter(t => t.amount > 0)
      .reduce((acc, t) => ({
        total: acc.total + t.amount,
        count: acc.count + 1,
        avg: (acc.total + t.amount) / (acc.count + 1)
      }), { total: 0, count: 0, avg: 0 });
  }, [transactions]);

  return <div>总金额:{summary.total},平均:{summary.avg}</div>;
}

useMemo vs useCallback

  • useCallback(fn, deps) 等价于 useMemo(() => fn, deps)
  • useCallback 缓存的是函数本身
  • useMemo 缓存的是计算的结果

不要滥用 useMemo

useCallback 一样,useMemo 也不是免费的。简单的计算(如数组 map、filter)其开销可能还不如 useMemo 的对比开销大。

适合 useMemo 的场景:

  • 计算复杂度较高(O(n²) 及以上)
  • 计算结果作为 props 传给 React.memo 子组件
  • 计算结果作为其他 Hook 的依赖项

七、useReducer — 复杂状态管理

当状态逻辑变得复杂(多个子值、相互依赖、多层次更新),useState 就不太够用了。这时 useReducer 是更好的选择。

import { useReducer } from 'react';

// 1. 定义 reducer 函数
function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD':
      return [...state, { id: Date.now(), text: action.payload, done: false }];
    case 'TOGGLE':
      return state.map(t =>
        t.id === action.payload ? { ...t, done: !t.done } : t
      );
    case 'DELETE':
      return state.filter(t => t.id !== action.payload);
    default:
      return state;
  }
}

function TodoApp() {
  // 2. 使用 useReducer
  const [todos, dispatch] = useReducer(todoReducer, []);

  return (
    <div>
      <button onClick={() => dispatch({ type: 'ADD', payload: '新任务' })}>
        添加
      </button>
      {todos.map(todo => (
        <div key={todo.id}>
          <span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
            {todo.text}
          </span>
          <button onClick={() => dispatch({ type: 'TOGGLE', payload: todo.id })}>
            完成
          </button>
        </div>
      ))}
    </div>
  );
}

useReducer vs useState 的选择

场景 推荐
独立、简单的状态 useState
包含多个子值的复杂状态 useReducer
下一个状态依赖前一个 useReducer(或函数式 setState)
更新逻辑在组件外可独立测试 useReducer
只需浅层更新表单字段 useState

八、自定义 Hooks — 逻辑复用

自定义 Hook 是 React Hooks 的精髓之一。当你发现多个组件中有相似的逻辑时,可以提取成一个自定义 Hook。

import { useState, useEffect } from 'react';

// 自定义 Hook:获取数据
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;

    setLoading(true);
    fetch(url)
      .then(res => {
        if (!res.ok) throw new Error('请求失败');
        return res.json();
      })
      .then(data => {
        if (!cancelled) {
          setData(data);
          setLoading(false);
        }
      })
      .catch(err => {
        if (!cancelled) {
          setError(err);
          setLoading(false);
        }
      });

    return () => { cancelled = true; }; // 清理:防止竞态
  }, [url]);

  return { data, loading, error };
}

// 使用
function UserList() {
  const { data, loading, error } = useFetch('/api/users');

  if (loading) return <div>加载中...</div>;
  if (error) return <div>出错了:{error.message}</div>;
  return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

自定义 Hook 的命名规则

  • 必须以 use 开头(React 官方约定,linter 也会检查)
  • 内部可以调用其他 Hook
  • 普通函数不能调用 Hook,但自定义 Hook 可以

九、Hooks 使用规则

最后,牢记 React Hooks 的两条铁律:

规则一:只在顶层调用 Hooks

不要在循环、条件语句或嵌套函数中调用 Hook。

// ❌ 错误:在条件中调用
if (isLoading) {
  useEffect(() => { ... }, []);
}

// ✅ 正确:始终在顶层
useEffect(() => {
  if (!isLoading) { ... }
}, [isLoading]);

规则二:只在 React 函数中调用 Hooks

  • 在函数组件中调用 ✅
  • 在自定义 Hook 中调用 ✅
  • 在普通函数中调用 ❌
  • 在类组件中调用 ❌
  • 在回调中调用 ❌

总结

本文介绍了 React 中最常用的 7 个核心 Hook:

Hook 用途
useState 声明和管理组件状态
useEffect 处理副作用(请求、订阅、DOM 操作)
useRef 引用 DOM 元素、存储可变值
useContext 跨组件层级共享数据
useCallback 缓存函数引用
useMemo 缓存计算结果
useReducer 管理复杂状态逻辑

记住:Hooks 是工具,不是目的。不要在不需要的地方强行使用性能优化 Hook(useCallback、useMemo),先写出清晰的代码,遇到性能问题再针对性地优化。

希望这篇文章能帮你更好地理解和运用 React Hooks。有什么问题欢迎在评论区交流讨论~

❌