人人都会AI编程

第 8 章 常用 Hooks 全解

更新时间:2026-07-10

Hooks 是 React 16.8 引入的革命性特性,它让函数组件拥有了状态管理和副作用处理能力,彻底改变了 React 的开发模式。本章将深入讲解日常开发中最常用的 Hooks,覆盖基础用法、最佳实践和常见陷阱。

8.1 基础 Hooks

这五个 Hooks 是每个 React 开发者都必须熟练掌握的核心工具,它们构成了函数组件 90% 以上的逻辑。

useState:状态管理基石

useState 为函数组件添加内部状态。当状态更新时,组件会重新渲染。

基本用法:

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>
      点击次数:{count}
    </button>
  );
}

函数式更新: 当新状态依赖于旧状态时,应该使用函数式更新以确保正确性:

setCount(prevCount => prevCount + 1);

为什么需要这样?因为 React 可能会批量处理状态更新,如果直接使用 setCount(count + 1),多次快速点击可能都基于同一次旧值,导致只增加一次。函数式更新保证了每次更新都基于最新状态。

初始化代价较高时: 可以传入一个函数,它只在首次渲染时执行:

const [data, setData] = useState(() => {
  return expensiveComputation();
});

更新对象/数组时的不可变性: 状态应该被视为不可变的,必须创建新对象/数组,而不是在原内存上修改:

// ❌ 错误:直接修改
user.name = 'new name';
setUser(user);

// ✅ 正确:创建新对象
setUser({ ...user, name: 'new name' });

useEffect:副作用与生命周期管理

useEffect 用于处理副作用:数据请求、订阅、定时器、手动 DOM 操作等。它在渲染完成后异步执行,不会阻塞浏览器渲染。

基本模式:

useEffect(() => {
  // 副作用逻辑(如请求数据、注册监听)
  return () => {
    // 清理函数(如取消订阅、清除定时器)
  };
}, [依赖项数组]);

三种常见用法:

  1. 不传依赖项数组 → 每次渲染后都执行
useEffect(() => {
  document.title = `点击次数:${count}`;
});
  1. 空数组 [] → 仅在首次挂载后执行一次(模拟 componentDidMount)
useEffect(() => {
  fetch('/api/user').then(/*...*/);
}, []);
  1. 指定依赖 → 仅当依赖项变化时重新执行
useEffect(() => {
  fetch(`/api/user/${userId}`).then(/*...*/);
}, [userId]);

清理函数的作用: 防止内存泄漏。例如清除定时器、取消待完成的数据请求、移除事件监听:

useEffect(() => {
  const timer = setInterval(() => { /*...*/ }, 1000);
  return () => clearInterval(timer);
}, []);

React 18 开发模式下的双重执行: 在严格模式(StrictMode)下,React 会故意两次挂载组件(只发生在开发环境),帮助你提前发现副作用清理问题。如果清理函数正确,两次执行不会产生 bug。

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

useRef 返回一个可变的 ref 对象,其 .current 属性在整个组件生命周期内保持不变,且修改不会触发重新渲染。它有两个主要用途:

1. 获取 DOM 元素引用

function TextInput() {
  const inputRef = useRef(null);
  useEffect(() => {
    inputRef.current.focus();
  }, []);
  return <input ref={inputRef} />;
}

2. 保存跨渲染的变量

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

  useEffect(() => {
    intervalRef.current = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
    return () => clearInterval(intervalRef.current);
  }, []);

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

这里用 ref 保存定时器 id,因为它不会被闭包捕获旧值,清理函数总能拿到正确的 id。

常见场景:保存上一次的值

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

useContext:跨层级传递数据

useContext 用于读取 React.createContext 提供的上下文值,无需手动通过 props 层层传递。

创建并使用 Context:

const ThemeContext = createContext('light');

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar() {
  const theme = useContext(ThemeContext);
  return <div className={theme}>当前主题:{theme}</div>;
}

当 Provider 的 value 变化时,所有消费该 Context 的组件都会重新渲染。这是“性能隐患”的根源——如果 value 是一个对象,且父组件每次渲染都创建新对象,会导致所有消费者不必要的重渲染。解决方案是使用 useMemo 缓存 value:

const value = useMemo(() => ({ theme, fontSize }), [theme, fontSize]);

8.2 性能优化 Hooks

useMemo:缓存计算结果

useMemo 缓存一个计算量大的“值”,只有当依赖项变化时才重新计算。

function OrderSummary({ items }) {
  const totalPrice = useMemo(() => {
    return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  }, [items]);

  return <div>总价:{totalPrice}</div>;
}

items 不变则直接返回上次缓存的结果,避免每次渲染都执行 reduce。注意: 不要无脑使用 useMemo,它本身也有内存开销,只应用于真正需要避免重复计算的场景,比如复杂排序、大数据量过滤等。简单计算直接用 js 逻辑即可。

useCallback:缓存函数引用

useCallbackuseMemo 类似,但它缓存一个“函数”,只有当依赖项变化时才会返回新函数。主要用于避免子组件不必要的重渲染

当把函数作为 prop 传给使用 React.memo 的子组件时,如果每次渲染都创建新函数,子组件会认为 prop 变化了而重新渲染:

const handleClick = useCallback(() => {
  doSomething(count);
}, [count]);

常见误区: 很多开发者一遇到函数就套 useCallback,但其实只有当子组件使用了 React.memo 且函数作为 prop 时才需要。对于没有 memo 的组件,useCallback 是徒劳的。

useMemo 与 useCallback 的选择

  • 需要缓存值(对象、数组、计算结果)→ useMemo
  • 需要缓存函数(尤其要传给 memo 子组件或作为其他 Hooks 依赖)→ useCallback
  • 两者本质上等价:useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

8.3 进阶 Hooks

useReducer:复杂状态管理的利器

当状态逻辑较复杂(多个子值、下一个状态依赖前一个状态)或涉及多种操作时,useReduceruseState 更清晰。它遵循 Redux 风格。

const initialState = { count: 0 };

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

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
    </>
  );
}

当有多个 useState 相互关联,或者状态更新逻辑分散在多个回调中时,使用 useReducer 可以把更新逻辑集中到 reducer 中,提高可维护性。

useLayoutEffect:同步执行的副作用

useEffect 是异步执行的,不会阻塞浏览器绘制;而 useLayoutEffect 则在 DOM 变更之后同步执行,在浏览器绘制之前完成。几乎 99% 的场景应该用 useEffect,仅在需要读取 DOM 布局并同步修改、避免闪烁时才用 useLayoutEffect

例如:获取滚动位置或元素尺寸后立即调整 UI:

useLayoutEffect(() => {
  const rect = ref.current.getBoundingClientRect();
  setTooltipPosition({ x: rect.right, y: rect.top });
}, []);

如果使用 useEffect 可能会先渲染默认位置再跳变(闪屏)。

useImperativeHandle:暴露自定义组件方法

通常配合 forwardRef 一起使用,让父组件能够调用子组件内部定义的方法。应尽量少用,因为它打破了数据流单向性。典型场景:输入框焦点控制。

const FancyInput = forwardRef((props, ref) => {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} />;
});

// 父组件
function App() {
  const inputRef = useRef();
  return (
    <>
      <FancyInput ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>聚焦</button>
    </>
  );
}

useTransition 和 useDeferredValue:并发渲染优先级控制

React 18 引入了并发特性,这两个 Hooks 用于区分紧急更新和非紧急更新。

useTransition: 返回一个标志 isPending 和一个 startTransition 函数。将耗时的更新包裹在 startTransition 中,React 会将它标记为非紧急,在渲染过程中可以被更紧急的更新(如输入)打断。

function SearchPage() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();

  const handleChange = (e) => {
    // 紧急更新:输入框值
    setQuery(e.target.value);
    // 非紧急更新:搜索结果
    startTransition(() => {
      setSearchResults(filterData(e.target.value));
    });
  };

  return (
    <div>
      <input value={query} onChange={handleChange} />
      {isPending && <Spinner />}
      <ResultList data={searchResults} />
    </div>
  );
}

useDeferredValue: 直接延迟一个值的更新,常用于当父组件传递一个频繁变化的值给子组件,而子组件渲染开销大时。

const deferredQuery = useDeferredValue(query);
return <SlowList query={deferredQuery} />;

React 会优先更新输入框,等主线程空闲后再更新 SlowList,保证交互流畅。

8.4 自定义 Hooks 设计与封装

自定义 Hooks 是复用状态逻辑的最佳方式。它本质上是一个以 use 开头的函数,内部可以调用其他 Hooks。

封装原则:

  1. 单一职责:一个 Hook 只做一件事。
  2. 可配置:通过参数灵活控制行为。
  3. 返回明确:通常返回 [state, action]{ data, loading, error } 等清晰结构。
  4. 命名规范:必须以 use 开头,这样 React 才能检测到调用其他 Hooks。

示例1:数据请求 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 => res.json())
      .then(json => {
        if (!cancelled) {
          setData(json);
          setLoading(false);
        }
      })
      .catch(err => {
        if (!cancelled) {
          setError(err);
          setLoading(false);
        }
      });
    return () => { cancelled = true; };
  }, [url]);

  return { data, loading, error };
}

使用:const { data, loading, error } = useFetch('/api/user'); 即可在任何组件中复用请求逻辑。

示例2:表单输入绑定

function useInput(initialValue) {
  const [value, setValue] = useState(initialValue);
  const onChange = useCallback((e) => setValue(e.target.value), []);
  const reset = useCallback(() => setValue(initialValue), [initialValue]);
  return { value, onChange, reset };
}

常见坑点:

  • 不要在条件语句或循环中调用自定义 Hook(因为它内部调用了 React Hooks,必须遵循 Hooks 规则)。
  • 当 Hook 返回函数(如 dispatchreset)时,尽量用 useCallback 包装以确保引用稳定,避免下游组件无谓重渲染。
  • 注意清理副作用,在 useEffect 返回的清理函数中取消请求、清除定时器等。

自定义 Hooks 的繁荣是 React 社区活力的体现,掌握它可以让你写出高度解耦、可测试的状态逻辑,是实现“逻辑复用”的银弹。