React Ref Callback 实现组件结构与逻辑分离

Published on
9 mins read
––– views
经编辑后转载

原文地址 公号:KooFE 前端团队

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 或将函数移出组件来避免这种行为。

使用场景

在 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/>  也不会渲染

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