不使用 Provider + useMutableSource 的 React Context
这篇文章最初发表在我的个人博客上。
不久前,我读了 React 的 RFC 文档中一篇名为 的 RFC useMutableSource
;它是一项实验性功能,简而言之,它允许你安全地读取、写入甚至编辑外部源(在 React 组件树之外)。这是一个非常棒的功能,我对此感到非常高兴,但它同时也是实验性的。你可能知道我正在开发一个名为 jotai 的开源状态管理库。这个库宣布了一项新功能,即无 Provider 模式。要了解它是什么,可以想象一下 React Context,但不需要组件Provider
。它与组件并非完全相同,但它能让你大致了解它。
为什么是新的?
是的,我们有模式和库允许我们从外部源读取和写入,但正如我所说,这个可以让你安全地做事;不再撕裂。
撕裂
想象一下,如果我们有一个值(状态),A 和 B 分别从中读取它,但在渲染过程中,该值发生了变化。B 组件比 A 组件晚,因此在渲染过程中,A 组件中的值为 0,而在较新的组件(B)中,该值为 1。我们称之为撕裂;这意味着您在视口中看到来自同一源的两个不同值。这是 React 并发模式中一种较新且难以理解的实现;有关更多信息,请参阅此内容。
实验性的,我为什么要使用它?
所以我考虑了一下,我们有两个选择:
- React 的实验版本:
yarn add react@experimental
- 的一致版本,您可以从这里
useMutableSource
复制粘贴
我推荐第二种选择,因为它不会改变,而且只要我们没有useMutableSource
主要的反应版本,现在就很好。
没有提供者的上下文
我想我们已经找到了你来这里的原因,但在开始之前,别忘了看看我的Github和Twitter;你会看到很多很棒的内容,也能帮助我的学习之旅。那就开始吧。
下面的一些代码是用 typescript 编写的,因此即使对于不懂 typescript 的人来说也更容易理解。
开始
首先我们需要创建一个简单的全局对象,它包含三个属性:
const globalStore = {
state: { count: 0 },
version: 0,
listeners: new Set<() => any>()
};
state
:类似 React Context 值的简单值version
:当状态的任何部分发生变化时,必须改变的重要部分listeners
:每当我们改变部分内容时,我们都会调用一组函数state
,以便我们通知他们有关更改的信息
现在我们需要创建一个可变的源globalStore
并赋予它版本,这样它将有助于触发新的变化,所以我们将在getSnapshot
和中访问它subscribe
;我们很快会讨论这些。
const globalStoreSource = createMutableSource(
globalStore,
() => globalStore.version // (store) => store.version (Optional) if you use the consistent and non-experimental version of useMutableSource
);
现在是时候讨论一下了getSnapshot
;简而言之,它是一个useMutableSource
每当状态改变时返回其返回值的函数。
const cache = new Map();
const getSnapshot = (store: typeof globalStore) => {
const setState = (
cb: (prevState: typeof store.state) => typeof store.state
) => {
store.state = cb({ ...store.state });
store.version++;
store.listeners.forEach((listener) => listener());
};
if (!cache.has(store.state) || !cache.has(store)) {
cache.clear(); // remove all the old references
cache.set(store.state, [{ ...store.state }, setState]);
// we cache the result to prevent the useless re-renders
// the key (store.state) is more consistent than the { ...store.state },
// because this changes everytime as a new object, and it always going to create a new cache
cache.set(store, store); // check the above if statement, if the store changed completely (reference change), we'll make a new result and new state
}
return cache.get(store.state); // [state, setState]
};
// later: const [state, setState] = useMutableSource(...)
看一下该setState
函数,首先我们使用cb
并传递之前的状态,然后将其返回值分配给我们的状态,然后我们更新存储版本并通知所有监听器新的变化。
我们使用了扩展运算符,
({ ...store.state })
因为我们必须克隆该值,所以我们为新的状态对象创建一个新的引用并禁用直接突变。
我们还没有listener
,那么我们如何添加一个呢?使用subscribe
函数,看一下这个:
const subscribe = (store: typeof globalStore, callback: () => any) => {
store.listeners.add(callback);
return () => store.listeners.delete(callback);
};
该函数将被调用useMutableSource
,因此它传递subscribe
两个参数:
store
:这是我们原来的商店callback
:这将导致我们的组件重新渲染(通过useMutableSource
)
所以当useMutableSource
调用 subscribe 时,我们会将 添加callback
到我们的监听器中。每当状态发生变化时(setState
),我们都会调用所有监听器,以便组件重新渲染。这就是我们每次使用 时都能获得更新值的原因useMutableSource
。
所以你可能想知道我们为什么要删除回调函数,答案是,当组件卸载时,useMutableSource
会调用subscribe()
,或者换句话说,我们称之为unsubscribe
。当它被删除时,我们将不再调用无用的回调函数,因为这样会导致组件被重新渲染到已卸载的(有时是旧的)组件上。
useContext
现在我们到达了终点线,不要过多考虑名称,我们只是想模仿无提供程序版本的 React 上下文。
export function useContext() {
return useMutableSource(globalStoreSource, getSnapshot, subscribe);
} // returns [state, setState]
现在我们可以在任何需要的地方使用这个函数了。可以看看这个例子,或者你也可以直接访问codesandbox。
function Display1() {
const [state] = useContext();
return <div>Display1 component count: {state.count}</div>;
}
function Display2() {
const [state] = useContext();
return <div>Display2 component count: {state.count}</div>;
}
function Changer() {
const [, setState] = useContext();
return (
<button
onClick={() =>
setState((prevState) => ({ ...prevState, count: ++prevState.count }))
}
>
+1
</button>
);
}
function App() {
return (
<div className="App">
<Display1 />
<Display2 />
<Changer />
</div>
);
}
现在,只要您单击 +1 按钮,就可以看到漂亮的变化,没有任何问题Provider
。
希望你喜欢这篇文章,别忘了分享和评论我的文章。如果你想告诉我什么,可以在推特上告诉我,或者在其他地方提及我,你甚至可以订阅我的新闻通讯。
- 封面图片:实验,Nicolas Thomas,unsplash