feat!: implement proxies, signals, and mapped signals #147
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, core, machines, react, stores
Description
Introduce
injectSignal
,injectMappedSignal
,ecosystem.signal
, theSignal
class, and theProxyWrapper
class. Signals now replace stores as the primary state container of atoms.More particularly, atoms now are signals -
class AtomInstance extends Signal
. When an atom returns a signal from the state factory, the atom becomes a thin wrapper around that signal. Otherwise, the atom itself is the signal. Whether an atom is or has a signal is an internal implementation detail of the atom. Consumers never have to know which it is - there is no.signal
property on atoms, unlike.store
.Expand
ExternalNode
andGraphNode#on
functionality to be a full event emission system. Export anAs
type helper for specifying custom signal events. Basic usage:Signal-wrapping atoms forward all events sent to them (
instance.send('myEvent')
) to the wrapped signal and all events sent to the wrapped signal (signal.send('myEvent')
) to its own listeners (instance.on('myEvent', callback)
).Implement
signal.mutate
, which recursively createsProxyWrapper
s and currently supports native JS arrays, objects, and sets. The mutation is translated into a list of "transactions" and sent to anymutate
listeners:signal.mutate
also accepts an object overload and a function-returning-object overload. Zedux recursively iterates over the object entries, applying each mutation for you (skipping undefined values as per #95).Mapped signals forward all state updates (via
.set
and.mutate
) and events (via.send
) up to relevant wrapped signals and propagates all changes and events in wrapped signals down to itself and thence to its own observers.Update all tests/example code snippets. Move the store-heavy ones to a
packages/react/test/stores
folder and migrate the rest to use signals. This gets us decent coverage of these changes. There's still a lot more to test, but I'm confident enough that the new APIs and core signal-based functionality are at least basically working.TODO
Besides testing and docs, there are quite a few code-level things still to do - too many to list here. I added
TODO
comments all throughout the code that I believe cover everything. While there are lots of individual items, every one is tiny in scope compared to the monumental changes in #114 and this PR.In short, despite the few PRs, we're maybe 90% of the way to Zedux v2. I expect to have Zedux v2 release candidates within the next few weeks 🎉
Deprecations
The
getInstance
atom getter is deprecated in favor ofgetNode
, which is more appropriately named, more robustly-typed to handle all nodes - not just atom instances - and shorter 😄I also deprecated
AtomInstance#getState
in favor of the newget
method (which the old AtomInstance class inherits from the new one, specifically overriding to work with the store), which will help marginally with migrating.Breaking Changes
Every
@zedux/atoms
API and type that worked with stores is now moved into a new@zedux/stores
package which depends on@zedux/atoms
. Every single API should continue to function as before. To migrate, simply change all usages of the following imports from@zedux/react
(or@zedux/atoms
which the react package re-exporrts) to@zedux/stores
:Notably,
injectStore
is no longer exported from@zedux/react
(i.e.@zedux/atoms
) at all. When working with stores, be sure to import the oldatom
,api
, andion
factories from@zedux/stores
as well, not the new ones from@zedux/react
.Many old atom type helpers are also only found in
@zedux/stores
now. The new helpers that are in@zedux/atoms
now have a*Of
format instead ofAtom*Type
. Simple incremental migration example:Since the new and old types/APIs work fine together, you can migrate in whatever increments you want.
No Codemod
We will probably not be making a codemod to migrate codebases to signals - for most usages, migration is as simple as:
and changing the
atom
,ion
, andapi
imports to@zedux/react
or@zedux/atoms
. While these basic usages are a simple find-and-replace, any composed-store usages, especially involving effects subscribers, are much less simple. Atoms and stores are different enough that a codemod could easily introduce subtle bugs. There still may be value in handling only the simple cases with a code mod, but since the@zedux/stores
package maintains full backwards-compatibility, I'm recommending to switch to that and and manually migrate your codebase incrementally to signals.Issues
#115 (does not fully resolve, but implements almost everything so we can get started using and testing the signals implementation)
Resolves #95. We may also change
store.setStateDeep
to ignore undefined, but it's probably better to leave that alone and recommend switching to signals instead to getsignal.mutate
's improved functionality.