React useRef Hook 完全指南:从 DOM 操作到持久化存储的实战解析

58 阅读5分钟

在 React Hooks 体系中,useState 是最常用的状态管理工具,但它有一个特性:每次状态变化都会触发组件重渲染。如果我们只是想“保存一个可变值”而不希望引起渲染,就需要另一个 Hook —— useRef。

useRef 返回一个可变的 ref 对象,其 .current 属性可以存储任意值,并且在整个组件生命周期内保持引用不变,最重要的是:改变 .current 不会触发重渲染。这使得它成为 React 中处理 DOM 操作、持久化变量、性能优化的利器。

本文将结合多个真实场景代码示例,系统讲解 useRef 的用法、与 useState 的区别,以及在受控/非受控组件中的应用,帮助你彻底掌握这个“默默奉献”的 Hook。

useRef 基础:创建一个持久化的可变引用

jsx

import { useRef } from 'react';

function App() {
  const inputRef = useRef(null); // 初始值 null

  return <input ref={inputRef} />;
}
  • useRef(initialValue) 创建一个 ref 对象。
  • 返回的对象结构:{ current: initialValue }。
  • .current 可在整个生命周期内随意读写,且不会触发渲染。
  • 常与 ref 属性配合,用于获取 DOM 节点。

场景一:DOM 操作 —— 自动聚焦输入框

这是 useRef 最经典的应用:获取 DOM 元素并调用原生方法。

jsx

import { useRef, useEffect } from 'react';

export default function App() {
  const inputRef = useRef(null);

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

  return <input ref={inputRef} placeholder="自动获得焦点" />;
}

相当于 Vue 中的 onMounted(() => inputEl.focus())。

为什么不用 useState?因为我们不需要根据焦点状态来渲染任何 UI,单纯的操作 DOM 而已,使用 useRef 更合适。

场景二:持久化变量 —— 定时器 ID 管理

在处理 setInterval、setTimeout、WebSocket 等需要清理的副作用时,我们常常需要保存一个可变值,并在组件卸载或多次点击时访问它。

jsx

import { useRef, useState } from 'react';

export default function App() {
  const intervalId = useRef(null);
  const [count, setCount] = useState(0);

  const start = () => {
    if (intervalId.current) return; // 防止重复启动

    intervalId.current = setInterval(() => {
      console.log('tick~~~~~');
      setCount(c => c + 1);
    }, 1000);
  };

  const stop = () => {
    if (intervalId.current) {
      clearInterval(intervalId.current);
      intervalId.current = null;
    }
  };

  return (
    <>
      <p>计数:{count}</p>
      <button onClick={start}>开始计时</button>
      <button onClick={stop}>停止计时</button>
    </>
  );
}

为什么不用 useState 存储 intervalId?

  • 如果用 useState,每次 setIntervalId 都会触发重渲染(即使值没变)。
  • 更严重的是:函数组件每次渲染都会重新执行,如果在渲染中访问旧的 id,可能出现闭包陷阱。
  • useRef 确保 .current 始终指向最新值,且修改不触发渲染,完美适合这种场景。

useRef vs useState:核心区别对比

特性useStateuseRef
返回值[state, setState]{ current: value }
修改是否触发渲染
典型用途驱动 UI 渲染的状态保存不影响渲染的可变值、DOM 引用
闭包安全性需注意 stale closure.current 始终是最新的
初始值传入初始值传入初始值(仅在首次渲染使用)

简单记忆:需要渲染用 useState,不需要渲染用 useRef

场景三:受控组件 vs 非受控组件 —— 表单值获取方式

表单处理是 React 中另一个常见战场,useRef 在这里大放异彩。

受控组件(Controlled Components)

值由 React 状态控制,实时同步,适合需要即时验证、联动、展示的场景。

jsx

import { useState } from 'react';

function LoginForm() {
  const [form, setForm] = useState({
    username: '',
    password: '',
  });

  const handleChange = (e) => {
    setForm({
      ...form,
      [e.target.name]: e.target.value,
    });
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('提交数据:', form);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="username"
        value={form.username}
        onChange={handleChange}
        placeholder="用户名"
      />
      <input
        type="password"
        name="password"
        value={form.password}
        onChange={handleChange}
        placeholder="密码"
      />
      <button type="submit">登录</button>
    </form>
  );
}

优点:数据始终与状态一致,便于校验、禁用按钮、实时预览等。

缺点:每次输入都触发重渲染(可通过防抖优化)。

非受控组件(Uncontrolled Components)

值由 DOM 自身管理,通过 ref 在需要时读取,适合一次性提交、文件上传等场景。

jsx

import { useRef } from 'react';

function CommentBox() {
  const textareaRef = useRef(null);

  const handleSubmit = () => {
    const comment = textareaRef.current.value.trim();
    if (!comment) {
      alert('请输入评论');
      return;
    }
    console.log('提交评论:', comment);
    textareaRef.current.value = ''; // 清空
  };

  return (
    <div>
      <textarea ref={textareaRef} placeholder="写下你的评论..." />
      <button onClick={handleSubmit}>提交评论</button>
    </div>
  );
}

优点:性能更好(无频繁渲染),代码更简洁。

缺点:无法实时获取值或做即时验证。

混合使用示例

实际项目中两者经常结合:

jsx

import { useState, useRef } from 'react';

function App() {
  const [value, setValue] = useState(''); // 受控:实时显示输入内容
  const inputRef = useRef(null);           // 非受控:提交时获取原生值

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('受控值:', value);
    console.log('非受控值:', inputRef.current.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <p>你输入了:{value}</p>
      <input value={value} onChange={(e) => setValue(e.target.value)} />
      <input ref={inputRef} placeholder="这个不会实时显示" />
      <button type="submit">提交</button>
    </form>
  );
}

最佳实践与注意事项

  1. 优先使用受控组件:React 官方推荐,大多数表单场景应使用受控方式,便于状态管理。

  2. 非受控适用于特定场景

    • 文件上传( 无法受控)
    • 与第三方库集成(如富文本编辑器)
    • 性能极致优化的大表单
  3. 避免滥用 useRef 存储大量状态:如果值需要驱动渲染,请用 useState 或其他状态管理方案。

  4. useRef 初始值只在首次渲染使用:后续渲染不会重新赋值。

  5. 清理副作用:在 useEffect 返回清理函数中清除 ref 存储的定时器、订阅等。

结语

useRef 虽然不像 useState 那样耀眼,但它在 React 开发中扮演着不可或缺的角色:

  • 它让我们能安全地操作 DOM。
  • 它为定时器、订阅等副作用提供了可靠的存储容器。
  • 它是非受控组件的核心支撑。
  • 它帮助我们避免不必要的重渲染,提升性能。

掌握 useRef,就等于掌握了 React 中“非响应式可变值”的处理艺术。无论是初学者还是有经验的开发者,都应该熟练运用这个强大的 Hook。

下次遇到“需要保存一个值但不想重渲染”的需求时,请第一时间想到 useRef —— 它永远在那里,默默守护你的组件性能和逻辑清晰。