Skip to content

使用ref操作DOM

🌟 什么是DOM Refs?

💡 基本概念

  • 🔮 Refs是脱围机制:让你"跳出"React的声明式更新
  • 🎯 访问DOM节点:获取由React渲染的真实DOM元素引用
  • 🧩 用途:访问浏览器DOM API,如聚焦、滚动、测量等

📝 创建和使用DOM Refs的三个步骤

1️⃣ 引入并创建ref

jsx
import { useRef } from 'react';

function MyComponent() {
  const myRef = useRef(null); // 初始值为null
  // ...
}

2️⃣ 将ref连接到DOM元素

jsx
<div ref={myRef}>内容</div>

3️⃣ 访问和操作DOM节点

jsx
function handleButtonClick() {
  // 通过.current属性访问DOM节点
  myRef.current.scrollIntoView();
  // 使用任何浏览器DOM API
  myRef.current.focus();
  myRef.current.style.opacity = 0.5;
}

🚀 常见使用场景

🔍 聚焦输入框

jsx
function Form() {
  const inputRef = useRef(null);
  
  function handleClick() {
    // 使输入框获得焦点
    inputRef.current.focus();
  }
  
  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleClick}>聚焦输入框</button>
    </>
  );
}

📜 滚动到特定元素

jsx
function CatFriends() {
  const firstCatRef = useRef(null);
  const secondCatRef = useRef(null);
  const thirdCatRef = useRef(null);
  
  function handleScrollToFirstCat() {
    firstCatRef.current.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center'
    });
  }
  
  return (
    <>
      <button onClick={handleScrollToFirstCat}>滚动到Neo</button>
      {/* 其他按钮... */}
      <div>
        <ul>
          <li>
            <img ref={firstCatRef} src="..." alt="Neo" />
          </li>
          {/* 其他图片... */}
        </ul>
      </div>
    </>
  );
}

🎬 控制媒体播放

jsx
function VideoPlayer() {
  const videoRef = useRef(null);
  const [isPlaying, setIsPlaying] = useState(false);
  
  function handlePlayPause() {
    if (isPlaying) {
      videoRef.current.pause();
    } else {
      videoRef.current.play();
    }
    setIsPlaying(!isPlaying);
  }
  
  return (
    <>
      <button onClick={handlePlayPause}>
        {isPlaying ? '暂停' : '播放'}
      </button>
      <video ref={videoRef} src="/video.mp4" />
    </>
  );
}

🔄 管理动态ref列表

🧩 使用ref回调处理列表项

当需要为列表中的每一项都绑定ref,但不知道会有多少项时,可以使用ref回调:

jsx
import { useRef, useState } from "react";

export default function CatFriends() {
  const itemsRef = useRef(null);
  const [catList, setCatList] = useState(setupCatList);

  function scrollToCat(cat) {
    const map = getMap();
    const node = map.get(cat);
    node.scrollIntoView({
      behavior: "smooth",
      block: "nearest",
      inline: "center",
    });
  }

  function getMap() {
    if (!itemsRef.current) {
      // 首次运行时初始化 Map。
      itemsRef.current = new Map();
    }
    return itemsRef.current;
  }

  return (
    <>
      <nav>
        <button onClick={() => scrollToCat(catList[0])}>Neo</button>
        <button onClick={() => scrollToCat(catList[5])}>Millie</button>
        <button onClick={() => scrollToCat(catList[9])}>Bella</button>
      </nav>
      <div>
        <ul>
          {catList.map((cat) => (
            <li
              key={cat}
              ref={(node) => {
                const map = getMap();
                map.set(cat, node);

                return () => {
                  map.delete(cat);
                };
              }}
            >
              <img src={cat} />
            </li>
          ))}
        </ul>
      </div>
    </>
  );
}

function setupCatList() {
  const catList = [];
  for (let i = 0; i < 10; i++) {
    catList.push("https://loremflickr.com/320/240/cat?lock=" + i);
  }

  return catList;
}

⚠️ 注意:在React严格模式下,ref回调会被调用两次,这有助于发现内存泄漏问题。

🧠 访问其他组件的DOM节点

⚠️ 陷阱

Ref是一个脱围机制。手动操作其他组件的DOM节点可能会让代码变得脆弱。

🔍 将ref传递给子组件

默认情况下,组件不会暴露其内部DOM节点的引用。但有时需要访问子组件中的DOM节点,例如让输入框获得焦点。

可以像传递其他prop一样将ref从父组件传递给子组件:

jsx
import { useRef } from 'react';

function MyInput({ ref }) {
  return <input ref={ref} />;
}

function Form() {
  const inputRef = useRef(null);
  
  function handleClick() {
    inputRef.current.focus();
  }
  
  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>聚焦输入框</button>
    </>
  );
}

在上面的例子中,Form组件创建了一个ref并传递给MyInput组件。MyInput组件将这个ref传递给<input>DOM元素。这样,Form组件就可以访问到<input>DOM节点并调用focus()方法。

🛡️ 使用useImperativeHandle限制暴露的API

有时你可能希望限制父组件对子组件DOM节点的访问权限,只暴露特定功能:

jsx
import { useRef, useImperativeHandle } from "react";

function MyInput({ ref }) {
  const realInputRef = useRef(null);
  
  // 只暴露特定方法给父组件
  useImperativeHandle(ref, () => ({
    // 只暴露focus方法,不暴露整个DOM节点
    focus() {
      realInputRef.current.focus();
    }
  }));
  
  return <input ref={realInputRef} />;
}

function Form() {
  const inputRef = useRef(null);
  
  function handleClick() {
    // 只能调用focus(),无法访问其他DOM属性
    inputRef.current.focus();
  }
  
  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>聚焦输入框</button>
    </>
  );
}

在这个例子中,MyInput组件中的realInputRef保存了实际的input DOM节点。而useImperativeHandle指示React将你自己指定的对象作为父组件的ref值。这样,Form组件内的inputRef.current将只有focus方法,而不是整个DOM节点。

⏱️ React何时设置refs

🔄 Ref的更新时机

  • 🏗️ 提交阶段:在DOM更新完成后设置ref.current
  • 🧩 渲染和提交:先完成渲染、更新DOM,然后设置ref
  • ⚠️ 不在渲染期间访问:渲染函数中不应读取或写入ref.current
jsx
// 在提交阶段结束后,ref.current包含DOM节点引用
useEffect(() => {
  // 这里可以安全地访问ref.current
  console.log(myRef.current.offsetHeight);
}, []);

🔄 结合state与refs进行DOM操作

📦 使用flushSync同步更新DOM

当需要在状态更新后立即操作DOM时,可以使用flushSync

jsx
import { useState, useRef } from 'react';
import { flushSync } from 'react-dom';

function TodoList() {
  const listRef = useRef(null);
  const [todos, setTodos] = useState([...]);
  
  function handleAddTodo() {
    const newTodo = { id: nextId++, text: '新待办事项' };
    
    // 使用flushSync确保DOM立即更新
    flushSync(() => {
      setTodos([...todos, newTodo]);
    });
    
    // 此时DOM已更新,可以安全滚动到新添加的项
    listRef.current.lastChild.scrollIntoView({
      behavior: 'smooth'
    });
  }
  
  return (
    <>
      <button onClick={handleAddTodo}>添加</button>
      <ul ref={listRef}>
        {todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
      </ul>
    </>
  );
}

⚠️ 使用Refs操作DOM的注意事项

🛑 避免的操作

  • 🚫 不要修改React管理的DOM结构:可能导致状态不一致
  • 🚫 不要添加/删除React管理的子元素:会与React的渲染冲突
  • 🚫 不要修改内容:优先使用state和props更新内容
jsx
// ❌ 危险操作示例
function DangerousExample() {
  const divRef = useRef(null);
  
  function handleClick() {
    // 直接修改DOM会导致与React状态不同步
    divRef.current.remove(); // 危险!
    
    // 之后如果React尝试更新这个元素,会导致错误
  }
  
  return (
    <>
      <button onClick={handleClick}>删除元素</button>
      <div ref={divRef}>这个元素会被直接从DOM中移除</div>
    </>
  );
}

✅ 安全的操作

  • 聚焦/失焦element.focus()element.blur()
  • 滚动element.scrollIntoView()scrollTop
  • 测量尺寸getBoundingClientRect()
  • 播放/暂停:媒体元素的play()pause()
  • 非破坏性样式调整:动画、过渡等
jsx
function SafeExample() {
  const divRef = useRef(null);
  
  // ✅ 安全的DOM操作
  function handleButtonClick() {
    // 测量
    const dimensions = divRef.current.getBoundingClientRect();
    console.log(dimensions);
    
    // 滚动
    divRef.current.scrollIntoView({ behavior: 'smooth' });
    
    // 动画
    divRef.current.style.transition = 'background-color 1s';
    divRef.current.style.backgroundColor = 'lightblue';
  }
  
  return (
    <>
      <button onClick={handleButtonClick}>安全操作</button>
      <div ref={divRef} style={{ height: '100px' }}>内容</div>
    </>
  );
}

📝 总结

  • 🔮 Refs作用:访问React管理的DOM节点
  • 🛠️ 基本用法useRef + ref属性连接DOM元素
  • 🚪 访问方式:通过ref.current访问真实DOM节点
  • 🔄 组件传递ref:可以像传递普通prop一样传递ref给子组件
  • 🛡️ 限制访问:使用useImperativeHandle控制暴露的API
  • 📋 动态列表:使用ref回调处理多个列表项的引用
  • ⏱️ 更新时机:在提交阶段后设置refs,不在渲染期间访问
  • 🔄 同步DOM:使用flushSync确保状态更新后立即更新DOM
  • ⚠️ 安全使用:只进行非破坏性操作,不修改React管理的DOM结构

参考:React官方文档-使用ref操作DOM