探索React 18的三个新API

1,439 阅读9分钟

现在React生态系统中最大的话题是React 18和其备受期待的并发渲染功能的全面发布。2021年6月,React团队宣布了 React 18的计划以及即将到来的内容。几个月后,在12月, React Conf 2021的主要话题是所有新宣布的并发渲染功能。

与React 18一起,几个新的API被运出,允许用户充分利用React的并发渲染功能。这些Hooks是。

本文将介绍这三个新的API,它们的使用案例,它们解决了什么问题,为什么要增加这些API,以及它们如何融入并发渲染的领域。

在我们开始之前,请注意

由于所有这些新的API都与并发渲染有关,我建议你首先熟悉这个概念,以及为什么React团队如此关注它。一个好的开始是React 18的公告工作组的公告。之后,下面的章节就会更有意义了。

useSyncExternalStore 钩子

React v16.14.0中引入的适应并发渲染的API之一是 [useMutableSource](https://github.com/reactjs/rfcs/pull/147),其目的是让React组件在并发渲染过程中安全、高效地与外部可变源集成。

Hook会附加到一个数据源,等待变化,并安排相应的更新。所有这些都会以一种防止 撕裂的方式发生,即由于同一状态有多个值而出现视觉不一致的情况。

这在新的并发渲染功能中是一个特别突出的问题,因为状态流可能很快就会交织在一起。然而,由于以下原因,采用useMutableSource 被证明是困难的。

1.Hook自然是异步的

Hook不知道如果selector 函数的结果值发生变化,它是否可以重用。唯一的解决方案是重新订阅所提供的数据源并再次检索快照,这可能会导致性能问题,因为它发生在每次渲染时。

对于用户和库(如Redux)来说,这意味着他们必须对项目中的每一个选择器进行备忘,并且不能在线定义他们的selector 函数,因为他们的引用不稳定。

2.它必须处理外部状态

最初的实现也是有缺陷的,因为它必须处理生活在React之外的状态。这意味着由于状态的易变性,状态可能随时改变。

因为React试图以异步方式解决这个问题,这有时会导致UI的可见部分被替换成回退,从而导致次优的用户体验。

所有这些都使库的维护者感到痛苦的迁移,对开发者和用户来说都是次优的体验。

解决这些问题的方法是useSyncExternalStore

为了解决这些问题,React团队改变了底层实现,并将Hook更名为useSyncExternalStore ,以正确反映其行为。这些变化包括。

  • 每次选择器(用于快照)发生变化时,不重新订阅外部源--相反,React将比较选择器的结果值,而不是选择器函数,以决定是否再次检索快照,因此用户可以在不对性能产生负面影响的情况下内联定义选择器
  • 每当外部存储发生变化时,所产生的更新现在总是同步的,这可以防止UI被替换成回退。

唯一的要求是,getSnapshot Hook参数的结果值需要在参考上是稳定的。React在内部使用这个来确定是否需要检索一个新的快照,所以它需要是一个不可变的值或一个备忘/缓存对象。

为了方便起见,React将提供一个额外的Hook版本,自动支持对getSnapshot'的结果值进行备忘。

如何使用useSyncExternalStore

// Code illustrating the usage of `useSyncExternalStore`.
// Source: <https://github.com/reactwg/react-18/discussions/86>

import {useSyncExternalStore} from 'react';

// React will also publish a backwards compatible shim
// It will prefer the native API, when available
import {useSyncExternalStore} from 'use-sync-external-store/shim';

// Basic usage. getSnapshot must return a cached/memoized result
const state = useSyncExternalStore(store.subscribe, store.getSnapshot);

// Selecting a specific field using an inline getSnapshot
const selectedField = useSyncExternalStore(store.subscribe, () => store.getSnapshot().selectedField);

// Code illustrating the usage of the memoized version.
// Source: <https://github.com/reactwg/react-18/discussions/86>

// Name of API is not final
import {useSyncExternalStoreWithSelector} from 'use-sync-external-store/with-selector';

const selection = useSyncExternalStoreWithSelector(
  store.subscribe,
  store.getSnapshot,
  getServerSnapshot,
  selector,
  isEqual
);

TheuseId Hook

在服务器端运行React

很长时间以来,React项目只在客户端运行。简而言之,这意味着所有的代码都被发送到用户的浏览器(客户端),然后由浏览器负责渲染并向用户显示应用程序。

React作为一个整体一直在向服务器端渲染(SSR)的领域扩展。在SSR中,服务器负责在React代码的基础上生成HTML结构。而不是所有的React代码,只有HTML被发送到浏览器上。

然后,浏览器只负责采取该结构,并通过渲染组件、在其上添加CSS和附加JavaScript来使其成为互动的。这个过程被称为水化。

水合的最重要要求是,由服务器和客户端生成的HTML结构必须匹配。如果它们不匹配,浏览器就无法确定它应该对结构的某一部分做什么,这将导致错误的渲染或非交互式的用户界面。

这在依赖标识符的功能中尤为突出,因为它们必须在两边匹配,例如在生成独特的造型类名称和可访问性标识符时。

useID 钩子的演变

为了解决这个问题,React最初引入了useOpaqueIdentifier Hook,但不幸的是,它也有一些问题。

  • 在不同的环境中,Hooks会产生不同的输出(不透明)。
    • 服务器端:它将产生一个字符串
    • 客户端:它将产生一个特殊的对象,必须直接传递给DOM属性。

这意味着Hook只能产生一个标识符,而且不可能动态地生成新的ID,因为它必须遵守Hooks的规则。因此,如果你的组件需要X个不同的标识符,它将不得不调用Hook X次,这显然在实践中不能很好地扩展。

// Code illustrating the way `useOpaqueIdentifier` handles the need for N identifiers in a single component, namely calling the hook N times. 
// Source: <https://github.com/facebook/react/pull/17322#issuecomment-613104823>

function App() {
  const tabIdOne = React.unstable_useOpaqueIdentifier();
  const panelIdOne = React.unstable_useOpaqueIdentifier();
  const tabIdTwo = React.unstable_useOpaqueIdentifier();
  const panelIdTwo = React.unstable_useOpaqueIdentifier();

  return (
    <React.Fragment>
      <Tabs defaultValue="one">
        <div role="tablist">
          <Tab id={tabIdOne} panelId={panelIdOne} value="one">
            One
          </Tab>
          <Tab id={tabIdTwo} panelId={panelIdTwo} value="one">
            One
          </Tab>
        </div>
        <TabPanel id={panelIdOne} tabId={tabIdOne} value="one">
          Content One
        </TabPanel>
        <TabPanel id={panelIdTwo} tabId={tabIdTwo} value="two">
          Content Two
        </TabPanel>
      </Tabs>
    </React.Fragment>
  );
}

某些可访问性API,如 [aria-labelledby](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-labelledby_attribute)可以通过一个空格分隔的列表接受多个标识符,但由于Hook的输出被格式化为不透明的数据类型,它总是必须直接连接到DOM属性上。这意味着不可能正确使用上述的可访问性API。

为了解决这个问题,该实现被改变了,并更名为useId 。这个新的Hook API在SSR和水化过程中生成稳定的标识符,以避免不匹配。在服务器渲染的内容之外,它退回到一个全局计数器。

而不是像useOpaqueIdentifier 那样创建一个不透明的数据类型(在服务器中是一个特殊的对象,在客户端是一个字符串),useId Hook在两边都产生一个不透明的字符串。

这意味着,如果我们需要X个不同的ID,就不需要再调用Hook X次。相反,一个组件可以调用一次useId ,并将其作为整个组件所需的标识符的基础(例如,使用后缀),因为它只是一个字符串。这就解决了useOpaqueIdentifier 中存在的两个问题。

如何使用useID

下面的代码例子说明了如何根据我们上面讨论的内容来使用useId 。因为React生成的ID是全局唯一的,而后缀是本地唯一的,所以动态创建的ID也是全局唯一的--因此不会造成任何水化不匹配。

// Code illustrating the improved way in which `useId` handles the need for N identifiers in a single component, namely calling the hook once and creating them dynamically. 
// Source: <https://github.com/reactwg/react-18/discussions/111>

function NameFields() {
  const id = useId();
  return (
    <div>
      <label htmlFor={id + '-firstName'}>First Name</label>
      <div>
        <input id={id + '-firstName'} type="text" />
      </div>
      <label htmlFor={id + '-lastName'}>Last Name</label>
      <div>
        <input id={id + '-lastName'} type="text" />
      </div>
    </div>
  );
}

TheuseInsertionEffect Hook

关于CSS-in-JS库的问题

最后一个将在React 18中添加的Hook--我们将在这里讨论--是useInsertionEffect 。这个钩子与其他钩子略有不同,因为它的唯一目的是对CSS-in-JS库很重要,这些库在运行中生成新的规则并在文档中插入<style> 标签。

在某些情况下,<style> 标签需要在客户端生成或编辑,如果不小心的话,在并发渲染中会造成性能问题。这是因为当CSS规则被添加或删除时,浏览器必须检查这些规则是否适用于现有的树。它必须重新计算所有的样式规则并重新应用它们--而不仅仅是改变了的那些。如果React发现另一个组件也产生了一个新的规则,同样的过程会再次发生。

这实际上意味着CSS规则必须在React渲染时针对每一帧的所有DOM节点进行重新计算。虽然你很有可能不会遇到这个问题,但它的扩展性并不好。

理论上,有一些方法可以解决这个问题,但主要是与时间有关。解决这个时间问题的最好办法是在所有其他DOM变化的同时生成这些标签,就像React库那样。最重要的是,它应该发生在任何东西试图访问布局之前,也应该发生在所有东西被呈现给浏览器绘制之前。

这听起来像是一件 [useLayoutEffect](https://blog.logrocket.com/useeffect-vs-uselayouteffect-examples/)可以解决的问题,但问题是,同一个Hook会被用于读取布局和插入样式规则。这可能会导致不受欢迎的行为,例如在一次计算中多次计算布局或读取不正确的布局。

useInsertionEffect 如何解决并发渲染的问题

为了解决这个问题,React团队引入了useInsertionEffect Hook。它与useLayoutEffect Hook非常相似,但它不能访问DOM节点的引用。

这意味着它只能用于插入样式规则。它的主要用途是插入全局DOM节点,如<style> ,或SVG<defs> 。由于这只与生成标签的客户端有关,Hook不会在服务器上运行。

// Code illustrating the way `useInsertionEffect` is used.
// Source: <https://github.com/reactwg/react-18/discussions/110>

function useCSS(rule) {
  useInsertionEffect(() => {
    if (!isInserted.has(rule)) {
      isInserted.add(rule);
      document.head.appendChild(getStyleForRule(rule));
    }
  });
  return rule;
}

function Component() {
  let className = useCSS(rule);
  return <div className={className} />;
}

最后的想法

React 18最令人期待的功能是其并发渲染功能。随着团队的宣布,我们收到了新的API,这些API将允许用户根据其使用情况采用并发渲染功能。虽然有些是全新的,但其他的是根据社区反馈对以前的API进行改进的版本。

在这篇文章中,我们介绍了三个最新的API,即useSyncExternalStoreuseId ,和useInsertionEffect 钩子。我们看了一下它们的用例,它们解决的问题,为什么与以前的版本相比某些变化是必要的,以及它们对并发渲染有什么作用。

充满了新的功能,React 18绝对是值得期待的

The postExploring React 18's three new APIsappeared first onLogRocket Blog.