如何使用 ref 操作 DOM?(八)useImperativeHandle 给自己的组件公开特定的 API

1,510 阅读3分钟

这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战

翻译自:beta.reactjs.org/learn/manip…

因为 React 已经根据 render 的输出处理了 DOM 结构,所以你的组件不经常需要操作 DOM。然而,有的时候你可能需要操作 React 管理的 DOM 元素,比如,将焦点放到一个节点上,滚动到这个节点,或者去计算它的宽和高。React 中没有内置的方法去做这些事情,所以你将会需要 ref 去指向这个 DOM 节点。

这个系列的文章你将会学到:

  • 如何使用 ref 属性访问由 React 管理的 DOM 节点
  • 如何将 JSX 的 ref 属性关联到 useRef 钩子
  • 如何访问其他组件的 DOM 节点
  • 在哪种情况下,修改 React 管理的 DOM 是安全的

关于 ref 相关的介绍和例子,可以看我前面一个系列的文章 useRef 简单易懂解析

系列文章

useImperativeHandle 给自己的组件公开特定的 API

前面的示例中,MyInput 公开原始 DOM 输入元素。这使得父组件可以对它调用 focus()。然而,这也允许父组件做其他事情。例如,改变它的 CSS 样式。在不常见的情况下,你可能希望限制公开的功能。你可以通过 useImperativeHandle 来实现:

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

const MyInput = forwardRef((props, ref) => {
  const realInputRef = useRef(null);
  useImperativeHandle(ref, () => ({
    // 只曝光 focus,别无其他
    focus() {
      realInputRef.current.focus();
    },
  }));
  return <input {...props} ref={realInputRef} />;
});

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

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}

在这里,MyInput 中的 realInputRef 保存实际的输入 DOM 节点。然而,useImperativeHandle 指示 React 提供你自己的特殊对象作为父组件的引用的值。所以 inputRef。当前的 Form 组件中只有 focus 方法。在这种情况下,ref "handle" 不是 DOM 节点,而是你在 useImperativeHandle 调用中创建的自定义对象。

一个 flushSync 的实践

此图像轮播有一个 “Next” 按钮,可以切换活动图像。单击时使图库水平滚动到活动图像。您需要在活动图像的 DOM 节点上调用 scrollIntoView()

node.scrollIntoView({
  behavior: 'smooth',
  block: 'nearest',
  inline: 'center'
});
import { useState } from 'react';

export default function CatFriends() {
  const [index, setIndex] = useState(0);
  return (
    <>
      <nav>
        <button onClick={() => {
          if (index < catList.length - 1) {
            setIndex(index + 1);
          } else {
            setIndex(0);
          }
        }}>
          Next
        </button>
      </nav>
      <div>
        <ul>
          {catList.map((cat, i) => (
            <li key={cat.id}>
              <img
                className={
                  index === i ?
                    'active' :
                    ''
                }
                src={cat.imageUrl}
                alt={'Cat #' + cat.id}
              />
            </li>
          ))}
        </ul>
      </div>
    </>
  );
}

const catList = [];
for (let i = 0; i < 10; i++) {
  catList.push({
    id: i,
    imageUrl: 'https://placekitten.com/250/200?image=' + i
  });
}

解决方案

你可以声明一个 selectedRef,然后有条件地将它传递给当前图像:

<li ref={index === i ? selectedRef : null}>

index === i 时,说明图像是被选中的,<li> 将被赋值为 selectedRef。React 将确保选中的 selectedRef.current 总是指向正确的 DOM 节点。

注意,flushSync 的调用会同步更新 state,在 React 在滚动之前更新 DOM 之前。否则 selectedRef.current 总是指向先前选定的项。

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

export default function CatFriends() {
  const selectedRef = useRef(null);
  const [index, setIndex] = useState(0);

  return (
    <>
      <nav>
        <button onClick={() => {
          flushSync(() => {
            if (index < catList.length - 1) {
              setIndex(index + 1);
            } else {
              setIndex(0);
            }
          });
          selectedRef.current.scrollIntoView({
            behavior: 'smooth',
            block: 'nearest',
            inline: 'center'
          });            
        }}>
          Next
        </button>
      </nav>
      <div>
        <ul>
          {catList.map((cat, i) => (
            <li
              key={cat.id}
              ref={index === i ?
                selectedRef :
                null
              }
            >
              <img
                className={
                  index === i ?
                    'active'
                    : ''
                }
                src={cat.imageUrl}
                alt={'Cat #' + cat.id}
              />
            </li>
          ))}
        </ul>
      </div>
    </>
  );
}

const catList = [];
for (let i = 0; i < 10; i++) {
  catList.push({
    id: i,
    imageUrl: 'https://placekitten.com/250/200?image=' + i
  });
}

总结来说是,为了滚动到正确的 DOM 节点,需要先同步更新 state,然后更新 ref。