feat: implement scoped atoms, inject
, and ecosystem.withScope
#172
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
@affects atoms, react, stores
Description
I spent a few days exploring the
inject
utility discussed in #129. I was mostly just experimenting but I somehow actually knocked the whole thing out. It is pretty complex but so powerful that I really want to start using it now 💪 . So here we are.Implement
inject
. This is a new injector that is a thin wrapper around React'suse
util (hence the name) but with a few key differences:inject
does not support promises. It will if we ever introduce async atoms (which are currently waiting on the TC39 async context proposal).inject
accepts atom templates and will pull a provided instance from<AtomProvider>
.inject
should not be used conditionally, though it can be used in loops, unlike normal injectors, as long as a given context is passed toinject
at least once on every evaluation.inject
calls should also, preferably, not be reordered, as that could result in ids generating with those values reordered in rare edge cases (extremely rare - you'd have to reset the ecosystem then restore some value that depended on the old id. Not likely to ever happen). Ifinject
is used conditionally, Zedux might not capture a contextual value that is actually required.Implement
ecosystem.withScope
, a new utility that accepts a scope and a callback function and runs the callback function in a scoped context with the passed scope. This is recursive - anyecosystem.withScope
calls inside an existing scope will recursively look up through all scopes to find a provided context.Scope
This is a new concept in Zedux. Some definitions:
atom()
factory)Examples
Scope in React:
This is typically how scoped atoms should be consumed. When used this way, you basically don't have to think about it. Scopes "just work". Zedux will throw an error if you forget to provide a contextual value.
Scopes outside React:
Extra Considerations
Some things we may want to address later.
String hash vs WeakMap
Scoped atoms get their scope as part of their id. This currently uses Zedux's React Query-esque key hashing algorithm. While this is very convenient for DX, it may be preferable to create an id for every provided object reference we encounter (using the ecosystem's existing
b
aseKeys WeakMap) for performance reasons. This is a candidate for an ecosystem config option so you can hash in dev, map in prod. Though note that they have slightly different behavior:Implicit Dependencies
Scopes implicitly change the behavior/needs of the atom they're scoping. This is why we need
inject
to not be called conditionally - so Zedux can figure out everything that's implicitly needed while we have scope, so we can use those values when we're no longer in a scoped context.Rather than putting this no-conditional-calls requirement on
inject
, we could give atoms the ability to define their scope explicitly, up-front as a config option passed toatom()
/similar:And Zedux would always capture those contextual values regardless of what
inject
actually uses.Scope Propagation
Scoped nodes propagate their scope to all observers. This makes scopes kind of a leaky abstraction - scoped nodes "leak" scope into everything that uses them. There are maybe helper APIs we could introduce to make working with multiple scopes easier.
Hydration
Scoped atoms can't be hydrated. They shouldn't need to be - their whole purpose is to pull values from a current, live scope.
While we could make it so scoped atoms are given an initial hydration (by making scopes explicit, see above), it would be counterproductive: The primary purpose of hydration is a page load speed boost. Hydrated scoped atoms would always make initial load slower since they must evaluate once before we know their scope anyway, then we'd find the hydration, hydrate, and reevaluate. While that's all fast, the tiny bit of extra overhead means hydrating scoped atoms will just never be good.
We should add a way to easily filter out scoped atoms when dehydrating the ecosystem.
Don't hydrate scoped atoms; hydrate the scope. I'll make sure this is documented.