在 React 开发中,useStateuseRef 是我们最常用的两个 Hook。
大部分人会把它们单独理解:

  • useState —— 驱动组件更新的状态存储器
  • useRef —— 存储不会触发更新的可变引用

但在实际项目里,这两个 Hook 常常需要结合起来使用,才能同时满足“响应式更新”和“高性能引用”的双重需求。本文将从基础到进阶,带你理解 useState + useRef 的最佳实践


一、基础回顾

useState

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

setCount(count + 1); // 更新 state 会触发重新渲染

特点:

  • 值改变会导致组件重新渲染。
  • 适合用来存放需要影响 UI 的数据。

useRef

const timer = useRef<number | null>(null);

useEffect(() => {
  timer.current = window.setInterval(() => {
    console.log("tick");
  }, 1000);

  return () => clearInterval(timer.current!);
}, []);

特点:

  • .current 的值改变不会触发重新渲染。
  • 常用于保存 DOM 节点定时器 ID外部库实例等。

二、常见问题:单独使用时的缺陷

需求: 保存数据
仅用 useState?
频繁 re-render,性能浪费
仅用 useRef?
UI 不会自动更新
需要 useState + useRef 结合

三、useState + useRef 的典型结合模式

1. 状态和快照并存

import { useState, useRef } from "react";

function InputLogger() {
  const [text, setText] = useState("");       // UI 驱动
  const latestText = useRef(text);            // 快照缓存

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value);
    latestText.current = e.target.value;      // 保存最新值,不触发额外渲染
  };

  const logLatest = () => {
    console.log("最新输入(不依赖渲染):", latestText.current);
  };

  return (
    <div>
      <input value={text} onChange={handleChange} />
      <button onClick={logLatest}>打印最新输入</button>
    </div>
  );
}

2. 避免闭包陷阱

function TimerCounter() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

  useEffect(() => {
    countRef.current = count; // 每次渲染时同步最新值
  }, [count]);

  useEffect(() => {
    const timer = setInterval(() => {
      setCount(countRef.current + 1); // 避免闭包问题
    }, 1000);

    return () => clearInterval(timer);
  }, []);

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

流程示意图:

Timer count(state) countRef(ref) countRef count UI 读取最新 .current setCount(current+1) 触发重新渲染 更新 .current Timer count(state) countRef(ref) countRef count UI

四、设计模式总结

场景 推荐 Hook 原因
驱动 UI 更新 useState 渲染绑定
缓存不影响渲染的数据 useRef 性能优化
异步/闭包中保持最新值 useState + useRef 避免 stale closure
存储第三方库实例、DOM 节点 useRef 生命周期稳定
既要 UI 更新又要缓存 useState + useRef 状态+快照模式

五、真实业务案例:输入框防抖

我们来模拟一个常见的业务场景:
一个输入框,每次输入都需要保存最新值。

  • 方案 A:仅用 useState
    • 每次输入都 setState,组件频繁 re-render。
  • 方案 B:仅用 useRef
    • 性能很好,但 UI 不会自动更新。
  • 方案 C:useState + useRef 结合
    • UI 更新频率受控,缓存值随时可用。

性能实验代码(简化版)

function PerformanceTest() {
  const [text, setText] = useState("");       // UI 绑定
  const refText = useRef("");                 // 快照缓存
  const renderCount = useRef(0);              // 渲染计数器

  renderCount.current++;

  const handleChangeState = (e: React.ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value);                  // 方案 A
  };

  const handleChangeRef = (e: React.ChangeEvent<HTMLInputElement>) => {
    refText.current = e.target.value;         // 方案 B
  };

  const handleChangeHybrid = (e: React.ChangeEvent<HTMLInputElement>) => {
    refText.current = e.target.value;         // 缓存
    if (e.target.value.length % 5 === 0) {
      setText(refText.current);               // 方案 C,控制更新频率
    }
  };

  return (
    <div>
      <p>渲染次数:{renderCount.current}</p>
      <input onChange={handleChangeState} placeholder="仅 useState" />
      <input onChange={handleChangeRef} placeholder="仅 useRef" />
      <input onChange={handleChangeHybrid} placeholder="useState + useRef" />
    </div>
  );
}

对比结果

假设用户输入 100 次字符:

方案 渲染次数 UI 是否实时更新 性能表现
仅 useState 100 次 ✅ 是 ❌ 低(频繁 re-render)
仅 useRef 1 次(初始) ❌ 否 ✅ 高(但 UI 不更新)
useState + useRef 20 次(按需触发) ✅ 是 ⚡ 平衡(最佳实践)

六、整体思维导图

useState + useRef
useState
useRef
结合
触发渲染
UI 驱动
适合显示状态
不触发渲染
快照缓存
DOM/实例存储
状态+快照
避免闭包陷阱
高性能优化

七、结语

  • useStateuseRef 并不是对立的,而是互补的。
  • useState 驱动视图,useRef 保存快照,两者结合能覆盖更多场景。
  • 在实际业务里:
    • UI 驱动用 useState
    • 缓存 & 性能优化用 useRef
    • 异步/闭包用 二者结合

理解好它们的配合方式,你的 React 代码会更高效、更优雅。 🚀

更多推荐