服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

【第2881期】React Ref Callback:最佳实践

日期: 来源:前端早读课收集编辑:ikoofe

前言

从定义到场景的解释。今日前端早读课文章由 @ikoofe 翻译分享,公号:KooFE 前端团队授权。

正文从这开始~~

在 React 中,"Ref" 具有两个相关的含义,而且经常让人困惑。在本文正式开始之前,我们先弄清楚它的定义:

  • 作为 useRef hook 返回的 “ref 对象”:在这种场景中,它就是一个普通的 JavaScript 对象,具有一个名为 current 的属性,并且可以读取或设置为任意值。

  • 作为 JSX DOM 元素上的 “ref 属性”:用于访问其对应的 DOM 元素

这两者经常一起使用,“ref 对象” 可以传递给 “ref 属性”,React 会将对 DOM 元素设置为它的 current 属性值。

ref callback

ref 属性除了接受 ref 对象之外,还可以接受函数也就是 ref callback。在该函数中,DOM 元素作为其唯一参数。

与 effect 函数一样,React 在组件周期中的某些时刻中调用它。当创建 DOM 元素之后会立即执行 ref callback(参数是 DOM 元素),在删除元素时也会再次调用 ref callback,只不过这时的参数是 null。

如果 ref callback 被定义为内联函数,React 将在每次渲染时调用它两次,第一次的参数是 null,第二次的参数是 DOM 元素。

虽然内联 ref callback 被调用两次可能会令人惊讶,如果从 React 的角度来看,我认为这种行为是合理的。它保证了一致性,因为每次渲染都会创建新的函数实例,它可能是一个完全不同的函数。这些函数可能会依赖 props 或 state,而这些 props 或 state 也可能在此期间发生了变化。

因此 React 需要清除旧的 ref callback(参数是 null),然后设置新的回调(参数是 DOM 元素)。这样我们可以根据条件来设置 ref 属性的值,甚至在 React 元素之间交换它们。

这可能会导致一些不必要的调用。在大多数情况下,这不是引起什么问题。如果你不想执行这些不必要的调用,可以通过在 useCallback 中包装 ref callback 或将函数移出组件来避免这种行为。

使用场景

在 React Docs 中关于 ref callback 的内容较少。也许是他们故意不去讨论它,因为它的使用场景非常少,访问 DOM 元素的场景并不多见。

ref callback 是 React 的一个小众功能,你不会每天都需要它。尽管如此,还是有一些场景会用到它,否则,它就不会存在于 React 中了!所以让我们来看一下在哪些场景会用到它。

需要明确的是,只有在访问底层的 DOM 元素时,才需要 ref callback。那么,ref callback 何时有用?答案是,当您想要在 React 中对 DOM 元素执行操作时,请使用 ref callback。据我所知,这可以归结为以下四种情况。

  • 当元素挂载或更新时,调用 DOM 元素上的方法来执行一些操作。

  • “通知” DOM 元素的更改,当 DOM 元素的某些属性发生更改时,重新读取该元素。

  • 将 DOM 元素设置到 state 中,以便在渲染期间访问它。

  • 共享 DOM 元素;使用 DOM 元素执行多项操作。

现在,让我们具体来看看每一个场景。

1. DOM 元素挂载并滚动到它所在的位置

您可以在 ref callback 中调用 DOM 元素上的方法,以执行滚动或聚焦等 DOM 操作。例如,自动滚动到列表中的最后一项:

 // On first render and on unmount there
// is no DOM element so `el` will be `null`
const scrollTo = (el) => {
if (el) {
el.scrollIntoView({ behavior: "smooth" });
}
};

function List({ data }) {
return (
<ul>
{data.map((d, i) => {
const isLast = i === data.length - 1;
return (
<li
key={d.name}
// ref callback to scroll to the last list element
ref={isLast ? scrollTo : undefined}
>
{d.name}
</li>
);
})}
</ul>
);
}

记住,管理 DOM 是 React 的工作,避免执行 DOM 上的可变方法, 比如(insert, remove, set, replace 等),对于 focus 和 scroll 等非破坏性的操作则允许我们开发实现。

还要注意,浏览器在没有用户交互的情况下是不允许调用 DOM 元素的某些方法,如 requestFullscreen。当在 ref callback 中调用时,所有此类受保护的方法都不会执行任何操作。

2. 当 DOM 元素变化时的重新渲染

当我们通过 React 访问某些 DOM 元素属性时,可以使用 ref callback。当我们读取一个 DOM 元素属性,比如滚动位置,或者在 ref callback 中调用一个获取元素信息的方法,比如 getBoundingClientRect(),并将该信息设置到 state 中。

测量 DOM 元素

这是一段直接来自(旧)React 文档的片段:如何测量 DOM 节点。这是 ref callback 的一个很好的例子,所以将其复制到这里。

 const [size, setSize] = useState();

const measureRef = useCallback((node) => {
setSize(node.getBoundingClientRect());
}, []);

return <div ref={measureRef}>{children}</div>;

在这个案例中,没有选择使用 useRef,因为当 ref 是一个对象时,它并不会把当前 ref 值的变化情况通知到我们。使用 callback ref 可以确保即便被测量的节点在子组件延迟显示 (比如为了响应一次点击),我们依然能够在父组件接收到相关的信息,以便更新测量结果。注意到我们传递了 [] 作为 useCallback 的依赖列表。这确保了 ref callback 不会在再次渲染时改变,因此 React 不会在非必要的时候调用它。

3. 在 render 中访问 DOM 元素

如果在 ref 回调中将 DOM 元素设置到 state,它将触发新的渲染,因为这正是设置 state 的作用。但是它不会陷入无限渲染循环,因为 setState 是一个稳定的函数,因此 ref callback 仅在挂载和卸载时调用。

在这种情况下,为什么我们不使用 useRef?答案是,因为不允许在渲染过程中访问 ref 对象。对于渲染中的 DOM 元素,必须通过 state 来访问。接下来举几个例子进行介绍。

React Portal

React portal 主要用于解决组件树和 DOM 树的结构之间不一致的问题。portal 将 DOM 树上不同位置上的组件连接到一起,最为常使用的场景就是将 Modal 弹窗覆盖整个视窗。

 // Assume an empty div with id 'modal' is in your HTML
const modalEl = document.getElementById("modal");

function Modal({ children, ...props }) {
return ReactDOM.createPortal(
<ModalBase {...props}>
{children}
</ModalBase>,
modalEl
);
}

可以使用 document.getElementById() 来获取 DOM 元素,前提是你能保证它是存在的。或许你不想通过 HTML 来控制 Modal,而是希望能 portal 到一个 React 创建的 DOM 元素上。

这就需要在进行 render 时访问到相应的 DOM 元素,使用 ref callback 可以实现这个功能。

 function Parent() {
const [modalElement, setModalElement] = useState(null);

return (
<div>
<div id="modal-location" ref={setModalElement} />
{/* Imagine that the modal container and the
Modal itself are farther apart in the component tree */}
<Modal modalElement={modalElement}>Warning</Modal>
</div>
)
}

function Modal({ children, modalElement, ...props }) {
return modalElement
? ReactDOM.createPortal(
<ModalBase {...props}>{children}</ModalBase>,
modalElement
)
: null;

在最开始,modalElement 的值是 null,所以需要在创建 portal 之前做一下判断。

非受控复合组件

非受控复合组件(Uncontrolled Compound Components)是一种高级的 React 模式,其核心是 ref callback 来处理 React portal。

复合组件(Compound Component)是将多个组件组合到一起工作,进而形成一个能够展示的 UI。复合组件将复杂的功能拆分为更小的块,并且它们在一起共同完成整个复杂功能。这样就可以避免产生一个有很多 props 的 “上帝组件”。

这种模式与 HTML 元素组合比较类似,比如:

  • <select> 中包含多个 <option>

  • <table> 组件会由 <thead> 和 <tbody> 组成

  • <details> 元素中会包含 <summary>

在 React 中,数据总是向下流动。当数据流不符合组件树的结构时,我们可以通过提升 state 来调整数据流。大多数时候,这是一个很好的解决方案。

对于一些共享组件,如对话框或侧边栏,页面上只能有一个,提升 state 会使这些 state “过于全局”。这些 state 将通过许多中间组件连接起来,而这些中间组件实际上并不需要知道它。它会污染整个链条上的组件,并会使代码变得混乱。

我们可以把 React state 看作是悬挂在组件树上的绳子。绳子的长度代表了定义 state 的组件到使用 state 的 UI 之间的距离。所以,当你的绳子长度越长、数量越多时,它们就越容易被缠在一起。所以要尽量缩短绳子的长度,同时控制绳子的数量。

出现这个问题的根源是,我们强行将组件树和数据流适配成 DOM 树的形状。反过来,我们可以通过组件树适应数据流来反转控制。使用 Portal,我们可以重置组件间的距离,并保持 DOM 结构不变。这样就可以将拉近相关组件的距离,即便是在 DOM 树中的离得很远。这使我们的 React 代码更容易理解。

非受控复合组件的实现过程:

  • ref callback 定位到元素位置。获取 DOM 元素并将其置于 state 中。这里我们不能使用 ref 对象,因为我们需要在 render 中使用它,而且要在设置它的时候触发更新。

  • 用 Context 共享元素位置。将这个 DOM 元素放存放在 context 中,以便 context 中的所有组件都可以访问 DOM 中的这个位置。

  • 使用 Portal 连接到该位置。从 context 中获取对 DOM 元素,并将组件进行 portal。

如果你在想 “这一切听起来很复杂”,是的,你没有错。这是一种高级模式,需要一些额外的成本,作为回报 -- 它能够让你编写了更简单的组件。这是值得的(IMO)。也许你会发现,你并不经常需要它,但是一旦有需要时,就会体验到其中的乐趣。“Amazing”!

比如在下面的例子中实现一个复杂的面包屑:

每个 <Breadcrumb/> 都会 portal 到 <BreadcrumbPortal/> 中的 breadcrumbElement 元素

<BreadcrumbPortal/> 会按它们的渲染顺序在 <Breadcrumbs/> 展示出来

如果 <BreadcrumbPortal/> 没有渲染,这时 breadcrumbElement 为 null,<Breadcrumb/> 也不会渲染

4. 共享 DOM Ref

经常会出现不止一个消费者需要访问 DOM 元素。假设你想测量一个 <div> 的宽度,并将其交给另一个 React 之外的库来处理 DOM 内容。对于 React 来说,这样的元素就是一个黑盒。React 既不知道它的内部有什么,也不关心它是什么。这个元素完全交给另外一个库来管理。

一个典型的例子就是使用 D3 或 @observable/plot 创建的响应式图标。在下面的例子中,我们会使用 @observable/plot 创建一个 plot,并且使用 react-use-measure 来计算元素的宽度。使用 ref callback 将 DOM 元素传递给它们俩:

 import useMeasure from "react-use-measure";
import * as Plot from "@observablehq/plot";

export function BoxPlot({ data }) {
const [measureRef, { width, height }] = useMeasure({ debounce: 5 });

const plotRef = useRef<HTMLDivElement | null>(null);

useEffect(() => {
const boxPlot = Plot.plot({
width: Math.max(150, width),
marks: [
Plot.boxX(data),
],
});

plotRef.current.append(boxPlot);

return () => boxPlot.remove();
}, [data, width]);

const initBoxPlot = useCallback((el: HTMLDivElement | null) => {
plotRef.current = el;
measureRef(el);
}, []);

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

总结

  • ref callback 是一个传递给元素的 ref 属性的函数。React 会在组件挂载时调用它,这时的参数是 DOM 元素;当组件卸载的时候也会调用它,这时的参数是 null

  • 当你设置不同的 ref callback 时,React 也会调用 ref callback

  • 切换绑定和取消绑定 ref 到一个 DOM 元素,ref callback 可以让我们实现特定的操作

  • ref callback 可以用来做以下事情

  • 操作 DOM,比如在组件挂载的时候滚动或聚焦

  • 在 React 获取 DOM 属性,比如宽度或滚动位置

  • 在 React 控制的 DOM 元素上使用 Portal

  • 非受控复合组件是一个很强大的模式

  • 将 DOM 元素提供给多个消费者

关于本文
译者:@ikoofe
译文:https://mp.weixin.qq.com/s/xo58X_ocO5XsP93ZTwl-oQ
作者:@Jules Blom
原文:https://julesblom.com/writing/ref-callback-use-cases

这期前端早读课
对你有帮助,帮” 赞 “一下,
期待下一期,帮” 在看” 一下 。

相关阅读

  • 【第2880期】浅谈前端组件设计

  • 前言前端代码逐渐从 “平铺” 转变到了 “层级” 结构,从 “面向过程” 进阶为 “面向对象”,前端组件也成为了近几年来的热门议题。今日前端早读课文章由 @ELab.yangyuqin 分
  • 前端工程化-VSCode插件集成脚手架和组件库

  • 目录VSCode插件能做什么?VSCode可扩展能力有哪些?如何开发一个VSCode插件?VSCode插件如何集成基建的脚手架和组件库?(FAW保姆级教程)前端常见插件的实现原理分析?前言我们程序员
  • 第426堂好课丨小仙炖:品类创新,是一个系统工程

  • 这几年在消费赛道,我们看到了太多品牌起起落落。有一个词经常被轻飘飘地抛出来,那就是“品类创新”。这些创新品类、品牌,它们有没有可能真正影响市场,占领消费者心智呢?小仙炖鲜
  • 十个Python图像处理工具,不可不知!

  • 来源丨Python宝典 & 小象这些Python库提供了一种简单直观的方法来转换图像并理解底层数据。今天的世界充满了数据,图像是这些数据的重要组成部分。但是,在使用它们之前,必须对
  • 钙钛矿从“0”到“1”还差什么?

  • 声明:本文仅梳理公司和行业的最新基本面,并非在当前时间点推荐买卖公司,本公众号不具备个股操作指导功能,投资有风险,入市需谨慎要点总结:协鑫光电目前规划:①先达到100兆瓦级别的
  • 3个容易混淆的前端框架概念

  • 关注“脚本之家”,与百万开发者在一起出处:魔术师卡颂(ID:gh_52d0bec584f9)如若转载请联系原公众号有3个容易混淆的前端框架概念:响应式更新单向数据流双向数据绑定在继续阅读本
  • 她们,是中国培训圈的主力

  • 在培训行业中,女性占比应该超过60%。在培训经理指南20万的读者中,其中的女性占比高达55.9%,比男性高出11%。‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍从年龄分布来

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

  • 【第2880期】浅谈前端组件设计

  • 前言前端代码逐渐从 “平铺” 转变到了 “层级” 结构,从 “面向过程” 进阶为 “面向对象”,前端组件也成为了近几年来的热门议题。今日前端早读课文章由 @ELab.yangyuqin 分
  • 【早说】设计优雅降级

  • The degradation of design elegance requires an analysis of two issues. One is the need to consider what unsound situations might be encountered, the second is t
  • 【早说】感知变化

  • Changed the internal structure of the company. In order to feel the change of the external environment at any time, we establish a mechanism that can listen to
  • 【第2881期】React Ref Callback:最佳实践

  • 前言从定义到场景的解释。今日前端早读课文章由 @ikoofe 翻译分享,公号:KooFE 前端团队授权。正文从这开始~~在 React 中,"Ref" 具有两个相关的含义,而且经常让人困惑。在本文正式
  • 上海科级干部退休金9500?我还特意算了一下。

  • 每年3月份是申报个税的时候,博格还专门写了一篇文章《退税了12000》,因为2022年通过个人养老金账户买12000的养老Y份额,按20%税率计算,给博格减免2400的个税。所以,在晚上直播的
  • 一文读懂西藏自治区农业产业概况

  • 小蜂说本文基于农小蜂在运营过程中积累的:西藏自治区农业产值、种植(养殖)规模、产量等数据对西藏自治区农业产业概况进行简析,梳理西藏自治区农业产业基本面及发展趋势,让对农业