跳到主要内容

useMergedRef

合并多个 ref 到单个 ref 的 Hook,用于需要将多个 ref 附加到同一元素的场景。

基本用法

实时编辑器
function Demo() {
  const [value, setValue] = useState('');
  const inputRef = useRef(null);
  const focusRef = useRef(null);
  const mergedRef = useMergedRef(inputRef, focusRef);

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

  const handleGetValue = () => {
    if (inputRef.current) {
      setValue(inputRef.current.value);
    }
  };

  return (
    <div>
      <Input ref={mergedRef} placeholder="输入文本" style={{ marginBottom: '12px' }} />
      <Group spacing="md">
        <Button onClick={handleFocus}>聚焦输入框</Button>
        <Button onClick={handleGetValue}>获取值</Button>
      </Group>
      {value && (
        <div
          style={{
            marginTop: '12px',
            padding: '12px',
            backgroundColor: 'var(--ifm-color-emphasis-100)',
            borderRadius: '6px',
          }}
        >
          <strong>输入值:</strong> {value}
        </div>
      )}
    </div>
  );
}
结果
Loading...

与 Hook 配合

结合自定义 Hook 使用:

实时编辑器
function Demo() {
  const hoverRef = useRef(null);
  const { ref: hoverHookRef, isHovered } = useHover();

  const mergedRef = useMergedRef(hoverRef, hoverHookRef);

  return (
    <div>
      <div
        ref={mergedRef}
        style={{
          padding: '32px',
          backgroundColor: isHovered
            ? 'var(--ifm-color-primary-lightest)'
            : 'var(--ifm-color-emphasis-100)',
          borderRadius: '8px',
          textAlign: 'center',
          cursor: 'pointer',
          transition: 'background-color 0.2s',
        }}
      >
        <div style={{ fontSize: '48px', marginBottom: '12px' }}>{isHovered ? '👆' : '👇'}</div>
        <div style={{ fontWeight: 'bold', marginBottom: '8px' }}>
          {isHovered ? '鼠标悬停中' : '将鼠标悬停在这里'}
        </div>
      </div>
    </div>
  );
}
结果
Loading...

转发 ref

在组件中转发多个 ref:

实时编辑器
function Demo() {
  const CustomInput = React.forwardRef((props, ref) => {
    const internalRef = useRef(null);
    const mergedRef = useMergedRef(ref, internalRef);

    useDidMount(() => {
      console.log('组件已挂载,ref 已合并');
    });

    return <Input ref={mergedRef} {...props} />;
  });

  const externalRef = useRef(null);

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

  return (
    <div>
      <CustomInput ref={externalRef} placeholder="自定义输入框" style={{ marginBottom: '12px' }} />
      <Button onClick={handleFocus}>外部聚焦</Button>
    </div>
  );
}
结果
Loading...

动态 ref 数量

合并动态数量的 refs:

实时编辑器
function Demo() {
  const [count, setCount] = useState(1);
  const refs = Array.from({ length: count }, () => useRef(null));
  const mergedRef = useMergedRef(...refs);

  const handleClick = () => {
    refs.forEach((ref, index) => {
      if (ref.current) {
        console.log(`Ref ${index + 1}:`, ref.current);
      }
    });
  };

  return (
    <div>
      <div
        style={{
          marginBottom: '12px',
          padding: '12px',
          backgroundColor: 'var(--ifm-color-emphasis-100)',
          borderRadius: '6px',
        }}
      >
        <strong>当前合并了 {count} 个 refs</strong>
      </div>
      <Group spacing="md" style={{ marginBottom: '12px' }}>
        <Button onClick={() => setCount(Math.max(1, count - 1))}>减少</Button>
        <Button onClick={() => setCount(count + 1)}>增加</Button>
        <Button onClick={handleClick}>日志输出</Button>
      </Group>
      <div
        ref={mergedRef}
        style={{
          padding: '24px',
          backgroundColor: 'var(--ifm-background-surface-color)',
          border: '1px solid var(--ifm-color-emphasis-300)',
          borderRadius: '8px',
          textAlign: 'center',
        }}
      >
        这个元素被 {count} 个 ref 引用
      </div>
    </div>
  );
}
结果
Loading...

API

参数

function useMergedRef<T = any>(
...refs: Array<React.Ref<T> | undefined>
): (instance: T | null) => void;
参数说明类型默认值
...refs要合并的 ref 数组Array<React.Ref<T> | undefined>-

返回值

返回一个合并后的 ref 回调函数。

(instance: T | null) => void

支持的 ref 类型

useMergedRef 支持所有类型的 React refs:

Ref 对象

const ref = useRef(null);
const mergedRef = useMergedRef(ref);

Ref 回调

const refCallback = (node) => {
console.log('Node:', node);
};
const mergedRef = useMergedRef(refCallback);

转发 Ref

const Component = React.forwardRef((props, ref) => {
const internalRef = useRef(null);
const mergedRef = useMergedRef(ref, internalRef);
return <div ref={mergedRef} />;
});

工作原理

  1. 接收任意数量的 refs(ref 对象或回调函数)
  2. 返回一个新的 ref 回调函数
  3. 当元素挂载或更新时,同时更新所有 refs
  4. 自动处理 nullundefined 的情况

使用场景

  • Hook 组合: 多个需要 ref 的 Hook 一起使用
  • 转发 ref: 组件内部需要 ref,同时对外暴露 ref
  • 第三方库: 集成需要 ref 的第三方库
  • 测量元素: 同时用于多个测量或监听目的
  • 动画库: 多个动画库同时控制一个元素

常见场景

与 useClickOutside 结合

function Dropdown() {
const internalRef = useRef(null);
const clickOutsideRef = useClickOutside(() => setOpened(false));
const mergedRef = useMergedRef(internalRef, clickOutsideRef);

return <div ref={mergedRef}>...</div>;
}

与 useHover 结合

function Card() {
const cardRef = useRef(null);
const { ref: hoverRef, isHovered } = useHover();
const mergedRef = useMergedRef(cardRef, hoverRef);

return <div ref={mergedRef}>...</div>;
}

与 forwardRef 结合

const Button = React.forwardRef((props, ref) => {
const internalRef = useRef(null);
const mergedRef = useMergedRef(ref, internalRef);

return <button ref={mergedRef} {...props} />;
});

注意事项

  • 传入的 refs 可以是 undefinednull,会自动过滤
  • 返回的 ref 是稳定的,不会在每次渲染时改变
  • 支持任意数量的 refs 合并
  • 对性能影响极小

TypeScript 类型

// 基本用法
const ref1 = useRef<HTMLDivElement>(null);
const ref2 = useRef<HTMLDivElement>(null);
const mergedRef = useMergedRef<HTMLDivElement>(ref1, ref2);

// 带可选 ref
const mergedRef = useMergedRef<HTMLDivElement>(ref1, ref2, undefined);

// 组件中使用
type Props = {
customRef?: React.Ref<HTMLDivElement>;
};

const Component = ({ customRef }: Props) => {
const internalRef = useRef<HTMLDivElement>(null);
const mergedRef = useMergedRef(customRef, internalRef);

return <div ref={mergedRef} />;
};

最佳实践

  • 在需要多个 ref 时使用,单个 ref 直接使用即可
  • forwardRef 配合使用时特别有用
  • 确保传入的 refs 类型一致
  • 可以安全地传入 undefined 作为参数