This post was originally posted on my personal blog.
A while ago, I was reading an RFC from react's RFCs called useMutableSource
; it was an experimental feature that, in a nutshell, lets you safely read, write and even edit an external source (outside of the react components tree). It's a banger feature, which I'm really chuffed for it, but it's experimental at the same time. You may know that I'm working on an Open-source state-management library called jotai. This library announced a new feature, the Provider-less mode. To know what it is, think of React Context, but no need for a Provider
component, it's not exactly that, but it gives you the idea.
Why a new one?
Yea, we have patterns and libraries that allow us to read and write from an external source, but as I said, this one lets you do things safely; no tearing anymore.
Tearing
Think of tearing as something like if we have a value(state) that A and B read from it, but somehow in the rendering, the value changes. The B component is later than A, So in Rendering, the value in the A component is 0, and in the newer component (B), the value is 1. We call this tearing; it means you see two different values in the viewport from one exact source. It's a new and hard to understand implementation in React concurrent mode; for more information, see this.
Experimental, Why should I use it?
So I thought about this, we have two options:
- Experimental version of react:
yarn add react@experimental
- Consistent version of
useMutableSource
, you can copy paste it from here
I recommend the second option because it's not going to change, and good for now as long as we don't have useMutableSource
in a major react version.
Context with no Provider
I think we have reached what brought you here, but wait before all of this, don't forget to look at my Github and Twitter; you're going to see cool stuff there and help me with my learning journey too. So let's start.
Some of the code below is written with typescript, so it will be more understandable, even for people who don't know typescript.
Start
First we need to create a simple global object, which contains three properties:
const globalStore = {
state: { count: 0 },
version: 0,
listeners: new Set<() => any>()
};
-
state
: simple value like react Context value -
version
: important part that has to change whenever any part of the state changes -
listeners
: a set of functions that we call them every time we change part of thestate
, so we notify them about the changes
Now we need to create a mutable source from globalStore
and give it the version, so it'll help it with triggering new changes, so we're going to access it in getSnapshot
and subscribe
; we'll talk about these soon.
const globalStoreSource = createMutableSource(
globalStore,
() => globalStore.version // (store) => store.version (Optional) if you use the consistent and non-experimental version of useMutableSource
);
Now it's the time to talk about getSnapshot
; in a nutshell, it's a function that useMutableSource
returns its returned value whenever the state changes.
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(...)
Take a look at the setState
function, first we use cb
and pass it the previous state, then assign its returned value to our state, then we update the store version and notify all the listeners of the new change.
we used the spread operator
({ ...store.state })
because we have to clone the value, so we make a new reference for the new state object and disable direct mutations.
We don't have any listener
yet, so how we can add one? with the subscribe
function, take a look at this:
const subscribe = (store: typeof globalStore, callback: () => any) => {
store.listeners.add(callback);
return () => store.listeners.delete(callback);
};
This function's going to get called by useMutableSource
, So it passes subscribe
two parameters:
-
store
: which is our original store -
callback
: this is going to cause our component a re-render (byuseMutableSource
)
So when useMutableSource
calls the subscribe, we're going to add the callback
to our listeners. Whenever something changes in the state (setState
), we call all of our listeners so that the component will get re-rendered. That's how we have the updated value every time with useMutableSource
.
So you may wonder we delete the callback in return, the answer is that when the component unmounts, useMutableSource
will call subscribe()
, or in another term, we call it unsubscribe
. When it gets deleted, we'll no longer call a useless callback that will cause a re-render to an unmounted (or sometimes an old) component.
useContext
Now we reached the end line, don't think too much about the name, we just wanted to mimic the Provider-less version of React context.
export function useContext() {
return useMutableSource(globalStoreSource, getSnapshot, subscribe);
} // returns [state, setState]
Now we can use this function everywhere we want. Take a look at this example, or if you want, you could go straight for the 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>
);
}
Now whenever you click the +1 button, you can see the beautiful changes without any Provider
.
I hope you enjoyed this article, and don't forget to share and reaction to my article. If you wanted to tell me something, tell me on Twitter or mention me anywhere else, You can even subscribe to my newsletter.
- Cover image: Experiment, Nicolas Thomas, unsplash
Top comments (6)
Thanks for this article. And I just wonder, if I don't want to use redux, can I use this approach for any "global" state? I'm aware that react-redux uses this internally.
Currently hook stores the memorized state locally under each fiber. With this approach, I wonder where the store is placed.
Thanks for sharing.
Thank you, Fang, you made my day. Yes, you can use this approach, And I use it in my projects. Or you can use the better version of it, which we work on, jotai.
github.com/pmndrs/jotai
The store lives in a simple object, but we attach some useStates to it basically when we use uMS, so when it changes, we call those useStates(setStates) in the components. So no wasteful renders like React Context. Just your subscribed component is going to be changed.
github.com/reactjs/rfcs/blob/maste...
github.com/reactwg/react-18/discus...
thanks for the quick reply man, i actually came from the RFC page :)
will take a close look at article you forwarded and jotai. Always interested at some localized version of redux. Thank you.
Yes, I appreciate it. I recommend you take a look at the simulated version which I gave the link in the article. It's a good alternative to the experimental one.
Great article will definitely look into this. But I do question, couldn't this behavior be achieved with way less code using RxJs and Observables?
Thanks, Austin; I don't know actually about RxJS, but every solution except the react internals has some trade-offs, maybe tearing and ...., I just made a vanilla fast solution with uMS, but if you can merge the observables solution with uMS, why not, it would be great. The point of this article is teaching uMS, and we can do many amazing things with it.