人人都会AI编程

8.1 基础 Hooks

更新时间:2026-07-10

Hooks 是 React 16.8 引入的特性,它允许你在函数组件中使用状态和其他 React 特性,彻底告别类组件。基础 Hooks 是所有 React 开发者必须熟练掌握的核心工具。

useState:让函数组件拥有状态

useState 是最核心的 Hook,它让函数组件能够声明和更新内部状态。每次状态改变,组件都会重新渲染。

基本用法:

const [state, setState] = useState(initialValue);
  • state:当前的状态值
  • setState:更新状态的函数
  • initialValue:状态的初始值(可以是任意类型,也可以传入一个函数进行惰性初始化)

实际示例:

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

  return (
    <div>
      <p>点击次数:{count}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
      <button onClick={() => setCount(0)}>重置</button>

      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="请输入姓名"
      />
    </div>
  );
}

关键注意事项:

  • 状态是不可变的:更新状态时必须传入新值,React 用 Object.is 比较新旧值判断是否重渲染。如果状态是对象或数组,请始终创建新对象/数组,而不能直接修改原值:
  // ❌ 错误:直接修改原数组
  arr.push(newItem);
  setArr(arr);

  // ✅ 正确:创建新数组
  setArr([...arr, newItem]);
  
  • 函数式更新:当新状态依赖旧状态时,建议使用函数式更新,避免闭包陷阱:
  setCount(prevCount => prevCount + 1);
  

这在连续调用或异步场景中尤其重要。

  • 惰性初始化:如果初始状态需要复杂计算,传递一个函数,React 只会在首次渲染时执行它:
  const [data, setData] = useState(() => {
    const saved = localStorage.getItem('data');
    return saved ? JSON.parse(saved) : defaultData;
  });
  
  • 状态更新是异步的:在同一个事件处理函数中多次调用 setState,React 会批量处理,你无法立即拿到最新值。如果需要在更新后立即执行操作,请使用 useEffect 监听该状态。

useEffect:处理副作用

副作用是指组件渲染之外的操作,例如数据请求、订阅事件、操作 DOM、设置定时器等。useEffect 让你在函数组件中统一管理这些副作用。

基本用法:

useEffect(() => {
  // 副作用逻辑
  return () => {
    // 清理函数(可选)
  };
}, [依赖项数组]);

三种典型使用场景:

  1. 不传递依赖项:每次渲染后都执行(几乎不用,容易死循环)
  2. 传递空数组 []:只在组件挂载时执行一次,类似 componentDidMount
  3. 传递具体依赖:当依赖项变化时重新执行

实际示例:

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

  useEffect(() => {
    let cancelled = false; // 防止竞态

    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        if (!cancelled) {
          setUser(data);
          setLoading(false);
        }
      });

    return () => {
      cancelled = true; // 组件卸载或依赖变化时取消
    };
  }, [userId]); // userId 变化时重新请求

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

清理函数的重要性:

如果副作用创建了需要手动清除的资源(如定时器、订阅、事件监听),必须在清理函数中处理,否则会导致内存泄漏:

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

  return () => clearInterval(timer); // 组件卸载时清除定时器
}, []);

常见的依赖项陷阱:

  • 如果你在 useEffect 中使用了组件内的变量或函数,而没有将它列入依赖数组,React 会报警告,并且可能访问到过期的闭包变量。
  • 不要为了消除警告而随意添加依赖,要确保逻辑正确。如果某个函数只在 useEffect 中使用且不变,可以将其定义在 useEffect 内部。

useRef:跨渲染周期的持久引用

useRef 返回一个可变的 ref 对象,其 .current 属性在组件的整个生命周期内保持不变。它有两个核心用途:访问 DOM 元素和存储不触发重渲染的变量。

1. 获取 DOM 节点引用:

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

  const handleClick = () => {
    inputRef.current.focus(); // 直接操作 DOM
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={handleClick}>聚焦输入框</button>
    </div>
  );
}

2. 存储可变值(不触发重渲染):

当你需要在组件多次渲染之间保存一个值,但它的变化不需要导致界面更新时,使用 useRef 而不是 useState

function Timer() {
  const timerIdRef = useRef(null);
  const [seconds, setSeconds] = useState(0);

  const start = () => {
    if (timerIdRef.current) return; // 防止重复启动
    timerIdRef.current = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);
  };

  const stop = () => {
    clearInterval(timerIdRef.current);
    timerIdRef.current = null;
  };

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

  return (
    <div>
      <p>{seconds} 秒</p>
      <button onClick={start}>开始</button>
      <button onClick={stop}>停止</button>
    </div>
  );
}

这里 timerIdRef 用于保存定时器 ID,它不会导致组件重渲染,完美避开了 useState 的“值变化就渲染”的特性。

useState 的区别:

  • useState 的值变化会触发重渲染。
  • useRef.current 变化不会触发重渲染。
  • useRef 的值在渲染之间持久存在,而普通变量在每次渲染时都重新创建。

useContext:穿越组件树的共享数据

useContext 让你在组件树中直接读取 Context 的值,无需通过 Props 逐层传递。它解决了“Props 钻探”问题。

使用步骤:

  1. 创建 Context:
const ThemeContext = createContext('light');
  1. 在组件树上层提供值:
function App() {
  const [theme, setTheme] = useState('dark');

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}
  1. 在任意子组件中消费:
function Toolbar() {
  return <ThemedButton />;
}

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

  return (
    <button
      style={{ background: theme === 'dark' ? '#333' : '#eee' }}
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
    >
      切换主题
    </button>
  );
}

ThemedButton 不需要从父组件接收 Props,直接通过 Context 获取主题和切换函数,中间组件 Toolbar 无需关心这些数据。

性能注意事项:

Context 的 Provider 值变化时,所有使用该 Context 的组件都会重新渲染。如果 Context 值是一个对象/数组,每次父组件重渲染都会创建新对象,导致所有消费者不必要地渲染。解决方案:

  • 将 Context 拆分得更细,让不同职责的数据分开传递。
  • 使用 useMemo 缓存 Context 的 value:
const value = useMemo(() => ({ theme, setTheme }), [theme]);
  • 或者使用轻量级状态管理库(如 Zustand)来替代大规模 Context。

适用场景:

  • 全局主题、语言、认证用户信息等很少改变的数据
  • 组件库中的配置信息
  • 浅层组件树中的共享数据

不建议将所有状态都丢进 Context,过度使用会降低组件复用性和性能。遵循“就近原则”,能通过 Props 解决的就不要滥用 Context。


这四种基础 Hook 是构建 React 应用的基石。下面章节会进一步介绍性能优化 Hooks 和进阶 Hooks,帮助你应对更复杂的场景。