Our benchmarks revealed several performance bottlenecks in Zedux. None of these are problematic - Zedux is already 2-3x faster than Jotai, which itself one of the fastest state managers. However, we determined that Zedux is capable of gunning for some insane numbers akin to the fastest signals libs (like S.js, Solid signals, and Reactively). But it required a big rework.
The Rework
Zedux v1 stores atom instances in the ecosystem (ecosystem._instances), selectors in the Selectors class (ecosystem.selectors._items), and the graph nodes for each atom instance or selector cache in the Graph class (ecosystem._graph.nodes).
This led to lots of unnecessary reads - every time an atom instance updates, it has to find its graph node. Every time the graph propagates changes to nodes, it has to find the node's instances. We did this originally to support instance force-destruction - if an instance was destroyed, its graph node would remain until its dependents recreated the atom instance. But it didn't take long to determine this approach is unnecessary.
I refactored all the node/instance storage locations to use JS Maps, which are significantly faster at reads/writes/iterating when immutability isn't involved (which it isn't here). But doubling up the node storage locations still slows things down quite a bit.
Migrate ecosystem._instances, ecosystem.selectors._items, and ecosystem._graph.nodes to a single ecosystem.nodes map. This speeds up Zedux by almost 2x.
Additionally, the system for tracking graph updates during evaluation and applying them was unnecessarily slow, using a duplicated stack and creating lots of extra JS maps and objects for GC. Replace all of this with reusing the existing evaluation "stack" and a single pendingFlags mutation on every graph edge to gain an extra 10-20% speed boost.
Additionally x2, the most expensive piece of every atom instance and selector cache is the creation of a closure that can be passed to the scheduler to schedule jobs for that instance. Rather than creating a new function for every node, make the nodes themselves valid scheduler jobs. This way, we can reuse prototype methods for the scheduler's callbacks. This earns another 10-20% speed boost.
Some further perf tweaks included giving selector instances access to the ecosystem, giving both source and observer nodes access to the graph edge, and prioritizing checks for nodes in functions that accept either nodes or templates (so nodes can be passed directly for a speed boost).
Summary:
// Before:
interface Job {...}
interface EcosystemGraphNode {...}
class AtomInstanceBase {...}
class AtomInstance extends AtomInstanceBase {...}
class SelectorCache {...}
// After:
interface Job {...}
class GraphNode implements Job {...}
class AtomInstance extends GraphNode {...}
class SelectorInstance extends GraphNode {...}
I also removed Zedux's source maps - Vite wasn't generating them correctly anyway and the non-minified output of the dev builds is very readable. Plus the source maps tripled the size of the npm package and prevented chrome's line-level profiling from kicking in. This also means we don't have to ship the src code of each package to npm, further reducing the downloaded package size.
Phase 2
The keen observer will notice that the broadPropagation benchmark, while almost 3x faster than v1, is still significantly slower than the rest. This is because Zedux uses an O(n log n) insertion sort algorithm to schedule jobs for every node update. While this is almost on par with top-tier solutions for deep updates and all other metrics, it's significantly slower for broad updates (e.g. a single atom that's used directly by many other atoms).
Broad graphs are by far the most common in real-world scenarios, so this is something we need to address. In fact, it's the main purpose of this PR, even though this PR doesn't address it directly yet.
This rework is a necessary phase 1 step that simplifies the graph. This will help with switching us to a system that can skip the scheduler for most graph updates. Phase 2 will implement the actual skipping of the scheduler. I'm currently planning on phase 2 being part of Zedux v2, but seeing the crazy gains we've already achieved, I might want to push it back to v3. I'm open to any thoughts.
Breaking Changes
This rework required a few breaking changes. Beyond the required ones, we have lots of changes planned for Zedux v2. I took this opportunity to get a headstart on quite a few of those, so this PR has a few more changes than necessary. I'll try to list every change here so we can copy them to the migration guide.
This PR also starts migrating us to using single-letter properties in Zedux's classes/objects for internal fields. These single letters are well documented and a shortened version of a real property name, so they're easy to work with once you know them. The idea is that end users shouldn't need to use these internal fields, so they're obfuscated by using single-letters just like React 19 now obfuscates its internals. This along with the main refactor reduces the minified bundle size of the React package by 5kb already. And we're not done yet. I won't document these single-letter properties here, but the plan is to document them in the official docs before v2.
When referring to these single-letter properties, we type e.g. ecosystem.nodes, keeping it user-friendly while still showing that the property's actual name is simply n.
We're also reducing the number of classes used, preferring plain functions wherever possible since they minify so much better and properly hide internal details.
For any _-prefixed public properties, some have new equivalents and some were removed or are no longer exposed. I'm not documenting these here since it's unlikely anyone was using them (and you should be aware they're prone to break if you do). One goal of Zedux v2 is to remove these unstable-but-exposed methods, hiding stuff that's really internal and properly exposing stuff that should be exposed - via stable single-letter properties for things most users shouldn't care about.
Deleted Selectors class
This class was where selector caches were stored and served as a bridge between them and the ecosystem. You could access it via ecosystem.selectors. That property is gone.
The parts of this class that dealt with an individual cached selector have been moved to the new SelectorInstance class. The parts that dealt with the all selectors have been moved to the Ecosystem class.
Selectors#addDependent moved to GraphNode#on (which SelectorInstance inherits).
Selectors#dehydrate merged with ecosystem.dehydrate. We're still exploring options for how to dehydrate only selectors or atoms, but currently ecosystem.dehydrate('@@selector') works to dehydrate only selectors.
Selectors#destroyCache moved to GraphNode#destroy (which SelectorInstance inherits).
Selectors#find merged with ecosystem.find
Selectors#findAll merged with ecosystem.findAll
Selectors#getCache moved to ecosystem.getNode (see below for the new AtomGetters format)
Selectors#getCacheId moved to ecosystem.hash
See above note on _-prefixed public properties.
Just as atom instances are instances of an atom template, selector instances are now considered instances of a selector "template". The template is the selector function or selector config object. Unlike atoms, the function/object "template" reference matters - all its instances are cached under an id that the template is WeakMapped to.
SelectorCache reworked as SelectorInstance
You could get instances of SelectorCache via ecosystem.selectors.getCache. The concept of a selector "cache" is gone, replaced with selector "instances". Selector caches always functioned very similarly to atom instances and will even more so in v2. The new naming makes that apparent.
SelectorCache#isDestroyed moved to SelectorInstance#lifecycleStatus. We'll probably expose a cycle or status getter that's more user-friendly than the single-letter lifecycleStatus property. Not in this PR.
SelectorCache#nextReasons moved to SelectorInstance#why. These are really only used internally, accessed via ecosystem.why during evaluation. The new single-letter name reflects that.
SelectorCache#prevReasons removed with no replacement. See below section on prevReasons.
SelectorCache#result moved to GraphNode#get (which SelectorInstance inherits).
SelectorCache#selectorRef moved to GraphNode#template (which SelectorInstance inherits).
SelectorCache#args moved to GraphNode#params (which SelectorInstance inherits).
Deleted EvaluationStack class
This class kept track of what was evaluating and exposed the universal reactive atomGetters object that gets passed to all ions and atom selectors. You could access it via ecosystem._evaluationStack. That property is gone.
Another optimization introduced in this PR is to replace the evaluation "stack" inline by mutating a single object. This is significantly faster than pushing/popping an array. See benchmark here.
We also want to be able to access the reactive atomGetters much easier than the current ecosystem._evaluationStack.atomGetters since it's how Zedux mimics signals libraries - any reactive context calling a reactive atomGetter automatically gets its graph dependencies updated.
Most of this class has been migrated to internal utils in our evaluationContext.ts util. The exception is:
_evaluationStack.atomGetters moved to ecosystem.live. Since _evaluationStack was underscore prefixed, it's unlikely anyone was using this, which is just wrong. It's meant to be used. The new location indicates that so much better.
Deleted Graph class
Ah it served us well. This kept track of all graph nodes and ran all graph update operations, including propagating state/promise changed or force destruction notifications to a node's dependents. It could be accessed via ecosystem._graph. That property is gone.
Most of this class's functionality has been moved to the new GraphNode class which both AtomInstance and the new SelectorInstance class inherit. This was the primary focus of this refactor - to make nodes take care of their own operations.
Nothing to document here, since ecosystem._graph was underscore-prefixed, so it was likely unused. Comment if you have an exception and I'll fill in any migration details.
Replaced AtomInstanceBase class with GraphNode
AtomInstanceBase was kind of a pointless placeholder class that AtomInstance inherited from. Its purpose was supposed to be defining everything that a valid atom instance requires for Zedux to be able to work with it. You would then be able to create your own classes that extend AtomInstanceBase that add functionality (or remove bloat) from the standard AtomInstance class. These base-extending classes would work with all of Zedux's APIs, TypeScript included.
After this refactor, the new GraphNode class is the natural successor to AtomInstanceBase. It's much more useful by itself and accomplishes everything AtomInstanceBase was designed to do.
The only functionality this class had was the addDependent method which has been moved to GraphNode#on.
Note that we're still not documenting how to extend Zedux's classes as part of v2, but maybe soon after.
AtomInstance Changes
AtomInstance#status moved to GraphNode.lifecycleStatus. Same as selectors, we'll probably reintroduce a user-friendly cycle or status getter later (before v2).
AtomInstance#prevReasons removed with no replacement. See below section on prevReasons.
AtomInstance#ecosystem moved to a single-letter GraphNode#ecosystem property (which AtomInstance inherits). Users shouldn't be accessing the ecosystem from a node. This now indicates that, but the new property is stable and can be used if needed.
AtomInstance#_createdAt removed with no replacement. This was adding some unnecessary overhead and cruft to all atom instances. I've yet to use this property for debugging or anything. It can theoretically be useful though. Adding it back is already possible via plugins if needed.
Removed All prevReasons Properties
prevReasons had no internal use and was really just for user benefit, to see why an atom or selector evaluated last time it did. However, we found we never used it and it actually causes a "memory leak" of sorts:
The prevReasons would hold onto the previous state of the node. If the node had giant state (we had one with 27MB of data), it could really add up. It isn't a real memory leak since that memory is cleaned up the next time the node evaluates (or if it's destroyed), but it's still some very unnecessary cruft.
So we removed prevReasons from selectors and atom instances. It's still possible to track previous evaluation reasons yourself using an ecosystem plugin on the stateChanged mod if you do need this functionality.
Plugin Action Changes
Not documenting these here as Zedux v2 is reworking plugins entirely, but this PR modifies several plugin action payload types to use GraphNodes, which is more useful than the previous AnyAtomInstance | SelectorCache unions.
getInternals Changes
Nobody should be touching Zedux's internals (returned from the exported getInternals function). Its entire structure changed and will probably change again before v2. Comment if you need any details.
Deprecations
Zedux v2 will deprecate several things. This PR only deprecates one:
AtomInstance#getState deprecated in favor of the new GraphNode#get (which AtomInstance inherits).
TypeScript
AtomGenericsPartial replaced with the improved AnyAtomGenerics type helper (see below section on TypeScript in the list of new features)
New Features
This refactor also naturally added some new features, mostly as part of GraphNodes being standardized.
Ecosystem Features
ecosystem.dehydrate and ecosystem.findAll now accept similar exclude/include filters. You can now ecosystem.dehydrate('mySearchKey')! I already use this a lot.
ecosystem.live atom getters. Zedux now more closely mimics (other) signals implementations. Call ecosystem.live.get(myTemplate) in any reactive context to automatically register graph dependencies.
ecosystem.hash returns the fully qualified id for the passed selector+params combo. The plan is to make this replace _idGenerator.hashParams, supporting all types of nodes, not just selectors.
See below for the new atom getters changes (ecosystems are valid atom getters objects).
The New Atom Getters
In Zedux v1, atom getters have this shape:
{ ecosystem, get, getInstance, select }
While we won't remove any of that in v2, we will deprecate both getInstance and select. The plan is that Zedux v3 will have this atom getters shape:
{ ecosystem, get, getNode }
getInstance is replaced with the new getNode atom getter
select is replaced by get! :tada:. This will make refactoring between atoms and selectors much easier.
The plan is to also deprecate injectAtomSelector and useAtomSelector in favor of injectAtomValue/useAtomValue (or the get atom getter of course).
This PR only introduces the new getNode atom getter to the Ecosystem class. It is not formalized yet and nothing is deprecated yet, I'll do that in a follow-up PR. We had to add it now since the Selectors class was removed and we needed some way to get a selector instance.
The GraphNode class
Both AtomInstance and the new SelectorInstance class extend this class. GraphNode implements the Job interface, meaning all graph nodes are valid scheduler jobs.
All graph nodes have the following non-obfuscated properties:
destroy. This takes an optional force parameter, meaning AtomInstance#destroy retains its exact signature and functionality, but SelectorInstances get it too! You can now ecosystem.getNode(mySelector).destroy(true) to force-destroy a cached selector.
get. This is replacing SelectorCache#result and AtomInstance#getState as the new standard to get a node's state. We're considering making it reactive by default. Not all nodes have to have state - in fact the current plan is that v2 will introduce signals-style effects that are stateless graph nodes.
id. The unique id of the graph node.
on. This is replacing ecosystem.selectors.addDependent and AtomInstance#addDependent as a much more streamlined, optionally event-based way to add dependencies to a node. Pass a function as the first param to be notified on all events. Or pass an event name (currently "Updated" or "Destroyed" but those are changing before v2) to only be notified on those events. Zedux v2 will also add a similar property to ecosystems which will replace the current verbose plugin system.
params. A reference to the params used to instantiate the node. These will be passed to the selector or atom state factory every time it runs.
template. A reference to the template that this node is an instance of.
The new SelectorInstance class adds nothing user-facing on top of the base GraphNode class. That's really the purpose of a selector - to be the most lightweight node possible.
TypeScript
This PR introduces some wildly useful TypeScript improvements.
AtomGenerics now have Node and Template attached. This means, among other things, that atom instances of custom atom templates will have type safe access to all their custom class's properties.
The atom and ion factories properly apply the Node and Template generics recursively, fixing lots of ugly type errors about template._createInstance and instance.template types not matching.
The new GraphNode class accepts AtomGenerics, meaning SelectorInstances can now be passed to Zedux's AtomStateType, AtomParamsType, and AtomTemplateType type helpers to get the type of the relevant properties.
The new NodeOf type helper can pull the SelectorInstance type from a selector function or selector config object.
The AnyAtomGenerics type helper has been expanded to work similarly to AnyAtomTemplate and AnyAtomInstance - pass in any generics and the rest will default to any.
A new SelectorGenerics type helper, useful when creating functions that accept SelectorInstances (not selector templates - those are functions and can't be inferred by a type map like Zedux's AtomGenerics/SelectorGenerics).
New DehydrationFilter/DehydrationOptions and NodeFilter/NodeFilterOptions for the standardized include/exclude filter types.
The ParamlessTemplate type helper has been expanded for both selectors and selector templates.
All classes now allow passing no generics :tada:. They will default to any if not passed or inferred, though everything in Zedux is designed to infer well.
On that last note, we are considering deprecating all Atom*Type type helpers in v2 in favor of *Of (e.g. AtomStateType -> StateOf). Besides being more succinct, this is a pattern that I think is catching on in the TS community, so we want to stick to standards.
Summary
This PR is bigger than it needs to be, but really only by a little and it gets us well on our way to Zedux v2. I'll PR this against master, but don't merge it until the next/v1.3.x branch is merged into master, master is ready to be moved into v2 prep phase, and the v1.x support branch is cut.
This PR leaves some loose ends. I'll sum them all up in the Zedux v2 roadmap.
Description
Our benchmarks revealed several performance bottlenecks in Zedux. None of these are problematic - Zedux is already 2-3x faster than Jotai, which itself one of the fastest state managers. However, we determined that Zedux is capable of gunning for some insane numbers akin to the fastest signals libs (like S.js, Solid signals, and Reactively). But it required a big rework.
The Rework
Zedux v1 stores atom instances in the ecosystem (
ecosystem._instances
), selectors in the Selectors class (ecosystem.selectors._items
), and the graph nodes for each atom instance or selector cache in the Graph class (ecosystem._graph.nodes
).This led to lots of unnecessary reads - every time an atom instance updates, it has to find its graph node. Every time the graph propagates changes to nodes, it has to find the node's instances. We did this originally to support instance force-destruction - if an instance was destroyed, its graph node would remain until its dependents recreated the atom instance. But it didn't take long to determine this approach is unnecessary.
I refactored all the node/instance storage locations to use JS Maps, which are significantly faster at reads/writes/iterating when immutability isn't involved (which it isn't here). But doubling up the node storage locations still slows things down quite a bit.
Migrate
ecosystem._instances
,ecosystem.selectors._items
, andecosystem._graph.nodes
to a singleecosystem.nodes
map. This speeds up Zedux by almost 2x.Additionally, the system for tracking graph updates during evaluation and applying them was unnecessarily slow, using a duplicated stack and creating lots of extra JS maps and objects for GC. Replace all of this with reusing the existing evaluation "stack" and a single
p
endingFlags mutation on every graph edge to gain an extra 10-20% speed boost.Additionally x2, the most expensive piece of every atom instance and selector cache is the creation of a closure that can be passed to the scheduler to schedule jobs for that instance. Rather than creating a new function for every node, make the nodes themselves valid scheduler jobs. This way, we can reuse prototype methods for the scheduler's callbacks. This earns another 10-20% speed boost.
Some further perf tweaks included giving selector instances access to the ecosystem, giving both source and observer nodes access to the graph edge, and prioritizing checks for nodes in functions that accept either nodes or templates (so nodes can be passed directly for a speed boost).
Summary:
Numbers:
I also removed Zedux's source maps - Vite wasn't generating them correctly anyway and the non-minified output of the dev builds is very readable. Plus the source maps tripled the size of the npm package and prevented chrome's line-level profiling from kicking in. This also means we don't have to ship the
src
code of each package to npm, further reducing the downloaded package size.Phase 2
The keen observer will notice that the
broadPropagation
benchmark, while almost 3x faster than v1, is still significantly slower than the rest. This is because Zedux uses anO(n log n)
insertion sort algorithm to schedule jobs for every node update. While this is almost on par with top-tier solutions for deep updates and all other metrics, it's significantly slower for broad updates (e.g. a single atom that's used directly by many other atoms).Broad graphs are by far the most common in real-world scenarios, so this is something we need to address. In fact, it's the main purpose of this PR, even though this PR doesn't address it directly yet.
This rework is a necessary phase 1 step that simplifies the graph. This will help with switching us to a system that can skip the scheduler for most graph updates. Phase 2 will implement the actual skipping of the scheduler. I'm currently planning on phase 2 being part of Zedux v2, but seeing the crazy gains we've already achieved, I might want to push it back to v3. I'm open to any thoughts.
Breaking Changes
This rework required a few breaking changes. Beyond the required ones, we have lots of changes planned for Zedux v2. I took this opportunity to get a headstart on quite a few of those, so this PR has a few more changes than necessary. I'll try to list every change here so we can copy them to the migration guide.
This PR also starts migrating us to using single-letter properties in Zedux's classes/objects for internal fields. These single letters are well documented and a shortened version of a real property name, so they're easy to work with once you know them. The idea is that end users shouldn't need to use these internal fields, so they're obfuscated by using single-letters just like React 19 now obfuscates its internals. This along with the main refactor reduces the minified bundle size of the React package by 5kb already. And we're not done yet. I won't document these single-letter properties here, but the plan is to document them in the official docs before v2.
When referring to these single-letter properties, we type e.g.
ecosystem.n
odes, keeping it user-friendly while still showing that the property's actual name is simplyn
.We're also reducing the number of classes used, preferring plain functions wherever possible since they minify so much better and properly hide internal details.
For any
_
-prefixed public properties, some have new equivalents and some were removed or are no longer exposed. I'm not documenting these here since it's unlikely anyone was using them (and you should be aware they're prone to break if you do). One goal of Zedux v2 is to remove these unstable-but-exposed methods, hiding stuff that's really internal and properly exposing stuff that should be exposed - via stable single-letter properties for things most users shouldn't care about.Deleted
Selectors
classThis class was where selector caches were stored and served as a bridge between them and the ecosystem. You could access it via
ecosystem.selectors
. That property is gone.The parts of this class that dealt with an individual cached selector have been moved to the new
SelectorInstance
class. The parts that dealt with the all selectors have been moved to theEcosystem
class.Selectors#addDependent
moved toGraphNode#on
(whichSelectorInstance
inherits).Selectors#dehydrate
merged withecosystem.dehydrate
. We're still exploring options for how to dehydrate only selectors or atoms, but currentlyecosystem.dehydrate('@@selector')
works to dehydrate only selectors.Selectors#destroyCache
moved toGraphNode#destroy
(whichSelectorInstance
inherits).Selectors#find
merged withecosystem.find
Selectors#findAll
merged withecosystem.findAll
Selectors#getCache
moved toecosystem.getNode
(see below for the new AtomGetters format)Selectors#getCacheId
moved toecosystem.hash
_
-prefixed public properties.Just as atom instances are instances of an atom template, selector instances are now considered instances of a selector "template". The template is the selector function or selector config object. Unlike atoms, the function/object "template" reference matters - all its instances are cached under an id that the template is WeakMapped to.
SelectorCache
reworked asSelectorInstance
You could get instances of
SelectorCache
viaecosystem.selectors.getCache
. The concept of a selector "cache" is gone, replaced with selector "instances". Selector caches always functioned very similarly to atom instances and will even more so in v2. The new naming makes that apparent.SelectorCache#isDestroyed
moved toSelectorInstance#l
ifecycleStatus. We'll probably expose acycle
orstatus
getter that's more user-friendly than the single-letterl
ifecycleStatus property. Not in this PR.SelectorCache#nextReasons
moved toSelectorInstance#w
hy. These are really only used internally, accessed viaecosystem.why
during evaluation. The new single-letter name reflects that.SelectorCache#prevReasons
removed with no replacement. See below section onprevReasons
.SelectorCache#result
moved toGraphNode#get
(whichSelectorInstance
inherits).SelectorCache#selectorRef
moved toGraphNode#template
(whichSelectorInstance
inherits).SelectorCache#args
moved toGraphNode#params
(whichSelectorInstance
inherits).Deleted
EvaluationStack
classThis class kept track of what was evaluating and exposed the universal reactive
atomGetters
object that gets passed to all ions and atom selectors. You could access it viaecosystem._evaluationStack
. That property is gone.Another optimization introduced in this PR is to replace the evaluation "stack" inline by mutating a single object. This is significantly faster than pushing/popping an array. See benchmark here.
We also want to be able to access the reactive atomGetters much easier than the current
ecosystem._evaluationStack.atomGetters
since it's how Zedux mimics signals libraries - any reactive context calling a reactive atomGetter automatically gets its graph dependencies updated.Most of this class has been migrated to internal utils in our
evaluationContext.ts
util. The exception is:_evaluationStack.atomGetters
moved toecosystem.live
. Since_evaluationStack
was underscore prefixed, it's unlikely anyone was using this, which is just wrong. It's meant to be used. The new location indicates that so much better.Deleted
Graph
classAh it served us well. This kept track of all graph nodes and ran all graph update operations, including propagating state/promise changed or force destruction notifications to a node's dependents. It could be accessed via
ecosystem._graph
. That property is gone.Most of this class's functionality has been moved to the new
GraphNode
class which bothAtomInstance
and the newSelectorInstance
class inherit. This was the primary focus of this refactor - to make nodes take care of their own operations.Nothing to document here, since
ecosystem._graph
was underscore-prefixed, so it was likely unused. Comment if you have an exception and I'll fill in any migration details.Replaced
AtomInstanceBase
class withGraphNode
AtomInstanceBase
was kind of a pointless placeholder class thatAtomInstance
inherited from. Its purpose was supposed to be defining everything that a valid atom instance requires for Zedux to be able to work with it. You would then be able to create your own classes that extendAtomInstanceBase
that add functionality (or remove bloat) from the standardAtomInstance
class. These base-extending classes would work with all of Zedux's APIs, TypeScript included.After this refactor, the new
GraphNode
class is the natural successor toAtomInstanceBase
. It's much more useful by itself and accomplishes everythingAtomInstanceBase
was designed to do.The only functionality this class had was the
addDependent
method which has been moved toGraphNode#on
.Note that we're still not documenting how to extend Zedux's classes as part of v2, but maybe soon after.
AtomInstance
ChangesAtomInstance#status
moved toGraphNode.l
ifecycleStatus. Same as selectors, we'll probably reintroduce a user-friendlycycle
orstatus
getter later (before v2).AtomInstance#prevReasons
removed with no replacement. See below section onprevReasons
.AtomInstance#ecosystem
moved to a single-letterGraphNode#e
cosystem property (whichAtomInstance
inherits). Users shouldn't be accessing the ecosystem from a node. This now indicates that, but the new property is stable and can be used if needed.AtomInstance#_createdAt
removed with no replacement. This was adding some unnecessary overhead and cruft to all atom instances. I've yet to use this property for debugging or anything. It can theoretically be useful though. Adding it back is already possible via plugins if needed.Removed All
prevReasons
PropertiesprevReasons
had no internal use and was really just for user benefit, to see why an atom or selector evaluated last time it did. However, we found we never used it and it actually causes a "memory leak" of sorts:The
prevReasons
would hold onto the previous state of the node. If the node had giant state (we had one with 27MB of data), it could really add up. It isn't a real memory leak since that memory is cleaned up the next time the node evaluates (or if it's destroyed), but it's still some very unnecessary cruft.So we removed prevReasons from selectors and atom instances. It's still possible to track previous evaluation reasons yourself using an ecosystem plugin on the
stateChanged
mod if you do need this functionality.Plugin Action Changes
Not documenting these here as Zedux v2 is reworking plugins entirely, but this PR modifies several plugin action payload types to use
GraphNode
s, which is more useful than the previousAnyAtomInstance | SelectorCache
unions.getInternals
ChangesNobody should be touching Zedux's internals (returned from the exported
getInternals
function). Its entire structure changed and will probably change again before v2. Comment if you need any details.Deprecations
Zedux v2 will deprecate several things. This PR only deprecates one:
AtomInstance#getState
deprecated in favor of the newGraphNode#get
(whichAtomInstance
inherits).TypeScript
AtomGenericsPartial
replaced with the improvedAnyAtomGenerics
type helper (see below section on TypeScript in the list of new features)New Features
This refactor also naturally added some new features, mostly as part of GraphNodes being standardized.
Ecosystem Features
ecosystem.dehydrate
andecosystem.findAll
now accept similarexclude
/include
filters. You can nowecosystem.dehydrate('mySearchKey')
! I already use this a lot.ecosystem.live
atom getters. Zedux now more closely mimics (other) signals implementations. Callecosystem.live.get(myTemplate)
in any reactive context to automatically register graph dependencies.ecosystem.hash
returns the fully qualified id for the passed selector+params combo. The plan is to make this replace_idGenerator.hashParams
, supporting all types of nodes, not just selectors.The New Atom Getters
In Zedux v1, atom getters have this shape:
While we won't remove any of that in v2, we will deprecate both
getInstance
andselect
. The plan is that Zedux v3 will have this atom getters shape:getInstance
is replaced with the newgetNode
atom getterselect
is replaced byget
! :tada:. This will make refactoring between atoms and selectors much easier.The plan is to also deprecate
injectAtomSelector
anduseAtomSelector
in favor ofinjectAtomValue
/useAtomValue
(or theget
atom getter of course).This PR only introduces the new
getNode
atom getter to the Ecosystem class. It is not formalized yet and nothing is deprecated yet, I'll do that in a follow-up PR. We had to add it now since the Selectors class was removed and we needed some way to get a selector instance.The
GraphNode
classBoth
AtomInstance
and the newSelectorInstance
class extend this class.GraphNode
implements theJob
interface, meaning all graph nodes are valid scheduler jobs.All graph nodes have the following non-obfuscated properties:
destroy
. This takes an optionalforce
parameter, meaningAtomInstance#destroy
retains its exact signature and functionality, butSelectorInstance
s get it too! You can nowecosystem.getNode(mySelector).destroy(true)
to force-destroy a cached selector.get
. This is replacingSelectorCache#result
andAtomInstance#getState
as the new standard to get a node's state. We're considering making it reactive by default. Not all nodes have to have state - in fact the current plan is that v2 will introduce signals-style effects that are stateless graph nodes.id
. The unique id of the graph node.on
. This is replacingecosystem.selectors.addDependent
andAtomInstance#addDependent
as a much more streamlined, optionally event-based way to add dependencies to a node. Pass a function as the first param to be notified on all events. Or pass an event name (currently "Updated" or "Destroyed" but those are changing before v2) to only be notified on those events. Zedux v2 will also add a similar property to ecosystems which will replace the current verbose plugin system.params
. A reference to the params used to instantiate the node. These will be passed to the selector or atom state factory every time it runs.template
. A reference to the template that this node is an instance of.The new
SelectorInstance
class adds nothing user-facing on top of the baseGraphNode
class. That's really the purpose of a selector - to be the most lightweight node possible.TypeScript
This PR introduces some wildly useful TypeScript improvements.
AtomGenerics
now haveNode
andTemplate
attached. This means, among other things, that atom instances of custom atom templates will have type safe access to all their custom class's properties.atom
andion
factories properly apply theNode
andTemplate
generics recursively, fixing lots of ugly type errors abouttemplate._createInstance
andinstance.template
types not matching.GraphNode
class acceptsAtomGenerics
, meaningSelectorInstance
s can now be passed to Zedux'sAtomStateType
,AtomParamsType
, andAtomTemplateType
type helpers to get the type of the relevant properties.NodeOf
type helper can pull theSelectorInstance
type from a selector function or selector config object.AnyAtomGenerics
type helper has been expanded to work similarly toAnyAtomTemplate
andAnyAtomInstance
- pass in any generics and the rest will default toany
.SelectorGenerics
type helper, useful when creating functions that acceptSelectorInstance
s (not selector templates - those are functions and can't be inferred by a type map like Zedux'sAtomGenerics
/SelectorGenerics
).DehydrationFilter
/DehydrationOptions
andNodeFilter
/NodeFilterOptions
for the standardized include/exclude filter types.ParamlessTemplate
type helper has been expanded for both selectors and selector templates.any
if not passed or inferred, though everything in Zedux is designed to infer well.On that last note, we are considering deprecating all
Atom*Type
type helpers in v2 in favor of*Of
(e.g.AtomStateType
->StateOf
). Besides being more succinct, this is a pattern that I think is catching on in the TS community, so we want to stick to standards.Summary
This PR is bigger than it needs to be, but really only by a little and it gets us well on our way to Zedux v2. I'll PR this against master, but don't merge it until the
next/v1.3.x
branch is merged into master, master is ready to be moved into v2 prep phase, and thev1.x
support branch is cut.This PR leaves some loose ends. I'll sum them all up in the Zedux v2 roadmap.