«

一文讲清 16 个 React 常用钩子的机制和使用场景

时间:2026-4-24 14:26     作者:独元殇     分类: 前端技术


欢迎关注我的公众号,名叫「串串狗小刊」

[toc]

今天我整理一下笔记,把我以前记录的下面几个 react 钩子的概念和使用方式、场景简单的过一下。

是我以前在 dbuke 这个在线教程网站上学习时候做的笔记。现在这个网站貌似打不开了。

一共 6 类,16 个。这应该是涵盖 react 使用里的所有的常用钩子了(没包括一些 react 19 的),涵盖 React 日常使用!本文是手打的,不是 AI 文。

/ 01 / State Management(状态管理)
useState - 定义组件状态
useReducer - 复杂状态管理
useSyncExternalStore - 订阅外部数据源

/ 02 / Effect Hooks(副作用)
useEffect - 处理副作用
useLayoutEffect - 同步执行副作用
useInsertionEffect - 注入样式专用

/ 03 / Ref Hooks(引用)
useRef - 创建可变引用
useImperativeHandle - 暴露子组件方法

/ 04 / Performance Hooks(性能优化)
useMemo - 缓存计算结果
useCallback - 缓存函数引用
Context Hooks(上下文)
useContext - 读取上下文值

/ 05 / Transition Hooks(过渡)
useTransition - 标记非紧急更新
useDeferredValue - 延迟更新状态值

/ 06 / Random Hooks(杂项)
useDebugValue - 自定义 Hook 调试
useId - 生成唯一 ID

一共 6 类,16 个。

不深入细讲,只是简单每个都说说。

State Management(状态管理)

先说我们最常用的钩子。useState 是用到最频繁的一个。

因为 React 诞生的重要目的就是管理状态,统一管理应用状态!也就是在状态发生变化时,自动重新渲染组件。

useState - 定义组件状态

const [value, setValue] = useState('');  // 设置一个状态初始值

const handleChange = (e) => {  
  setValue(e.target.value);  // 事件一激活,就改值
};

<input
  type="text"
  value={value}
  onChange={handleChange}
/>

这个值存在第一个变量 value 里,第二个变量 setValue 是一个修改状态的函数。上面这个例子,我们在 input 里面输入什么,它就马上等于什么了。

useReducer - 复杂状态管理

useReducer 是另一个状态钩子,可以管理比 useState 更复杂的状态。这个用的少,但更强大。

const reducer = (state, action) => {
  switch (action) {
    case 'increment':
      return state + 1;
    default:
      return state;
  }
};

const [count, dispatch] = useReducer(reducer, 0);

<button onClick={() => dispatch('increment')}>Increment</button>

这个 dispatch 新函数,里面有个关键字 action,可以接收我们传入给 dispatch 的参数,然后做出更灵活的变化。

尤其是游戏界面这种,界面复杂、交互复杂的,会更好用。

useSyncExternalStore - 订阅外部数据源

然后是 useSyncExternalStore,这个钩子比较冷门。它可以把非 React 的状态,接入到 React 组件中。

Effect Hooks(副作用)

useEffect - 处理副作用

副作用钩子 useEffect,指的是组件渲染中与渲染结果无关联、但会影响环境状态的一些操作。

const [count, setCount] = useState(0);

useEffect(() => {
  document.title = `你点击了 ${count} 次`;
}, [count]);

<button onClick={() => setCount(count + 1)}>
  Click me
</button>

什么意思呢?看上面这段代码,useEffect 的第二个参数里传入了 count,那么只要这个 count 变化,就会执行里面的函数。

注意,useEffect 不应该用于”点击按钮时执行”、”获取数据时执行”这种场景。

其实并不需要经常使用这个钩子。一般我们用 useEffect 是在需要让 React 与浏览器 API 同步的时候,比如用 React 控制视频(或音频)的播放、暂停:

const ref = useRef(null); // 创建"引用",抓住视频 DOM 元素 
useEffect(() => {
  if (isPlaying) {
    ref.current.play();  // 播放
  } else {
    ref.current.pause();  // 暂停
  }
}, [isPlaying]);

<video ref={ref} src={src} loop playsInline />

当我们把 isPlaying 设置成 true 时,视频就开始播放了。这种,才是 useEffect 真正合适的场景!

useLayoutEffect - 同步执行副作用

其实还有个差不多的钩子,某些场景下更好用,叫 useLayoutEffect。它是在界面渲染之前就执行的。使用例子,在渲染前就获取某元素的高度:

const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0);

useLayoutEffect(() => {
  const { height } = ref.current.getBoundingClientRect();  // 获取提示框高度
  setTooltipHeight(height);  // 设置高度
}, []);

厉害吧!用它,浏览器还没开始渲染这个提示框,我们就获取到了这个提示框的高度,然后保存在状态里。

非常好用,可以防止页面闪烁跳跃,懂吧?

useInsertionEffect - 注入样式专用

既然比 useEffect 早,对吧?其实还有更早的。

还有一个很小众的钩子 useInsertionEffect,它专门为 CSS 和 JS 的库开发。这个运行得更早,比 useLayoutEffect 还早!专门用来确保所有的 CSS 样式都能应用到正确的组件元素上。

Ref Hooks(引用)

useRef - 创建可变引用

说说 useRef 钩子吧。

它类似于 useState,也能存状态,不过它不会触发组件渲染,所以更简单了!useRef 叫引用。

引用是可变的,而状态是不可变的。因此能使用等号运算符直接修改,比如 ref.current = 'something'。下面是一个计时器使用例子:

// 一个计时器使用案例
const [timer, setTimer] = useState(0);
const intervalRef = useRef(); // useRef 用来保存 setInterval 的 ID

const startTimer = () => {
  intervalRef.current = setInterval(() => {  // 存进去了
    setTimer((prevTimer) => prevTimer + 1);
  }, 1000);
};

const stopTimer = () => {
  clearInterval(intervalRef.current);  // 停止时就能用到
};

<p>Timer: {timer} seconds</p>
<button onClick={startTimer}>开始计时</button>
<button onClick={stopTimer}>停止计时</button>

你可能会问,为什么不直接用变量呢?因为 React 组件一更新,变量就没了…… 所以使用 React 内部的值,肯定是有用的!

useImperativeHandle - 暴露子组件方法

有个略冷门的钩子:useImperativeHandle。

转发一个引用的时候会用到。比如:

function ParentComponent() {
  const inputRef = useRef();

  const handleFocus = () => {
    inputRef.current.focus();
  };

  // ...

  return (
    <>
      <CustomInput ref={inputRef} />
      <button onClick={handleFocus}>激活 input</button>
    </>
  );
}

// 上面的代码肯定无法运行
// 因此我们使用 forwardRef + useImperativeHandle,把方法暴露出去
// 把子组件传递给父组件,然后就可以用了
const CustomInput = forwardRef((props, ref) => {
  const inputRef = useRef();

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

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

因为直接给自定义组件(如 CustomInput)传 ref,默认是拿不到 DOM 元素的。

Performance Hooks(性能优化)

useMemo - 缓存计算结果

再说说 useMemo 这个钩子。

它用一种叫”记忆化”的方法来缓存之前的结果,以此提升应用性能。它只会在依赖项发生变化时重新计算缓存的值,非常适合执行高消耗的计算,举个例子:

function SumComponent({ numbers }) {
  const sum = useMemo(() => {  // 把求和的结果缓存起来
    return numbers.reduce((total, n) => total + n, 0);
  }, [numbers]);

  return <h1>合计: {sum}</h1>;
}

把求和的结果存起来,组件再渲染时就不会重复计算了。(只有 numbers 变化时,才会重新缓存)

useCallback - 缓存函数引用

和 useMemo 差不多,有个叫 useCallback 的钩子。

这个能缓存函数!!

比如这个例子:

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

  // 单击后会执行
  const increment = useCallback(() => {
    setCount((c) => c + 1);
  }, []);

  return (
    <>
      <div>{count}</div>
      <Button onClick={increment} />
    </>
  );
}

// button 组件
function Button({ onClick }) {
  return <button onClick={onClick}>Click me</button>;
}

因为有个问题:每次重新渲染,里面的函数都会被重新创建。那我们只要 useCallback 一下,下次再渲染,就不用重复创建 increment 函数了。

useContext - 读取上下文值

再说说 useContext 这个钩子吧。它能读取上下文(可理解为 React 版全局变量)的值:

const ThemeContext = createContext();

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

function Form() {
  const theme = useContext(ThemeContext);
  // ...
}

createContext 创建了一个值(React 版全局变量),然后这个值通过 Provider 方法给里面填充(value 放什么,就是什么),之后我们在另一个组件里(比如 Form 里)使用 useContext 就可以访问到了。

这个常用于主题逻辑。

Transition Hooks(过渡)

useTransition - 标记非紧急更新

再说说 useTransition 钩子。

这是一个过渡钩子,我们可以用它来指定某些状态的更新。这个在处理涉及大量计算的状态更新时用处很大!因为状态更新如果被立即执行,可能会导致不良的用户体验。什么意思呢?比如这个例子:

const [filter, setFilter] = useState('');
const [inputValue, setInputValue] = useState('');
const [isPending, startTransition] = useTransition();

const filteredItems = items.filter(item => item.includes(filter));

<input
  value={inputValue}
  onChange={(event) => {
    setInputValue(event.target.value);
    startTransition(() => {
      setFilter(event.target.value);
    });
  }}
  placeholder="请填充内容...."
/>

{isPending ? (
  <p>加载中...</p>
) : (
  filteredItems.map(item => <div key={item}>{item}</div>)
)}

根据用户输入,来过滤一个列表。

但是在输入框中输入时,每次输入都重新渲染,会导致 UI 卡顿。

然后我们就用 useTransition 这个钩子里的 startTransition 包裹一下,这段逻辑就被标记为”非紧急”了。

另一个值 isPending 是一个布尔值,告诉你当前是否还有状态在等待中,这样我们就可以在状态更新完成之前,给用户显示一个”加载中……”。

useDeferredValue - 延迟更新状态值

再说说 useDeferredValue 这个钩子。

它和 useTransition 很相似,不过它可以在最合适的时机更新状态。

const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)

一个完整的例子如下:

function FilteredList({ items }) {
  const [inputValue, setInputValue] = useState('');
  const deferredFilter = useDeferredValue(inputValue);

  const filteredItems = items.filter(item =>
    item.toLowerCase().includes(deferredFilter.toLowerCase())
  );

  return (
    <>
      <input
        value={inputValue}
        onChange={(event) => setInputValue(event.target.value)}
        placeholder="Type to filter..."
      />
      {filteredItems.map(item => <div key={item}>{item}</div>)}
    </>
  );
}

useDeferredValue 很适合像过滤列表这样的场景。无需你手动设置过渡,也不需要任何异步状态。

Random Hooks(杂项)

useId - 生成唯一 ID

说一下 useId 钩子,这是一个随机钩子。

调用它时,会创建一个唯一的 ID。

它一般用于在表单输入框之间共享的 id。什么意思呢?看看这个例子:

function EmailInput({ name }) {
  const id = useId();

  return (
    <>
      <label htmlFor={id}>{name}</label>
      <input id={id} type="email" />
    </>
  );
}

function Form() {
  return (
    <form>
      <EmailInput name="Email" />
      <EmailInput name="Confirm Email" />
    </form>
  );
}

看下面的 Form 里,两次用了 EmailInput 组件,为了防止里面的 input 冲突,于是我们就用 useId 钩子。

标签: 原创 React