---
title: React 18 中的一些新 hooks
date: '2022-10-11'
tags: [React, TypeScript]
---

<Image
  src="/images/p/react18-new-hooks/react-hooks.jpg"
  alt=""
  priority
  width="811"
  height="541"
/>

## useTransition

返回一个状态值表示过渡任务的等待状态，以及一个启动该过渡任务的函数。它和 CSS 的过渡没有任何关系。useTransition 的主要目的是作用于在复杂的过渡任务时提供一个优先级较低的更新，过渡任务中触发的更新会让更紧急地更新先进行，比如点击。

过渡任务中的更新将不会展示由于再次挂起而导致降级的内容。这个机制允许用户在 React 渲染更新的时候继续与当前内容进行交互。

这里渲染一个长度为 10 万的列表和两个不同的按钮，第一个按钮会增加列表的长度并使第一个状态数值加一。第二个按钮只会增加自己的状态。

如果在这里设置状态时不使用 `setTransiton` ，那么在整个列表进行渲染时将无法处理其他任何操作。

在使用了 `setTransition` 后，整个列表进行渲染操作优先级将会比其他更紧急地操作低，从而可以响应第二个按钮点击。

`useTransition` 返回的元组中包含两个值 `[pending, setTransiton]` ，分别是 `setTransiton` 方法和表示正在过渡的状态 `pending` 。如果需要在过渡时展示特定的 UI 就可以使用 `pending` 来控制状态。

<RUASandpack
  template="react-ts"
  files={{
    '/Button.tsx': {
      code: sandpack.common.Button,
      hidden: true,
    },
    '/App.tsx': sandpack['react18-new-hooks'].useTransition,
  }}
/>

## useDeferredValue

`useDeferredValue` 接受一个状态值，并返回该的副本。返回的副本状态值将会在其他紧急更新后更新。它与 `useTransition` 比较类似，二者可以搭配使用。

因为函数的原子性，在整个组件更新（重新渲染）时，子组件也会随着一起更新。这里通过设置了状态 `value` 来对整个组件重新渲染，即使下方的列表没有任何变化也会一起重新渲染。而重新渲染 UI 界面对我们来说就是紧急更新。

将原本的状态值 `value` 与 `useDeferredValue` 返回的副本相比较就会发现 `value` 会随着 UI 一起被更新，而被延迟的状态 `deferred` 会等待 UI 更新结束后再做更新。

<RUASandpack
  template="react-ts"
  files={{
    '/Button.tsx': {
      code: sandpack.common.Button,
      hidden: true,
    },
    '/App.tsx': sandpack['react18-new-hooks'].useDeferredValue,
  }}
/>

## useId

`useId` 与其他的都不同，从名字就能看出来它的作用，返回一个在整个 APP 中唯一的 ID。它能够保证每个组件调用它返回的 ID 都唯一，即使是同一个组件被父组件多次调用。

通常的作用有：

- 为页面中需要唯一 ID 元素提供 ID，例如 `<label for="ID">` 。
- SSR 到客户端注入时需要 ID 避免错误。

<RUASandpack
  template="react-ts"
  files={{
    '/Input.tsx': {
      code: sandpack.common.Input,
      hidden: true,
    },
    '/App.tsx': sandpack['react18-new-hooks'].useId,
  }}
/>

## useSyncExternalStore

`useSyncExternalStore` 是目前最适合用来取代全局状态库的 hook，尤其是配合 redux 的思想后，简直就是手写的 mini redux。

首先看下该钩子的前面，其实不是很复杂。它接受一个范型，用于推断返回的状态。剩下的三个参数分别是：

- `subscribe`: 一个当状态更新后可执行的回调函数。该函数会收到一个回调函数，这个回调函数就是当状态后执行，React 用来对比是否需要重新渲染组件。
- `getSnapshot`: 返回当前状态的函数。
- `getServerSnapshot`: 在服务端渲染时返回当前状态的函数，可选。

```tsx
useSyncExternalStore<State>(subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => State, getServerSnapshot?: (() => State) | undefined): State
```

`useSyncExternalStore` 在组件中使用与 `useSelector` 很相似：

```tsx
const { count, info } = useSyncExternalStore(
  store.subscribe,
  store.getSnapshot,
);
```

但它的重点还是在 store 上：

```tsx
export type State = {
  count: number;
  info: string;
};
export type Store = {
  state: State;
  setState: (
    stateOrFn: Partial<State> | ((state: State) => Partial<State>),
  ) => void;
  subscribe: (listener: () => void) => () => void;
  listeners: Set<() => void>;
  getSnapshot: () => State;
};

const store: Store = {
  state: {
    count: 0,
    info: 'Hello',
  },
  setState(stateOrFn) {
    const newState =
      typeof stateOrFn === 'function' ? stateOrFn(store.state) : stateOrFn;
    store.state = {
      ...store.state,
      ...newState,
    };
    store.listeners.forEach((listener) => listener());
  },
  listeners: new Set(),
  subscribe(listener) {
    store.listeners.add(listener);
    return () => {
      store.listeners.delete(listener);
    };
  },
  getSnapshot() {
    return store.state;
  },
};

export default store;
```

其中， `listeners` 用于存放 `subscribe` 的回调，在我们更新状态后需要通知 React 来更新组件。所以在 `setState` 中遍历执行。之所以使用 `Set()` 是因为 `subscribe` 还需要返回一个函数用于注销 `listener` 。

<RUASandpack
  template="react-ts"
  files={{
    '/Input.tsx': {
      code: sandpack.common.Input,
      hidden: true,
    },
    '/Button.tsx': {
      code: sandpack.common.Button,
      hidden: true,
    },
    '/store.ts': sandpack['react18-new-hooks'].store,
    '/App.tsx': sandpack['react18-new-hooks'].useSyncExternalStore,
  }}
/>

## useInsertionEffect

`useInsertionEffect` 在通常情况下都用不上，它的唯一目的是对 CSS-in-JS 库很重要。

CSS-in-JS 库通常需要在运行时插入或修改 `<style>`  标签等。当 CSS 规则被添加或删除时，浏览器必须检查这些规则是否适用于现有的 DOM 树。它必须重新计算所有的样式规则并重新应用它们--而不仅仅是改变了的那些。如果 React 发现另一个组件也产生了一个新的规则，同样的过程会再次发生。

解决这个问题的最好办法就是所有东西呈现给浏览器绘制前就进行改变。没错 `useInsertionEffect` 与 `useEffect` 有着同样的签名，但它会同步的在所有 DOM 更改之前触发。比 `useLayoutEffect` 还要早触发，这样就可以用于在重绘之前注入样式。

<RUASandpack
  template="react-ts"
  files={{
    '/App.tsx': sandpack['react18-new-hooks'].useInsertionEffect,
  }}
/>
