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

这是我参与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。

猜你喜欢

转载自juejin.im/post/7032287741882662948