说说对 React refs 的理解?应用场景?

一、是什么

Refs(引用)是 React 提供的一种"逃生舱"机制,允许开发者直接访问 DOM 元素或组件实例,绑定不会触发重新渲染的可变值。

在 React 的声明式编程模型中,绝大多数交互都可以通过 state 和 props 来完成。但某些场景确实需要直接操作 DOM 或持有可变引用,这时就需要 refs。

Refs 的核心特性:

  • 修改 ref 的值不会触发组件重新渲染
  • ref 的值在组件的整个生命周期内保持不变(同一个对象引用)
  • 可以在渲染之间持久化存储任意可变值

二、创建和使用 Refs

useRef(函数组件)

useRef 是函数组件中创建 ref 的标准方式:

import { useRef, useEffect } from 'react';

function TextInput() {
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    inputRef.current?.focus();
  }, []);

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

useRef 返回一个 { current: T } 对象。将这个对象赋给 JSX 元素的 ref 属性后,React 会在挂载时将 DOM 节点赋值给 current,卸载时设为 null

createRef(类组件)

在类组件中使用 React.createRef()

class TextInput extends React.Component {
  inputRef = React.createRef<HTMLInputElement>();

  componentDidMount() {
    this.inputRef.current?.focus();
  }

  render() {
    return <input ref={this.inputRef} placeholder="自动聚焦" />;
  }
}

注意:createRef 每次调用都会创建新的 ref 对象,在函数组件中不应使用(每次渲染都会重新创建)。

回调 Refs

回调 ref 是一个函数,React 在挂载时传入 DOM 节点,卸载时传入 null

function MeasuredComponent() {
  const [height, setHeight] = useState(0);

  const measuredRef = useCallback((node: HTMLDivElement | null) => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  }, []);

  return (
    <div>
      <div ref={measuredRef} style={{ padding: 20 }}>
        <p>这段内容的高度会被测量</p>
        <p>可以包含动态内容</p>
      </div>
      <p>内容高度:{height}px</p>
    </div>
  );
}

回调 ref 的优势在于可以在 ref 设置时立即执行逻辑,适合需要在元素挂载时进行测量或初始化的场景。

三、forwardRef——转发 Ref

默认情况下,函数组件不接受 ref prop。要让父组件访问子组件内部的 DOM 元素,需要使用 forwardRef

import { forwardRef, useRef } from 'react';

interface ButtonProps {
  children: React.ReactNode;
  variant?: 'primary' | 'secondary';
}

const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  function Button({ children, variant = 'primary' }, ref) {
    return (
      <button ref={ref} className={`btn btn-${variant}`}>
        {children}
      </button>
    );
  }
);

function Toolbar() {
  const btnRef = useRef<HTMLButtonElement>(null);

  const handleFocusButton = () => {
    btnRef.current?.focus();
  };

  return (
    <div>
      <Button ref={btnRef} variant="primary">
        提交
      </Button>
      <button onClick={handleFocusButton}>聚焦提交按钮</button>
    </div>
  );
}

在 React 19 中,ref 可以作为普通 prop 传递,不再强制需要 forwardRef

function Button({ children, ref }: ButtonProps & { ref?: React.Ref<HTMLButtonElement> }) {
  return <button ref={ref}>{children}</button>;
}

四、useImperativeHandle——自定义暴露的实例值

useImperativeHandle 允许子组件自定义通过 ref 暴露给父组件的值,而不是直接暴露 DOM 节点:

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

interface VideoPlayerHandle {
  play: () => void;
  pause: () => void;
  seek: (time: number) => void;
  getCurrentTime: () => number;
}

const VideoPlayer = forwardRef<VideoPlayerHandle, { src: string }>(
  function VideoPlayer({ src }, ref) {
    const videoRef = useRef<HTMLVideoElement>(null);

    useImperativeHandle(ref, () => ({
      play() {
        videoRef.current?.play();
      },
      pause() {
        videoRef.current?.pause();
      },
      seek(time: number) {
        if (videoRef.current) {
          videoRef.current.currentTime = time;
        }
      },
      getCurrentTime() {
        return videoRef.current?.currentTime ?? 0;
      },
    }));

    return <video ref={videoRef} src={src} />;
  }
);

function VideoApp() {
  const playerRef = useRef<VideoPlayerHandle>(null);

  return (
    <div>
      <VideoPlayer ref={playerRef} src="/video.mp4" />
      <div className="controls">
        <button onClick={() => playerRef.current?.play()}>播放</button>
        <button onClick={() => playerRef.current?.pause()}>暂停</button>
        <button onClick={() => playerRef.current?.seek(0)}>重播</button>
      </div>
    </div>
  );
}

这种模式在封装复杂组件(如编辑器、播放器、表单)时非常有用,它限制了父组件对子组件内部的访问权限。

五、常见应用场景

1. 管理焦点

function SearchForm() {
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      if (e.key === '/' && document.activeElement !== inputRef.current) {
        e.preventDefault();
        inputRef.current?.focus();
      }
    };

    document.addEventListener('keydown', handler);
    return () => document.removeEventListener('keydown', handler);
  }, []);

  return (
    <div className="search-form">
      <input ref={inputRef} placeholder='按 "/" 快速搜索' />
    </div>
  );
}

2. 测量 DOM 元素

function AutoResizeTextarea() {
  const textareaRef = useRef<HTMLTextAreaElement>(null);

  const handleInput = () => {
    const el = textareaRef.current;
    if (el) {
      el.style.height = 'auto';
      el.style.height = `${el.scrollHeight}px`;
    }
  };

  return (
    <textarea
      ref={textareaRef}
      onInput={handleInput}
      style={{ overflow: 'hidden', resize: 'none' }}
      placeholder="输入内容,高度自动调整..."
    />
  );
}

3. 集成第三方 DOM 库

import { useRef, useEffect } from 'react';

function ChartComponent({ data }: { data: DataPoint[] }) {
  const containerRef = useRef<HTMLDivElement>(null);
  const chartRef = useRef<EChartsInstance | null>(null);

  useEffect(() => {
    if (containerRef.current) {
      chartRef.current = echarts.init(containerRef.current);
    }

    return () => {
      chartRef.current?.dispose();
    };
  }, []);

  useEffect(() => {
    chartRef.current?.setOption({
      xAxis: { type: 'category', data: data.map(d => d.label) },
      yAxis: { type: 'value' },
      series: [{ data: data.map(d => d.value), type: 'bar' }],
    });
  }, [data]);

  return <div ref={containerRef} style={{ width: '100%', height: 400 }} />;
}

4. 存储可变值(不触发渲染)

function Timer() {
  const [seconds, setSeconds] = useState(0);
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);

  const start = () => {
    if (intervalRef.current !== null) return;
    intervalRef.current = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);
  };

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

  const reset = () => {
    stop();
    setSeconds(0);
  };

  useEffect(() => {
    return () => stop();
  }, []);

  return (
    <div>
      <p>计时:{seconds} 秒</p>
      <button onClick={start}>开始</button>
      <button onClick={stop}>停止</button>
      <button onClick={reset}>重置</button>
    </div>
  );
}

5. 保存前一次渲染的值

function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T | undefined>(undefined);

  useEffect(() => {
    ref.current = value;
  });

  return ref.current;
}

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

  return (
    <div>
      <p>当前值:{count},上一次:{prevCount ?? '无'}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </div>
  );
}

6. 无限滚动的 Intersection Observer

function InfiniteList({ loadMore }: { loadMore: () => Promise<void> }) {
  const sentinelRef = useRef<HTMLDivElement>(null);
  const [items, setItems] = useState<Item[]>([]);

  useEffect(() => {
    const observer = new IntersectionObserver(
      entries => {
        if (entries[0].isIntersecting) {
          loadMore();
        }
      },
      { threshold: 1.0 }
    );

    if (sentinelRef.current) {
      observer.observe(sentinelRef.current);
    }

    return () => observer.disconnect();
  }, [loadMore]);

  return (
    <div className="list">
      {items.map(item => (
        <div key={item.id} className="list-item">{item.title}</div>
      ))}
      <div ref={sentinelRef} className="sentinel" />
    </div>
  );
}

六、使用注意事项

  1. 避免过度使用 ref:能用 state 和 props 声明式解决的问题,不要用 ref 命令式操作
  2. 不要在渲染期间读写 ref.current(除了初始化):渲染应该是纯函数,ref 操作应在 effect 或事件处理中进行
  3. ref 的设置时机:React 在 commit 阶段设置 ref,DOM 更新完成后才可用
  4. 函数组件没有实例:不能对函数组件使用 ref 获取实例,只能配合 forwardRef + useImperativeHandle 暴露特定方法
// ❌ 渲染期间读取 ref
function Bad() {
  const ref = useRef(0);
  ref.current += 1; // 不要在渲染期间修改 ref
  return <div>{ref.current}</div>; // 不要在渲染期间读取 ref
}

// ✅ 在事件处理或 effect 中使用 ref
function Good() {
  const ref = useRef(0);

  const handleClick = () => {
    ref.current += 1;
    console.log(`点击了 ${ref.current} 次`);
  };

  return <button onClick={handleClick}>点击</button>;
}

七、总结

Refs 是 React 声明式模型中的命令式"逃生舱"。useRef 用于创建引用,forwardRef 用于转发引用,useImperativeHandle 用于自定义暴露的接口。主要应用场景包括 DOM 操作(焦点、测量、第三方库集成)和存储不触发渲染的可变值。使用时应遵循最小必要原则,优先使用声明式方案。