不使用 Provider + useMutableSource 的 React Context

2025-06-08

不使用 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主要的反应版本,现在就很好。

没有提供者的上下文

我想我们已经找到了你来这里的原因,但在开始之前,别忘了看看我的GithubTwitter;你会看到很多很棒的内容,也能帮助我的学习之旅。那就开始吧。

下面的一些代码是用 typescript 编写的,因此即使对于不懂 typescript 的人来说也更容易理解。

开始

首先我们需要创建一个简单的全局对象,它包含三个属性:

 const globalStore = {
  state: { count: 0 },
  version: 0,
  listeners: new Set<() => any>()
};
Enter fullscreen mode Exit fullscreen mode
  • 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
);
Enter fullscreen mode Exit fullscreen mode

现在是时候讨论一下了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(...)
Enter fullscreen mode Exit fullscreen mode

看一下该setState函数,首先我们使用cb并传递之前的状态,然后将其返回值分配给我们的状态,然后我们更新存储版本并通知所有监听器新的变化。

我们使用了扩展运算符,({ ...store.state })因为我们必须克隆该值,所以我们为新的状态对象创建一个新的引用并禁用直接突变。

我们还没有listener,那么我们如何添加一个呢?使用subscribe函数,看一下这个:

const subscribe = (store: typeof globalStore, callback: () => any) => {
  store.listeners.add(callback);
  return () => store.listeners.delete(callback);
};
Enter fullscreen mode Exit fullscreen mode

该函数将被调用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]
Enter fullscreen mode Exit fullscreen mode

现在我们可以在任何需要的地方使用这个函数了。可以看看这个例子,或者你也可以直接访问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>
  );
}
Enter fullscreen mode Exit fullscreen mode

现在,只要您单击 +1 按钮,就可以看到漂亮的变化,没有任何问题Provider

希望你喜欢这篇文章,别忘了分享和评论我的文章。如果你想告诉我什么,可以在推特上告诉我,或者在其他地方提及我,你甚至可以订阅我的新闻通讯

  • 封面图片:实验,Nicolas Thomas,unsplash
鏂囩珷鏉ユ簮锛�https://dev.to/aslemammad/react-context-without-provider-usemutablesource-4aph
PREV
微服务身份验证策略:理论到实践 理论 实践
NEXT
如何在 Windows 上安装 DeepSeek-R1 32B:系统要求、Docker、Ollama 和 WebUI 设置