Charlie
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里一个数据的快照。你需要传入两个函数作为入参」
-
subscribe
函数得订阅这个store, 并且返回一个可以取消订阅的函数。 -
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 组件用useSyncExternalStore
hook将外部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来管理你的状态。
❞useSyncExternalStore
API通常在你与已经存在的外部非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对象上会触发online
和offline
事件,你需要将回调参数订阅对应的事件,并且返回一个清除订阅的函数:
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() {
// ...
}
作者介绍