灏天阁

五种新的hooks在React18版本

· Yin灏

五种新的hooks在React18版本

探索 useTransition、useDeferredValue、useId、useSyncExternalStore、useInsertionEffect 和 useEffect 双重挂载 React 18 于 2022 年 3 月 29 日发布,该版本引入了 5 个新的 Hooks。

  • useTransition
  • useDeferredValue
  • useId
  • useSyncExternalStore
  • useInsertionEffect

在本文中,我们将详细探讨这些 Hooks。

在 Create React App 中设置工作环境

我们将使用 Create React App 作为基础来探索这些新的 Hooks。下面的命令可以创建一个 React 项目:

npx create-react-app react-release-18
cd react-release-18

React 版本自动指向 React 18。目前,它仍然使用旧的 root API。如果你执行 npm start 命令,会出现一个警告信息:

react-dom.development.js:86 Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot

新的 root API 由 createRoot 调用,它添加了 React 18 的所有改进并启用了并发特性。下面的 src/index.js 被修改为使用新的根 API:

import React from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";

const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

第 2 行导入了来自 ‘react-dom/client’ 的 createRoot。

第 8 行调用 createRoot 生成根元素。

现在工作环境已经准备好了,我们可以开始探索新的 Hooks 了。

useTransition

useTransition() 是一个用于过渡动画的 Hook。它返回过渡状态和一个函数,用于启动过渡。

const [isPending, startTransition] = useTransition();

React 状态更新可以分为两类:

  • 紧急更新 - 反映直接交互,例如打字、点击、按压、拖拽等等。
  • 过渡更新 - 用于从一个视图过渡到另一个视图。

过渡更新会让出给更紧急的更新。以下是在 src/App.js 中使用 useTransition 的示例代码:

import { useEffect, useState, useTransition } from "react";

const SlowUI = ({ value }) => (
  <>
    {Array(value)
      .fill(1)
      .map((_, index) => (
        <span key={index}>{value - index} </span>
      ))}
  </>
);

function App() {
  const [value, setValue] = useState(0);
  const [value2, setValue2] = useState(100000);
  const [isPending, startTransition] = useTransition();

  const handleClick = () => {
    setValue(value + 1);
    startTransition(() => setValue2(value2 + 1));
  };

  return (
    <>
      <button onClick={handleClick}>{value}</button>
      <div
        style={{
          opacity: isPending ? 0.5 : 1,
        }}
      >
        <SlowUI value={value2} />
      </div>
    </>
  );
}

export default App;

上述应用程序由两个组件组成(第 25 至 32 行): button(第 25 行):一个简单的按钮。显示的数字由第 14 行的 value 控制。点击按钮会增加 value(第 19 行,紧急和快速更新)和 value2(第 20 行,过渡和缓慢更新)的值。

SlowUI(第 26 至 32 行):该组件定义在第 3 至 11 行,生成由 value2(第 15 行)控制的 100000 个以上的 span 元素。更新这么多元素需要更长的时间。第 16 行的 useTransition 返回过渡状态 isPending 和启动过渡的函数 startTransition。当在第 20 行调用 startTransition 时,isPending 变为 true,SlowUI 变为半透明(浅色),并显示陈旧数据(第 28 行)。当过渡完成时,isPending 变为 false,SlowUI 变为完全不透明(实色),并显示更新后的数据。

如果删除第 20 行的 startTransition 并直接调用 setValue2(value2 + 1),则会看到同时进行如此多的更新时,UI 不再可用。

useTransition 钩子返回 isPending 和 startTransition。如果您不需要为 isPending 显示特殊的 UI,请删除第 16 行,并在代码顶部添加以下行:

import { startTransition } from "react";

useDeferredValue

useDeferredValue(value) 是一个接受一个值并返回一个新值的 hook,它将延迟到更紧急的更新。旧值将保留,直到紧急更新完成,然后渲染新值。这个 hook 类似于使用防抖或节流来延迟更新。

以下是例子:

import { useDeferredValue, useState } from "react";

const SlowUI = () => (
  <>
    {Array(50000)
      .fill(1)
      .map((_, index) => (
        <span key={index}>{100000} </span>
      ))}
  </>
);

function App() {
  const [value, setValue] = useState(0);
  const deferredValue = useDeferredValue(value);

  const handleClick = () => {
    setValue(value + 1);
  };

  return (
    <>
      <button onClick={handleClick}>{value}</button>
      <div>DeferredValue: {deferredValue}</div>
      <div>
        <SlowUI />
      </div>
    </>
  );
}

export default App;

上面的应用程序由三个组件组成(23-27 行):

  • button(第 23 行):这是一个简单的按钮。显示数字由第 14 行的 value 控制。点击按钮会增加 value(第 18 行),这是一个紧急且快速的更新。
  • div (line 24): 它显示 deferredValue。
  • SlowUI(第 25-27 行):该组件在第 3-11 行定义,生成 50000 个固定数量的 span 元素。尽管该组件没有属性并且在视觉上不会更新,但它需要很长时间来更新这么多元素。

useDeferredValue 可以与 startTransition 和 useTransition 一起使用。

useId

在 Web 应用程序中,有一些情况需要唯一的 ID,例如:

  • ,其中 for 属性必须等于相关元素的 id 属性,以将它们绑定在一起。
  • aria-labelledby,其中 aria-labelledby 属性可以使用多个 ID。

useId() 是一个生成唯一 ID 的 hook 函数:

  • 该 ID 在服务器端和客户端之间保持稳定,避免了服务器端渲染时出现的不匹配问题。
  • 该 ID 在整个应用程序中是唯一的。在多个根节点应用程序的情况下,createRoot/hydrateRoot 有一个可选的属性 identifierPrefix,可以用来添加前缀以避免冲突。
  • 该 ID 可以添加前缀和/或后缀,以生成在组件中使用的多个唯一 ID。这似乎是微不足道的。但是,useId 是从 useOpaqueIdentifier 进化而来的,后者生成了一个不可操作的不透明 ID。
import { useId } from "react";

const Comp1 = () => {
  const id = useId();
  return <div>Comp1 id({id})</div>;
};

const Comp2 = () => {
  const id = useId();
  return (
    <>
      <div>Comp2 id({id})</div>
      <label htmlFor={`${id}-1`}>Label 1</label>
      <div>
        <input id={`${id}-1`} type="text" />
      </div>
      <label htmlFor={`${id}-2`}>Label 2</label>
      <div>
        <input id={`${id}-2`} type="text" />
      </div>
    </>
  );
};

const Comp3 = () => {
  const id = useId();
  return (
    <>
      <div>Comp3 id({id})</div>
      <div aria-labelledby={`${id}-a ${id}-b ${id}-c`}>I am Comp3</div>
    </>
  );
};

function App() {
  return (
    <>
      <Comp1 />
      <Comp2 />
      <Comp3 />
    </>
  );
}

export default App;

上述应用程序由三个组件组成(第 38-40 行):

  • Comp1:定义在第 3-6 行,生成并显示一个 ID,即 :r0:。
  • Comp2:定义在第 8-23 行,生成一个 ID,即 :r1:。从这个 ID,它派生了两个唯一的 ID,即 :r1:-1(用于 Label 1 + 输入字段)和 :r1:-2(用于 Label 2 + 输入字段)。
  • Comp3:定义在第 25-33 行,生成并显示一个 ID,即 :r2:。从这个 ID,它派生了三个唯一的 ID,即 :r1:-a,:r1:-b 和 :r1:-c,用于 aria-labelledby 属性。

useSyncExternalStore

useSyncExternalStore 是一个钩子函数,建议用于从外部数据源(存储)读取和订阅。

以下是此钩子函数的签名:

const state = useSyncExternalStore(subscribe, getSnapshot[, getServerSnapshot]);

useSyncExternalStore 是一个钩子函数,建议用于从外部数据源(存储)读取和订阅。

以下是此钩子函数的签名:

const state = useSyncExternalStore(subscribe, getSnapshot[, getServerSnapshot]);

该方法接受三个参数:

  • subscribe:用于注册一个回调函数,每当存储更改时调用该函数。
  • getSnapshot:返回存储的当前值的函数。
  • getServerSnapshot:返回在服务器端渲染期间使用的快照的函数。这是一个可选参数。

该方法返回存储的值,即状态 state。

我们创建一个 useSyncExternalStore 的示例,它读取当前浏览器窗口的宽度并在屏幕上显示它。

请使用以下代码替换现有的 src/App.js 文件:

import { useSyncExternalStore } from "react";

function App() {
  const width = useSyncExternalStore(
    (listener) => {
      window.addEventListener("resize", listener);
      return () => {
        window.removeEventListener("resize", listener);
      };
    },
    () => window.innerWidth
    // () => -1,
  );

  return <p>Size: {width}</p>;
}

export default App;

useInsertionEffect

useEffect(didUpdate) 接受一个包含命令式可能具有副作用的代码的函数,这些副作用包括变异、订阅、定时器、日志记录和其他副作用。默认情况下,effect 会在每次完成渲染后运行,但是可以通过第二个参数的数组来控制调用。

useLayoutEffect 和 useEffect 有相同的签名,但它会在所有 DOM 变化后同步触发。也就是说,它会在 useEffect 之前被触发。它用于从 DOM 中读取布局并进行同步重新渲染。在 useLayoutEffect 中安排的更新将在浏览器有机会进行渲染之前同步刷新。

useInsertionEffect 是 React 18 中引入的新特性。它与 useEffect 有相同的签名,但是会在所有 DOM 变化之前同步触发。也就是说,它会在 useLayoutEffect 之前被触发。它用于在读取布局之前将样式插入到 DOM 中。 useInsertionEffect 主要用于 CSS-in-JS 库,例如 styled-components。由于该钩子的作用范围有限,它无法访问 refs,也不能安排更新。

以下示例位于 src/App.js 中,演示了 useEffect、useLayoutEffect 和 useInsertionEffect 的用法:

import { useEffect, useInsertionEffect, useLayoutEffect } from "react";

const Child = () => {
  useEffect(() => {
    console.log("useEffect child is called");
  });
  useLayoutEffect(() => {
    console.log("useLayoutEffect child is called");
  });
  useInsertionEffect(() => {
    console.log("useInsertionEffect child is called");
  });
};

function App() {
  useEffect(() => {
    console.log("useEffect app is called");
  });
  useLayoutEffect(() => {
    console.log("useLayoutEffect app is called");
  });
  useInsertionEffect(() => {
    console.log("useInsertionEffect app is called");
  });
  return (
    <div className="App">
      <Child />
      <p>Random Text</p>
    </div>
  );
}

export default App;

这些 effect 会按以下顺序被调用:

  • useInsertionEffect child is called.
  • useInsertionEffect app is called.
  • useLayoutEffect child is called.
  • useLayoutEffect app is called.
  • useEffect child is called.
  • useEffect app is called.

随着 React 18 的发布,StrictMode 获得了一个额外的行为,称为严格的效果模式。当启用严格效果时,React 在开发模式下有意地对新挂载的组件进行双重调用效果(挂载->卸载->挂载)。有趣的是,useInsertionEffect 不会被调用两次。

- Book Lists -