返回文章列表
开发技术ReactHooks前端开发状态管理

React Hooks 学习笔记

2025-03-1510 分钟

为什么使用 Hooks

React Hooks 是 React 16.8 引入的新特性,它让我们在函数组件中也能使用状态和其他 React 特性。在 Hooks 出现之前,如果组件需要管理状态或执行副作用,就必须使用类组件。Hooks 的出现使得函数组件变得更加强大,同时也简化了组件间的逻辑复用。本文将重点介绍四个最常用的 Hook:useState、useEffect、useCallback 和 useMemo。

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>
      <button onClick={() => setCount(prev => prev - 1)}>-1</button>
    </div>
  );
}

// 使用函数初始化(适用于计算开销较大的初始值)
function ExpensiveComponent() {
  const [data, setData] = useState(() => {
    return computeExpensiveInitialValue();
  });

  return <div>{data}</div>;
}

// 管理对象状态
function UserForm() {
  const [form, setForm] = useState({
    name: '',
    email: '',
    age: 0,
  });

  const handleChange = (field: string, value: string | number) => {
    setForm(prev => ({ ...prev, [field]: value }));
  };

  return (
    <form>
      <input
        value={form.name}
        onChange={e => handleChange('name', e.target.value)}
        placeholder="姓名"
      />
      <input
        value={form.email}
        onChange={e => handleChange('email', e.target.value)}
        placeholder="邮箱"
      />
    </form>
  );
}

需要注意的是,useState 的更新函数在处理对象或数组时,必须创建新的引用而非直接修改原有数据,因为 React 通过引用比较来判断状态是否变化。

useEffect:副作用处理

useEffect 用于处理组件中的副作用操作,比如数据获取、订阅、手动修改 DOM 等。它相当于类组件中 componentDidMount、componentDidUpdate 和 componentWillUnmount 的组合。

import { useState, useEffect } from 'react';

function UserProfile({ userId }: { userId: number }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);

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

    async function fetchUser() {
      setLoading(true);
      try {
        const response = await fetch(`/api/users/${userId}`);
        const data = await response.json();
        if (!cancelled) {
          setUser(data);
        }
      } catch (error) {
        console.error('获取用户信息失败:', error);
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    }

    fetchUser();

    // 清除函数:组件卸载或 userId 变化时执行
    return () => {
      cancelled = true;
    };
  }, [userId]); // 依赖数组:仅在 userId 变化时重新执行

  if (loading) return <p>加载中...</p>;
  if (!user) return <p>用户不存在</p>;

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

// 事件监听的副作用
function WindowSize() {
  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 <p>窗口大小:{size.width} x {size.height}</p>;
}

useEffect 的依赖数组非常重要:传入空数组表示仅在组件挂载时执行一次;传入具体依赖则在依赖变化时重新执行;不传依赖数组则每次渲染后都会执行。合理管理依赖数组是避免无限循环和性能问题的关键。

useCallback:函数缓存

useCallback 用于缓存函数引用,避免在每次渲染时创建新的函数实例。当回调函数作为 prop 传递给子组件时,配合 React.memo 可以有效减少不必要的重渲染。

import { useState, useCallback, memo } from 'react';

// 子组件使用 memo 包裹
const TodoItem = memo(function TodoItem({
  todo,
  onToggle,
}: {
  todo: { id: number; text: string; done: boolean };
  onToggle: (id: number) => void;
}) {
  console.log(`TodoItem ${todo.id} 渲染`);
  return (
    <li onClick={() => onToggle(todo.id)}>
      {todo.done ? '✅' : '⬜'} {todo.text}
    </li>
  );
});

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: '学习 React', done: false },
    { id: 2, text: '写技术博客', done: false },
  ]);

  // 使用 useCallback 缓存函数
  const handleToggle = useCallback((id: number) => {
    setTodos(prev =>
      prev.map(todo =>
        todo.id === id ? { ...todo, done: !todo.done } : todo
      )
    );
  }, []); // 依赖为空,函数引用始终不变

  return (
    <ul>
      {todos.map(todo => (
        <TodoItem key={todo.id} todo={todo} onToggle={handleToggle} />
      ))}
    </ul>
  );
}

useMemo:计算结果缓存

useMemo 用于缓存计算结果,避免在每次渲染时重复执行开销较大的计算。它接收一个工厂函数和依赖数组,只有当依赖发生变化时才会重新计算。

import { useState, useMemo } from 'react';

function ProductList({ products }: { products: Product[] }) {
  const [filter, setFilter] = useState('');
  const [sortBy, setSortBy] = useState<'name' | 'price'>('name');

  // 使用 useMemo 缓存过滤和排序的结果
  const filteredAndSorted = useMemo(() => {
    console.log('重新计算过滤结果');
    return products
      .filter(p => p.name.includes(filter))
      .sort((a, b) => {
        if (sortBy === 'name') return a.name.localeCompare(b.name);
        return a.price - b.price;
      });
  }, [products, filter, sortBy]);

  return (
    <div>
      <input
        value={filter}
        onChange={e => setFilter(e.target.value)}
        placeholder="搜索商品"
      />
      <select
        value={sortBy}
        onChange={e => setSortBy(e.target.value as 'name' | 'price')}
      >
        <option value="name">按名称排序</option>
        <option value="price">按价格排序</option>
      </select>
      <ul>
        {filteredAndSorted.map(p => (
          <li key={p.id}>{p.name} - ¥{p.price}</li>
        ))}
      </ul>
    </div>
  );
}

使用建议

最后总结几条使用 Hooks 的建议:

  • 不要过度优化:不是所有函数都需要 useCallback,不是所有计算都需要 useMemo。只在确实存在性能问题时才使用。
  • 保持 Hook 调用顺序一致:不要在条件语句或循环中调用 Hook,确保每次渲染时 Hook 的调用顺序相同。
  • 合理拆分 useEffect:如果一个 useEffect 中处理了多个不相关的副作用,应该拆分成多个 useEffect。
  • 自定义 Hook 复用逻辑:当多个组件之间存在相似的状态逻辑时,可以将其提取为自定义 Hook。

掌握好这几个核心 Hooks,就能应对大部分 React 开发场景。随着项目经验的积累,可以进一步学习 useReducer、useContext、useRef 等其他 Hooks,以及自定义 Hook 的设计模式。