Charlie

V1

2023/04/03阅读:94主题:蔷薇紫

React新hook useSyncExternalStore

useSyncExternalStore

最近react官方出了最新英文文档,于是我打算对其中部分内容 原文地址

useSyncExternalStore是一个可以订阅外部store的react hook

const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)

参考

你应该在你的函数组件的顶层调用useSyncExternalStore,并且从外部store读取数据值

import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';

function TodosApp({
  const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
  // ...
}

这个hook会返回store里一个数据的快照。你需要传入两个函数作为入参

  1. subscribe函数得订阅这个store, 并且返回一个可以取消订阅的函数。
  2. getSnapshot函数得可以从store里读取数据快照。

下面有更多例子

参数

  • subscribe: 是一个函数,只有一个回调函数作为入参,并且使其订阅这个store. 当store发生改变的时候,这个回调函数应该得到执行,而且这会触发组件的重新渲染。subscribe函数应该返回一个可以取消订阅的方法。

  • getSnapshot: 是一个函数,返回一个组件中需要用到的store里的一个数据值的快照。当这个store没有改变的时候,重复调用getSnapshot必须返回同样的值。如果store发生改变并且返回的数据值不一样了(用Object.js做比较),那么Reacr重新渲染这个组件。

  • getServerSnapshot(可选参数): 是一个函数,返回store数据的初始快照。只会在服务端渲染的时候使用,并且是在服务端渲染好的内容往客户端灌水的时候。服务端的快照必须和客户端的一致。并且通常是序列化后被发送到客户端的,如果你不传这个参数,在服务端渲染的时候会报错

返回

当前你在组件渲染中使用到的store数据的快照

警告

  • 调用getSnapshot返回的这个store数据快照不能修改,如果所依赖的store有可以更改的数据,当数据发生改变返回新的不可修改的数据,否则返回上一次缓存的数据快照。

  • 如果在重新渲染的时候传来一个不同的subscribe函数,React会用新的subscribe重新订阅这个store。你可以通过在组件外面声明subscribe的方式来避免。

使用

订阅一个外部的store

通常你的React组件只会从组件的props、state、context里获取数据,然而有时候组件也会从外部store里读取数据,这个store里的数据也会改变。例如:

  • React以外的第三方的状态管理库所包含的状态
  • 浏览器API暴露的一个可以修改的状态值,并且可以通过事件订阅他的变化。
React会用这些函数让你的组件订阅这个store, 并且在其发生改变的时候重新渲染。
举个例子,在下面的沙箱中,todoStre是作为React外部的store来存储数据的. TodosApp 组件用useSyncExternalStorehook将外部store和组件本身联系起来了。

沙箱代码地址

// App.js
export default function TodosApp({
  const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
  return (
    <>
      <button onClick={() => todosStore.addTodo()}>Add todo</button>
      <hr />
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </>

  );
}
// This is an example of a third-party store
// that you might need to integrate with React.

// If your app is fully built with React,
// we recommend using React state instead.

let nextId = 0;
let todos = [{ id: nextId++, text'Todo #1' }];
let listeners = [];

export const todosStore = {
  addTodo() {
    todos = [...todos, { id: nextId++, text'Todo #' + nextId }]
    emitChange();
  },
  subscribe(listener) {
    listeners = [...listeners, listener];
    return () => {
      listeners = listeners.filter(l => l !== listener);
    };
  },
  getSnapshot() {
    return todos;
  }
};

function emitChange({
  for (let listener of listeners) {
    listener();
  }
}

注意

如果可以的话,我们建议使用React内置的useState来管理你的状态。useSyncExternalStoreAPI通常在你与已经存在的外部非React代码集成的时候才比较有用。

订阅一个浏览器API
另一个使用useSyncExternalStore的场景是,当你想订阅某个值,这个值是浏览器暴露出来的,并且会依情况发生改变。例如,如果你希望你的组件展示网络的连接状态是否为已连接, 浏览器会通过navigator.onLine暴露这个信息。
这个值navigator.onLine会发生改变,它和React相关的知识没关系,所以你可以用useSyncExternalStore来读取它。
import { useSyncExternalStore } from 'react';

function ChatIndicator({
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);
  // ...
}
要实现getSnapshot函数,就从浏览器API里读取当前的值
function getSnapshot({
  return navigator.onLine;
}
接下来,你需要实现subscribe函数,例如,当navigator.onLine改变的时候,window对象上会触发onlineoffline事件,你需要将回调参数订阅对应的事件,并且返回一个清除订阅的函数:
function subscribe(callback{
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}
现在React知道怎么从外部的navigator.onLine API读取值,也知道怎么订阅它的变化。断开你设备的网络,并注意观察组件的重新渲染:

沙箱代码地址

import { useSyncExternalStore } from 'react';

export default function ChatIndicator({
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);
  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

function getSnapshot({
  return navigator.onLine;
}

function subscribe(callback{
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

提取逻辑到自定义hook里

通常,你不会直接在你的组件里编写useSyncExternalStore,而是在你的自定义hook里调用,这可以让你在不同组件里使用同一个外部store.
例如,下面这个自定义的useOnlineStatus hook,追踪网络是否在线:
import { useSyncExternalStore } from 'react';

export function useOnlineStatus({
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);
  return isOnline;
}

function getSnapshot({
  // ...
}

function subscribe(callback{
  // ...
}
现在不同组件就可以调用 useOnlineStatus 而不需要重复实现:

沙箱代码地址

// App.js
import { useOnlineStatus } from './useOnlineStatus.js';

function StatusBar({
  const isOnline = useOnlineStatus();
  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

function SaveButton({
  const isOnline = useOnlineStatus();

  function handleSaveClick({
    console.log('✅ Progress saved');
  }

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? 'Save progress' : 'Reconnecting...'}
    </button>

  );
}

export default function App({
  return (
    <>
      <SaveButton />
      <StatusBar />
    </>

  );
}

// useOnlineStatus.js
import { useSyncExternalStore } from 'react';

export function useOnlineStatus({
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);
  return isOnline;
}

function getSnapshot({
  return navigator.onLine;
}

function subscribe(callback{
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

为服务端渲染提供支持

如果你的React应用使用服务端渲染,你的React组件也会在浏览器外部环境运行从而创建出实话的html, 当连接外部store时这会带来一些挑战:
  • 如果你链接浏览器才有的API, 这就不起作用,因为这在服务端不存在。
  • 如果你链接第三方数据store,你需要确保服务端和客户端的数据是一致的。
要解决这个问题,传入一个getServerSnapshot作为第三个参数给useSyncExternalStore
import { useSyncExternalStore } from 'react';

export function useOnlineStatus({
  const isOnline = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
  return isOnline;
}

function getSnapshot({
  return navigator.onLine;
}

function getServerSnapshot({
  return true// Always show "Online" for server-generated HTML
}

function subscribe(callback{
  // ...
}
这个getServerSnapshot函数类似于getSnapshot,但是它只在以下两种情况运行:
  • 在服务端生成HTML的时候运行
  • 在客户端注水期间运行,也就是说当React拿到服务端的HTML并使它可交互的时候。
这使你提供初始化的数据值快照,将用于应用在可交互前。如果没有提供有意义的初始化值给服务端渲染,就忽略这个参数,强制在客户端渲染。

注意

确保getServerSnapshot在客户端返回的初始化值是和服务端返回的初始化值是一样的。例如,如果getServerSnapshot在服务端提供了一些预填充的store内容,你需要将这个内容传递给客户端。一种处理这种情况的方式是,在服务端渲染时新增一个<script>标签,并设置一个全局变量,例如:window.MY_STORE_DATA,并且在客户端的时候在getServerSnapshot里读取这个全局数据值,你的外部store应该提供一个这样处理的指令.

疑难解答

我这报了一个错误:“The result of getSnapshot should be cached”

这个报错意味着你的getSnapshot方法每次调用的时候都返回一个新的对象,例如:

function getSnapshot({
  // 🔴 不要每次都从getSnapshot返回不同的对象
  return {
    todos: myStore.todos
  };
}
如果getSnapshot返回的值和上次不一样那么React就会重新渲染组件。这就是为什么,如果你总是返回不同的值,就会进入一个死循环,并且出现这个报错。
只有你的数据真的发生了变化,你的getSnapshot才需要返回一个不同的对象,如果你的store包含不可变的数据,你可以直接返回那个数据。
function getSnapshot({
  // ✅ 你可以直接返回这个不可变的数据
  return myStore.todos;
}
如果你的store数据是可变的,你的getSnapshot应该返回它的一个不可变的快照。这意味着,你需要创建一个新的对象,但不是每次调用的时候都创建,而是应该存储计算好的数据快照,并且这个数据在store里没变化的话就返回同一个快照。你的可变数据取决于你的可变store。

我的subscribe函数每次组件再渲染的时候都会调用。

这个subscribe是在组件里定义的,所以每次重新渲染的时候都是不同的函数。
function ChatIndicator({
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);
  
  // 🚩 每次都是一个不同的函数,所以React会再重新渲染的时候重新订阅
  function subscribe({
    // ...
  }

  // ...
}
如果你在重新渲染的时候传入一个不同的subscribe函数Reacr会重新订阅你的store. 如果这会造成性能问题,并且你想避免重新订阅,你就应该把subscribe函数定义在组件外边:
function ChatIndicator({
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);
  // ...
}

// ✅ 这样就会总是同一个函数了,因此Rect不会去重新订阅。
function subscribe({
  // ...
}

分类:

前端

标签:

React.js

作者介绍

Charlie
V1