Dear reader,
In the last few years, I've been using my own global store implementation with great success (yes, I'm aware there exist a bazillion "redux like" stores). My one's called tiny-atom.
In particular, the following 4 requirements for a global store emerged from my needs:
For example, tiny-atom uses requestAnimationFrame
to delay/batch the re-renders.
If naively implemented – a child component re-renders once from parent re-render, once from it's own subscription.
For example, a child component can break reading something non existant from store, when parent was supposed to un-render that child.
Important for performance, when many components bind to small slices of the shared state.
My current implementation is not Concurrent Mode ready. So, excitedly, I've tried out the new useMutableSource
in [email protected]
in place
of my current approach. It works pretty great! The useMutableSource
hook takes care of requirements 1-3 out of the box only leaving requirement 4 to the implementer!
Right now, I'm looking for the following answers:
How to correctly prevent re-rendering after store change if new snapshot is shallowly equal?
You can see my current approach below, but I don't think it's correct wrt Concurrent Mode.
UPDATE: I have updated the gist and code sandbox with a correct (?) implementation of selective subscription now. No more mutating refs.
Is this type of implementation fully Concurrent Mode compatible?
I tried running this simple store implementation through the https://github.com/dai-shi/will-this-react-global-state-work-in-concurrent-mode test suite and the following checks fail:
✕ check 7: proper branching with transition
✕ check 9: no tearing with auto increment
My concern is - does that mean useMutableSource
still has some tearing issues in some cases? (Note: I haven't looked into the the implementation of the test suite closely).
UPDATE Anyone know how to convert this super simple store into a fully React compliant store? E.g. I don't mind if it's turned into a useState/useReducer as long as it can be used in a similar manner?
I've put the code used in my exercise here: https://codesandbox.io/s/flamboyant-keller-m9unj
App.js
- a demo app for exploring the 4 requirements abovecreateStoreModern.js
- simpler store used inuseMutableSource
implementaionuseStoreModern.js
- implementation of store hooks usinguseMutableSource
createStoreLegacy.js
- more complex store that allows order specified subscriptionsuseStoreLegacy.js
- implementation of hooks withoutuseMutableSource
I'm also including the code for the modern implementation directly below
Yes, fair point, thus Question 1 - it's important to be able to not re-render if selected snapshot is shallowly the same - but the question is how?
I don't have an actual test (could write one), but you can sort of see that in the demo app linked in the Codesandbox.
In the sandbox demo app (https://m9unj.csb.app/) - if you click "Increment" - a "Modal" component shows up at the bottom. If you click "Increment" again, the "Modal" component is hidden without being re-rendered, even though that was the first subscription to fire:
In other words, it does not matter what order the subscriptions get fired in (listeners.push vs listeners.unshift, etc.) - the tree gets re-rendered in order. As opposed to when you use
useEffect()
to subscribe and thensetState
to re-render - this "Modal" component in the demo app would re-render first causing a crash (Note, this scenario is not in the sandbox, but I could add it).I don't know if this is proof enough.. it's a bit basic, but this is essentially how I've tested this scenario in the past.
Btw, as an aside, I've avoided the need for context based nested subscription mechanism like the one used in Redux (last I checked?), by keeping track of render order in a ref using a globally incrementing counter.. You then use this counter to find the right place for a subscription to be inserted into the listeners array. This way if you have nested components App > Page > Widget.. App gets assigned order 1, Page order 2, Widget order 3, they get pushed into subs in that order even though useEffect fires for Widget first, Page second, App third. Then, if another subtree is added under app, e.g. App > Modal > Button, these get assigned render order 1 (from before), 4 and 5 respectively and get pushed in the right order as well, making sure that App re-renders before Modal.