React Hooks — 什么是Hooks、基本Hooks和使用规则

作者: IT Sectr 发布日期: 2026-07-04 阅读时间: 10 分钟

React Hooks — 是允许在函数组件中使用状态、副作用、上下文和其他React功能的函数,无需编写类。Hooks于React 16.8引入,彻底改变了开发方式,取代了HOC和render props等复杂模式。根据React, 2024,有15个内置Hooks覆盖了组件管理的所有场景。

要点

  • Hooks — 无需extends Component即可将React功能连接到函数组件的函数。
  • useState — 管理状态的基本Hook,返回值和设置器。
  • useEffect — 用于副作用:API请求、订阅、定时器、DOM操作。
  • useContext — 无需Consumer包装器和static contextType即可访问React上下文。
  • Hooks规则 — 只在顶层调用,且只在函数组件或自定义Hooks内部调用。

什么是React Hooks?

React Hooks — 是React内置的一组函数,允许在不创建类的情况下“连接”到框架的内部机制。在React 16.8之前,函数组件只能接收props并返回JSX — 任何复杂逻辑都需要类组件。

类组件的问题 — 相关逻辑的分离。属于一个实体的代码(例如WebSocket订阅)被迫分布在componentDidMount、componentDidUpdate和componentWillUnmount之间。Hooks解决了这个问题:所有逻辑都分组在一个useEffect中。

第二个问题 — 逻辑复用。为了在类组件之间复用状态逻辑,必须应用HOC(高阶组件)或render props — 这两种模式都在组件树中创建了额外的嵌套。自定义Hooks在无需嵌套的情况下解决了这个问题。

useState — 状态管理

useState — 最基本的Hook。它接受一个初始值并返回一个元组:当前值和用于更新它的函数。与类中的this.setState不同,useState不会合并对象 — 新状态完全取代特定useState调用的先前状态。

js
import React, { useState } from 'react';

function Form() {
  const [text, setText] = useState('');
  const [submitting, setSubmitting] = useState(false);

  async function handleSubmit() {
    setSubmitting(true);
    await fetch('/api/submit', {
      method: 'POST',
      body: JSON.stringify({ text })
    });
    setSubmitting(false);
  }

  return (
    <div>
      <input
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <button onClick={handleSubmit}
        disabled={submitting}>
        {submitting ? 'Saving...' : 'Submit'}
      </button>
    </div>
  );
}

setState的函数形式

useState的设置器可以接受一个函数,该函数接收先前的值并返回新值。当新状态从先前状态计算而来且可能存在批量setState调用时,函数形式是必需的 — 每次调用都将获得最新的先前值。

useEffect — 副作用和生命周期

useEffect — 用于在函数组件中执行副作用的Hook。它接受两个参数:副作用函数和依赖数组。React在每次渲染后执行副作用,但指定依赖项时 — 仅当其中任何一个发生变化时。

空依赖数组[]意味着副作用在首次渲染后执行一次(类似componentDidMount)。从副作用返回函数是清理(cleanup),它在卸载时(类似componentWillUnmount)和副作用重新执行之前执行。

js
import React, { useState, useEffect } from 'react';

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const socket = new WebSocket(`wss://chat/${roomId}`);

    socket.onmessage = (event) => {
      setMessages(prev => [...prev, JSON.parse(event.data)]);
    };

    // 清理 - 关闭套接字
    return () => socket.close();
  }, [roomId]);

  return <ul>
    {messages.map((msg, i) =>
      <li key={i}>{msg.text}</li>
    )}
  </ul>;
}

useEffect的依赖项 — 是副作用与外部世界之间的唯一连接。如果在副作用中使用了来自props或状态的变量,它必须位于依赖数组中。忽略此规则是出现陈旧数据错误(stale closure)的根源。

useContext — 访问上下文

useContext — 用于读取上下文值的Hook。它接受由React.createContext创建的上下文对象并返回当前值。如果上下文发生变化,组件会自动用新值重新渲染。

在useContext出现之前,函数组件中访问上下文需要包装在Context.Consumer中,这会产生嵌套。useContext完全消除了Consumer — 只需在函数组件的任何位置调用该Hook即可。

js
import React, { createContext, useContext } from 'react';

const ThemeContext = createContext('亮色');

function ThemedButton() {
  const theme = useContext(ThemeContext);

  return <button
    className={`btn btn-${theme}`}
  >
    {theme === 'dark' ? 'moon' : '☀️'}
  </button>;
}

useContext不是Redux或全局状态的替代品。React建议将上下文用于很少变化的值 — 主题、语言、授权。对于频繁更新的数据,每次重新渲染都会重新计算上下文的所有消费者,这可能会降低性能。

useReducer — 复杂状态逻辑

useReducer — 对于具有复杂更新逻辑的状态,是useState的替代方案。它接受reducer函数和初始状态,返回当前状态和dispatch函数。Reducer接收当前状态和action,返回新状态 — 一个没有副作用的纯函数。

当多个操作(set、reset、increment、addItem)管理一个状态时,useReducer特别有用。替代多个具有不同设置器的useState — 一个具有清晰操作类型的reducer。这使得逻辑可预测且易于测试。

js
import React, { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + 1 };
    case 'setStep':
      return { ...state, step: action.step };
    case 'reset':
      return { count: 0, step: 1 };
    default:
      throw new Error('Unknown action: ' + action.type);
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, {
    count: 0,
    step: 1
  });

  return (
    <>
      <p>Count: {state.count}</p>
      <button
        onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button
        onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </>
  );
}

useRef, useMemo, useCallback — 优化

useRef创建一个在渲染之间持续存在的可变对象。更改current不会导致重新渲染。主要用途 — 访问DOM元素和存储可变值(定时器、先前值、库实例)。

useMemo记忆函数的结果。仅当其中一个依赖项更改时才重新计算值。用于昂贵的计算 — 过滤大数组、排序、格式化数据。useCallback — 用于记忆函数的useMemo特例。

js
import React, { useRef, useMemo, useCallback } from 'react';

function SearchPage({ items, query, onItemSelect }) {
  const inputRef = useRef(null);

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

  // 记忆过滤后的结果
  const filtered = useMemo(() => {
    return items.filter(item =>
      item.name.toLowerCase()
        .includes(query.toLowerCase())
    );
  }, [items, query]);

  // 记忆回调
  const handleSelect = useCallback((id) => {
    onItemSelect(id);
  }, [onItemSelect]);

  return (
    <div>
      <input ref={inputRef}
        value={query}
        onChange={e => setQuery(e.target.value)}
      />
      <ul>
        {filtered.map(item =>
          <li key={item.id}>
            <button
              onClick={() => handleSelect(item.id)}>
              {item.name}
            </button>
          </li>
        )}
      </ul>
    </div>
  );
}

重要规则:useRef、useMemo和useCallback — 优化工具,而非强制构造。不要将所有内容都包装在useMemo中 — 先衡量性能。过早优化会使代码复杂化。React渲染组件很快;只有在计算确实昂贵(O(n²)及以上)时才需要useMemo。

Hooks规则和常见错误

React为Hooks引入了两条严格的规则。第一:只在顶层调用Hooks — 不在条件、循环或嵌套函数内部。第二:只从React函数组件或自定义Hooks中调用Hooks。ESLint插件eslint-plugin-react-hooks自动检查这两条规则。

最常见的错误 — 违反Hooks的调用顺序。React依赖渲染之间Hooks的调用顺序。如果Hook位于条件内部且未在一次渲染中被调用,所有后续Hook都会移位,导致错误。

错误后果解决方案
条件中的HookHooks顺序被打乱将条件逻辑移到Hook内部
Stale closure副作用使用了旧值将所有依赖项添加到数组
无限循环无依赖项的useEffect更改状态指定依赖项或使用useReducer
在渲染中更改ref函数体中的副作用将变更移动到useEffect中

第二个常见错误 — useEffect中的stale closure。如果在副作用中使用了未在依赖项中指定的变量,副作用会在创建闭包时“捕获”其值。解决方案 — 始终将所有使用的变量包含在依赖数组中。如果依赖项过多,请将副作用拆分为多个部分。

常见问题

为什么不能在条件中调用Hooks?

React依赖渲染之间Hooks的调用顺序。如果在条件中调用了Hook且在一次渲染中未执行,所有后续Hook都会“偏移” — 它们的状态会混乱。ESLint规则hooks/exhaustive-deps会对此发出警告。

useEffect和useLayoutEffect有什么区别?

useLayoutEffect是同步的,在浏览器绘制更改之前执行。useEffect是异步的 — 在绘制之后执行。仅在需要在用户看到更改之前测量或更改DOM时才使用useLayoutEffect(弹出窗口位置、滚动恢复)。

如何在卸载时取消fetch请求?

在useEffect内部使用AbortController。创建一个controller,将signal传递给fetch,在清理函数中调用controller.abort()。对于旧版浏览器,使用cancelled标志即可:return () => { cancelled = true; },在setState之前进行检查。

什么是自定义Hook以及何时创建?

自定义Hook — 一个以use为前缀的JavaScript函数,使用内置Hooks。当相同的Hooks逻辑在两个或更多组件中重复时创建。示例:useDebounce、useLocalStorage、useMediaQuery。自定义Hooks取代了HOC和render props。

为什么useEffect在Strict Mode中执行两次?

Strict Mode中,React有意在开发模式下两次挂载和卸载组件,以发现未清理的副作用中的错误。如果清理函数编写正确,双重调用不会导致问题。在生产模式下不会发生这种情况。

总结

  • React Hooks — 用于在函数组件中使用状态、副作用和上下文的函数,取代了类。
  • useState — 用于本地状态的基本Hook,带有用于安全更新的函数式设置器。
  • useEffect — 用于副作用的通用Hook,带有依赖数组和清理函数。
  • useContext — 直接访问React上下文,无需Consumer包装器和额外嵌套。
  • useReducer — 用于具有操作类型的复杂状态逻辑,单个组件内的Redux替代方案。
  • useRef, useMemo, useCallback — 优化工具:DOM引用、值和回调的记忆化。
  • 顶层规则 — 严格限制在条件、循环和嵌套函数之外调用Hooks。

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读