字节笔记本

2026年3月22日

React Hooks 完全指南

这篇文章将全面介绍 React Hooks 的核心概念和常用 Hook,包括 useState、useEffect、useContext、useReducer、useRef、useMemo、useCallback 等,并通过实际场景示例讲解 Hooks 的使用技巧和注意事项。

useState

useState 是最基本的 Hook,用于在函数组件中添加状态管理。

jsx
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(count - 1)}>-1</button>
    </div>
  );
}

函数式更新

当新状态依赖前一个状态时,使用函数式更新:

jsx
setCount(prevCount => prevCount + 1);

惰性初始化

初始状态需要复杂计算时,传入函数避免每次渲染都重新计算:

jsx
const [state, setState] = useState(() => {
  return expensiveComputation();
});

注意事项

  • 状态更新是异步的,不会立即反映
  • 状态更新会触发组件重新渲染
  • 多次连续更新会被合并(在事件处理函数中)

useEffect

useEffect 用于处理副作用,如数据获取、订阅、手动修改 DOM 等。

jsx
import { useEffect } from 'react';

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

  useEffect(() => {
    const timer = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);

    return () => clearInterval(timer); // 清理函数
  }, []); // 空依赖数组,仅挂载时执行

  return <p>已运行 {seconds} 秒</p>;
}

依赖数组

依赖数组行为
不传每次渲染后都执行
[]仅在组件挂载和卸载时执行
[a, b]当 a 或 b 变化时执行

常见使用场景

数据获取:

jsx
useEffect(() => {
  const fetchData = async () => {
    const res = await fetch('/api/data');
    const data = await res.json();
    setData(data);
  };
  fetchData();
}, []);

订阅事件:

jsx
useEffect(() => {
  const handleResize = () => {
    setWidth(window.innerWidth);
  };
  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, []);

useEffect 常见问题

问题 1:无限循环

jsx
// 错误:每次渲染都触发 effect,effect 中更新状态又触发渲染
useEffect(() => {
  setData(fetchData());
}); // 缺少依赖数组

// 正确
useEffect(() => {
  setData(fetchData());
}, []); // 仅执行一次

问题 2:闭包陷阱

jsx
// 错误:setInterval 中的 count 永远是初始值
useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1); // count 是闭包中的旧值
  }, 1000);
  return () => clearInterval(id);
}, []);

// 正确:使用函数式更新
useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);
  return () => clearInterval(id);
}, []);

问题 3:依赖数组的引用陷阱

jsx
// 错误:每次渲染 options 都是新的对象引用
useEffect(() => {
  fetchData(options);
}, [options]); // 无限循环

// 正确:使用 useMemo 或将对象放在 effect 内部
const stableOptions = useMemo(() => ({ page: 1 }), []);
useEffect(() => {
  fetchData(stableOptions);
}, [stableOptions]);

useContext

useContext 用于跨组件传递数据,避免 props 逐层传递。

jsx
import { createContext, useContext } from 'react';

const ThemeContext = createContext('light');

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

function Toolbar() {
  return <ThemedButton />;
}

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <button className={theme}>按钮</button>;
}

多 Context 组合

jsx
function App() {
  return (
    <ThemeContext.Provider value="dark">
      <AuthContext.Provider value={{ user, login, logout }}>
        <Layout />
      </AuthContext.Provider>
    </ThemeContext.Provider>
  );
}

useReducer

useReducer 用于管理复杂状态逻辑,是 useState 的替代方案。

jsx
import { useReducer } from 'react';

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 initialState;
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>重置</button>
    </div>
  );
}

useState vs useReducer

场景推荐
简单状态(字符串、数字)useState
多个相关联的状态useReducer
下一个状态依赖前一个useReducer
状态逻辑复杂useReducer

useRef

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数。

访问 DOM 元素

jsx
function TextInputWithFocusButton() {
  const inputEl = useRef(null);

  const onButtonClick = () => {
    inputEl.current.focus();
  };

  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

保存跨渲染的可变值

jsx
function Timer() {
  const timerRef = useRef(null);

  const start = () => {
    timerRef.current = setInterval(() => {
      console.log('tick');
    }, 1000);
  };

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

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

useRef vs useState

特性useRefuseState
变更时触发重渲染
值的可变性可变不可变(需 setter)
适用场景DOM 引用、定时器 IDUI 状态

useMemo

useMemo 缓存计算结果,避免每次渲染都重新计算。

jsx
function ExpensiveComponent({ items, filter }) {
  const filteredItems = useMemo(() => {
    return items.filter(item => item.type === filter);
  }, [items, filter]);

  return (
    <ul>
      {filteredItems.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
}

使用原则

  • 只在计算开销较大时使用
  • 不要把 useMemo 作为语义保证
  • 过度使用反而会降低性能

useCallback

useCallback 缓存函数引用,避免子组件不必要的重渲染。

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

  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);

  return <Child onClick={handleClick} />;
}

const Child = React.memo(function Child({ onClick }) {
  return <button onClick={onClick}>子组件</button>;
});

useMemo vs useCallback

  • useMemo 缓存值:useMemo(() => compute(a, b), [a, b])
  • useCallback 缓存函数:useCallback(fn, [deps]) 等同于 useMemo(() => fn, [deps])

useImperativeHandle

useImperativeHandle 配合 forwardRef 自定义暴露给父组件的实例值。

jsx
import { forwardRef, useImperativeHandle, useRef } from 'react';

const FancyInput = forwardRef(function FancyInput(props, ref) {
  const inputRef = useRef(null);

  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    clear: () => {
      inputRef.current.value = '';
    },
  }));

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

function Parent() {
  const fancyInputRef = useRef(null);

  return (
    <>
      <FancyInput ref={fancyInputRef} />
      <button onClick={() => fancyInputRef.current.focus()}>
        聚焦输入框
      </button>
      <button onClick={() => fancyInputRef.current.clear()}>
        清空输入框
      </button>
    </>
  );
});

自定义 Hook

将组件逻辑抽取为可复用的函数:

jsx
function useWindowSize() {
  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 size;
}

// 使用
function ResponsiveComponent() {
  const { width, height } = useWindowSize();
  return <p>窗口大小: {width} x {height}</p>;
}

Hooks 使用规则

  1. 只在顶层调用:不要在循环、条件或嵌套函数中调用 Hook
  2. 只在 React 函数中调用:函数组件或自定义 Hook 中使用,普通 JS 函数不行
  3. 依赖数组要完整:useEffect 和 useCallback 的依赖数组应包含所有用到的外部变量
  4. 使用 ESLint 插件eslint-plugin-react-hooks 可以自动检查规则违规

Hooks 速查表

Hook用途
useState状态管理
useEffect副作用处理
useContext跨组件数据传递
useReducer复杂状态管理
useRefDOM 引用 / 可变值
useMemo缓存计算结果
useCallback缓存函数引用
useImperativeHandle自定义 ref 暴露
useLayoutEffectDOM 变更后同步执行
useDebugValue自定义 Hook 调试标签
分享: