这是我参与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 简单易懂解析
系列文章
- 如何使用 ref 操作 DOM?(一)使用 ref 访问 DOM 节点
- 如何使用 ref 操作 DOM?(二)使用示例
- 如何使用 ref 操作 DOM?(三)如何使用 ref 回调去管理列表的 ref
- 如何使用 ref 操作 DOM?(四)forwardRef 访问自己组件的 DOM 节点
- 如何使用 ref 操作 DOM?(五)原理、react state 同步更新 DOM 的方式
- 如何使用 ref 操作 DOM?(六)最佳实践
- 如何使用 ref 操作 DOM?(七)总结和实践
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。