概述
在React项目中说到状态管理,我们第一时间想到的就是使用useState、useReducer这种Hooks来进行状态管理。但是这种是针对React内部的状态,如果有时候我们需要订阅外部的状态并影响React组件的更新的话,那通过这种内部状态管理API显然不能满足了,这时候就需要使用本文的主角useSyncExternalStore
这个Hook了。这个API是React18提供的一个内置API,赋予了React能订阅外部状态的能力,当订阅的外部状态发生改变时,会触发React的重新渲染。本文将会从一下几个方面来逐步介绍该API的使用和原理,有需求的同学自行跳转至感兴趣的部分。
- 基础使用
- 源码解析
- Mount首次渲染时
- Update 更新渲染时
基础使用
函数定义
先看定义,useSyncExternalStore
接受三个参数:subscribe
、getSnapshot
、getServerSnapshot
然后返回一个数据快照。
export function useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T {
const dispatcher = resolveDispatcher();
return dispatcher.useSyncExternalStore(
subscribe,
getSnapshot,
getServerSnapshot,
);
}
详细说明如下:
- subscribe: 接收一个订阅函数,并返回一个取消订阅函数,该函数主要用于订阅外部store状态,订阅外部store的哪些状态取决于subscribe函数的实现。当订阅的状态值变化时,会触发组件的重现渲染。
- getSnapshot:一个用于获取当前状态快照的函数,React组件每次渲染都会执行该函数,然后获得当前订阅状态的快照,并将这个快照作为
useSyncExternalStore
函数返回值 - getServerSnapshot: 返回订阅状态的初始快照,它只会在服务端渲染(SSR)时,以及在客户端(浏览器端)进行服务端渲染内容的 hydration 时被用到。
- 返回值是一个数据快照,可以理解为就是
getSnapshot
的执行结果
PS: 由于getSnapshot
会在每次组件渲染时都会执行,所以一般将这个函数定义在组件外部或者使用useCallback
包裹,避免不必要的渲染。
想了解为什么使用
useCallback
包裹能避免重复渲染的可以查看这篇文章:【React Hooks原理 - useCallback、useMemo】
由于服务端渲染的是静态页面,不能进行动态交互,所以在React项目中会通过hydration(水合阶段),将其赋予动态交互能力,具体流程本文最后题外话会简单介绍。
语法使用
上面主要介绍了useSyncExternalStore
函数定义,这小节主要通过举例Demo的形式让我们初步了解该函数的运用,并方便下面源码解析的理解。(由于第三个参数getServerSnapshot
用于SSR,所以本文不做介绍,有兴趣的同学可以查看其他文章了解)
外部第三方Store定义:
- 这里要注意
subscribe
、getSnapshot
这两个函数会在useSyncExternalStore
Hook中调用并注入React自己的listener函数,所以这两个函数必须在第三方Store中暴露,否则不能正确使用useSyncExternalStore
功能。
const store = {
// 外部状态
state: {
todos: [],
user: {
name: 'Alice', age: 30 }
},
// 监听状态变化
listeners: new Set(),
// 订阅状态函数
subscribe(listener) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
},
// 获取快照函数
getSnapshot() {
return this.state;
},
// 状态更新后,通知所有注入的监听器并执行
notify() {
this.listeners.forEach(listener => listener());
},
// 更新todos状态
updateTodos(newTodos) {
this.state.todos = newTodos;
this.notify();
},
// 更新user状态
updateUser(newUser) {
this.state.user = newUser;
this.notify();
}
};
React项目代码:
import React from 'react';
import {
useSyncExternalStore } from 'react';
const todosStore = {
subscribe(listener) {
return store.subscribe(() => {
const {
todos } = store.getSnapshot();
listener(todos);
});
},
getSnapshot() {
const {
todos } = store.getSnapshot();
return todos;
}
};
const TodosComponent = () => {