Ecosystem
The ecosystem is an isolated atom environment. Every ecosystem creates:
- A scheduler for intelligently running atom-related tasks.
- A graph that manages atom dependencies.
- A Selectors class instance for managing cached Atom Selectors.
- An id generator that generates unique ids for unnamed Atom Selectors and external graph nodes.
The Ecosystem class itself defines many methods for creating, destroying, and inspecting atom instances and the graph they form.
Ecosystems can be used completely outside of React. This can be helpful for testing atoms and selectors.
Creation
Create ecosystems with the createEcosystem()
factory.
import { createEcosystem } from '@zedux/react'
const rootEcosystem = createEcosystem({ id: 'root' })
Ecosystems are also created automatically when using an <EcosystemProvider>
without passing an ecosystem
prop:
import { EcosystemProvider } from '@zedux/react'
function App() {
return (
<EcosystemProvider id="root">
<Routes />
</EcosystemProvider>
)
}
Additionally, the global ecosystem will be created automatically if atoms are used in React outside any <EcosystemProvider>
.
Providing
Ecosystems can take control of all atom usages in a React component tree by wrapping the tree in <EcosystemProvider>
.
function App() {
return (
<EcosystemProvider ecosystem={rootEcosystem}>
<Routes />
</EcosystemProvider>
)
}
Modifying Overrides
The ability to swap out atom implementations on the fly is one of Zedux's superpowers. Use .addOverrides
, .removeOverrides
, or .setOverrides
.
Properties
All properties are readonly!
An object. This is set to the atomDefaults
value you passed via EcosystemConfig when creating this ecosystem.
This object currently has a single optional property - ttl
. This will be used as the ttl for all atom instances in the ecosystem that don't specify their own.
If not set, atomDefaults.ttl
defaults to -1
, which means all atom instances in the ecosystem will live forever by default.
A boolean. May be undefined. This is set to the complexParams
value you passed via EcosystemConfig when creating this ecosystem.
Whether to allow non-serializable values as atom and selector params. See the complex params guide.
An object. May be undefined. A reference to the `context` object passed tothe createEcosystem()
factory(if any) or the latest .reset
call. This object should be constant until the ecosystem is reset via.reset()
.
When .reset()
is called, the previous context (if any) will be passed as the second parameter to the onReady
function as part of the reset.
A boolean. May be undefined. This is set to the destroyOnUnmount
value you passed via EcosystemConfig when creating this ecosystem.
This defaults to false
for ecosystems created via createEcosystem()
and to true
for ecosystems automatically created when rendering an <EcosystemProvider>
without passing an ecosystem
prop.
Whether to destroy this ecosystem when the last <EcosystemProvider>
providing it unmounts.
A string. The id of this ecosystem as set via EcosystemConfig when creating this ecosystem. The string '@@global'
is reserved for the global ecosystem.
If you didn't specify an id when creating this ecosystem, this will be a randomly-generated string.
An array of strings. This is set to the flags
value you passed via EcosystemConfig when creating this ecosystem.
These work in conjunction with atom flags to raise warnings when unsafe atom templates are not overridden in certain environments.
If an atom template is used that has a flag that is not present in this array, Zedux will log a warning.
Flag checking is off by default - simply don't pass a flags array to createEcosystem()
and Zedux will ignore all flags. To turn it on, but with no flags, pass an empty array.
createEcosystem() // flag checking disabled. Zedux will ignore all atom flags.
createEcosystem({ flags: [] }) // flag checking enabled! All flags will log warnings
createEcosystem({ flags: ['a'] }) // all atom flags except 'a' will log warnings
Which atoms, which flags, and which environments, is all up to you. You may want to flag atoms that run side effects you don't want to run in tests. Or you may want to flag atoms that use APIs that only work in the browser or in electron or any other environment.
An object. May be undefined. The shallowly merged result of all calls to ecosystem.hydrate()
minus any keys that have been consumed already.
A Zedux Store. This store has no state. It serves only as a message bus for notifying plugins of mod events.
See the plugins guide.
A function. May be undefined. This is set to the onReady
value you passed via EcosystemConfig when creating this ecosystem.
Will be called as soon as the ecosystem has initialized. Is also called every time the ecosystem is reset.
This is the ideal place to bootstrap data and preload atoms. Since this function is called on reset, it can be used to ensure the ecosystem's "necessary data" is always loaded.
Signature:
(ecosystem, prevContext?) => maybeCleanup
A reference to the previous context value of the ecosystem. ecosystem.reset()
can be optionally given a new context object. If that happens, the ecosystem's context will be updated before this function is called. So a reference to the old context is passed here.
This parameter will be undefined the first time onReady
runs. Thus you can use this to check if this is the initial run.
const ecosystem = createEcosystem({
context: { redux: reduxStore },
onReady: (ecosystem, prevContext) => {
if (!prevContext) {
// this is the initial run
} else {
// onReady is running after an ecosystem reset
const nextContext = ecosystem.context
if (prevContext.redux !== nextContext.redux) {
// ecosystem.reset() changed the redux store reference
}
}
}
})
ecosystem.reset() // doesn't change context (prevContext === ecosystem.context)
ecosystem.reset({ redux: otherReduxStore }) // replaces context
Note that replacing context is an all-or-nothing deal. Spread ecosystem.context
into a new object to update only part of the context:
ecosystem.reset({ ...ecosystem.context, specialField: 'new val' })
undefined
or a cleanup function that will be called when the ecosystem is reset or destroyed.An object mapping atom keys to atom templates. These are the currently-overridden atoms in this ecosystem. Modify this list by calling ecosystem.setOverrides()
, ecosystem.addOverrides()
, and ecosystem.removeOverrides()
.
If an initial overrides
array is passed, they will be immediately mapped into this object.
The Selectors class instance that tracks all cached atom selectors in this ecosystem.
A boolean. Whether the ecosystem is being used on the server to generate SSR content.
This is set to the ssr
value you passed via EcosystemConfig when creating this ecosystem. If not specified, defaults to false.
Currently the only thing this affects is injectEffect()
- SSR mode prevents effects from running at all in this ecosystem.
Methods
Adds new overrides to the ecosystem's current list of overrides and/or updates existing overrides - swapping them out with different implementations. All existing instances that match atom templates in the passed list will be force-destroyed, allowing their dependents to recreate them.
Signature:
addOverrides = (overrides) => void
Accepts a callback and batches all updates that happen synchronously while the callback is running. Flushes all updates once the passed callback completes.
Signature:
batch = (callback) => void
See the batching guide.
Returns a snapshot of the current state of instances in the ecosystem.
Signature:
dehydrate = ({
exclude?,
excludeFlags?,
include?,
includeFlags?,
transform?
}) => snapshot
Optional. An array of atom templates and/or case-insensitive fuzzy-search strings.
Any atom instances of the passed atom templates or whose id matches any of the passed strings will be excluded from the snapshot.
Optional. An array of strings.
Any atom instance whose template's flags contain any of the passed strings will be excluded from the snapshot.
Optional. An array of atom templates and/or case-insensitive fuzzy-search strings.
Only atom instances of the passed atom templates or whose ids match any of the passed strings will be included in the snapshot.
Excludes take priority over includes - if an atom instance is excluded via excludes
or excludeFlags
and included here, it will still be excluded.
Optional. An array of strings.
Only atom instance whose template's flags contain any of the passed strings will be included in the snapshot.
Excludes take priority over includes - if an atom instance is excluded via excludes
or excludeFlags
and included here, it will still be excluded.
Optional. A boolean. Default: true
.
Whether to transform state by passing it to dehydrate
atom config options.
An object mapping atom instance ids to the transformed form (unless transform: false
) of their current state.
Destroys all atom instances in the ecosystem, cleans up the ecosystem itself (e.g. by removing all currently scheduled tasks), and removes it from the internal store.
Signature:
destroy = (force?) => void
Optional. A boolean. Default: false
.
By default, destruction will bail out if this ecosystem is currently being used in any <EcosystemProvider>s
. Pass true to force destruction anyway. It is not recommended to pass this in a normal app.
Works similar to ecosystem.getInstance()
except it does NOT create the atom instance if it doesn't exist. In that case, .find()
returns undefined.
// creates the atom instance if needed:
instance = ecosystem.getInstance(myAtom, [...params])
// never creates an atom instance:
instance = ecosystem.find(myAtom, [...params])
.find()
can also take a fuzzy search string:
instance = ecosystem.find('myAtomKey')
In this case, .find()
returns the first atom instance it finds whose id contains the passed string (case-insensitive), unless an exact match is found, in which case the exact-matching instance will be returned.
Get all atom instances in the ecosystem.
Signature:
findAll = (atom?) => instances
.findAll()
returns only instances of the passed template. If a string is passed, .findAll()
returns only instances whose id includes the passed string (case-insensitive).Gets the current value of an atom instance's store. Creates the atom instance if it doesn't exist yet. See the get
AtomGetter.
const currentValue = myEcosystem.get(myAtom)
const withParams = myEcosystem.get(paramsAtom, ['param 1', 'param 2'])
Note that this method - like all AtomGetters on the ecosystem - never registers graph dependencies, no matter where it's used.
Gets an atom instance. Creates it if it doesn't exist yet. Set the getInstance
AtomGetter.
const instance = rootEcosystem.getInstance(myAtom)
const withParams = rootEcosystem.getInstance(paramsAtom, ['param 1', 'param 2'])
instance.getState()
instance.setState('new state')
Note that this method - like all AtomGetters on the ecosystem - never registers graph dependencies, no matter where it's used.
Hydrates the state of atom instances in this ecosystem, usually using a previous state snapshot from ecosystem.dehydrate()
.
If .hydrate()
has been called before, the new hydration will be shallowly merged into the existing hydration.
Signature:
hydrate = (snapshot, config?) => void
Required. An object. The keys of this object are ids corresponding to atom instances that may or may not exist in the ecosystem yet.
Optional. An object containing the following optional property:
{ retroactive }
By default, Zedux will update the state of all existing atom instances that have an entry in the passed snapshot. To disable this, pass { retroactive: false }
.
Registers a plugin with this ecosystem. Does nothing if the plugin has already been registered. See the plugins guide.
Signature:
registerPlugin = (plugin) => void
Accepts a single ZeduxPlugin instance.
Removes overrides previously set via .addOverrides()
or .setOverrides()
. All existing instances of atom templates in the passed list will be force-destroyed, allowing their dependents to recreate them using the original, non-overridden atom template.
You can pass either the original template or the override. Zedux only looks at their key
properties. You can also pass strings matching atom template keys.
Signature:
removeOverrides = (overrides) => void
Wipes the ecosystem - force destroying all atom instances and cached selectors - calls the cleanup function returned from the onReady
function (if any), and calls onReady
again to reinitialize the ecosystem.
Note that this doesn't remove overrides or plugins but does remove hydrations. This is because you can remove overrides/plugins yourself if needed, but there isn't currently a way to remove hydrations.
Signature:
reset = (newContext?) => void
Takes an optional newContext
object that will be passed to onReady
and set as the new .context
value of the ecosystem.
const myEcosystem = createEcosystem({
context: { someField: 'some val' },
id: 'my',
onReady: (ecosystem, prevContext) =>
console.log('old context:', prevContext, 'new context:', ecosystem.context),
})
// old context: undefined new context: { someField: 'some val' }
myEcosystem.reset({ someField: 'new val' })
// old context: { someField: 'some val' } new context: { someField: 'new val' }
Gets the current value of a cached atom selector. If the atom selector hasn't been cached yet, runs the selector but does not cache the result. Use ecosystem.selectors.getCache()
to create and cache a selector. See the select
AtomGetter.
const selection = myEcosystem.select(myAtomSelector)
const withArgs = myEcosystem.select(myAtomSelector, 'param 1', 'param 2')
Note that this method - like all AtomGetters on the ecosystem - never registers graph dependencies, no matter where it's used.
Replaces the ecosystem's list of overridden atoms with the passed overrides. All instances of atom templates in either the new or old lists will be force-destroyed, allowing their dependencies to recreate them.
To selectively update only certain atoms, use .addOverrides
or .removeOverrides
Signature:
setOverrides = (newOverrides) => void
Accepts an array of atom templates. This will be set as the new .overrides
property.
Removes a plugin from the ecosystem. Calls the cleanup returned from the plugin's registerEcosystem
function (if any), and turns off any mods the plugin enabled in the ecosystem that are not also requested by any other plugins.
Signature:
unregisterPlugin = (plugin) => void
Accepts a single ZeduxPlugin instance. If the plugin hasn't previously been registered via .registerPlugin()
, it will be ignored.
See what the ecosystem's atom graph currently looks like. There are 3 ways to view the graph:
- Top-down
- Bottom-up
- Flat
Flat is the default and is the most useful. It returns a normalized object containing every node in the graph. Each node points to its dependencies and dependents in the top-level object.
Top-down and bottom-up are mostly just for fun, to quickly gain some insight into what your dependency graph actually looks like. However, these views don't show "pseudo-nodes" (nodes that Zedux creates to represent external dependents like React components).
Signature:
viewGraph = (view) => graph
'flat'
, 'top-down'
, or 'bottom-up'
. Default: 'flat'
.Returns a list of EvaluationReasons detailing why the current atom instance or selector is evaluating.
If called outside a selector or atom state factory, ecosystem.why()
always returns undefined.
If this is the first evaluation of the current atom instance or selector, ecosystem.why()
returns an empty array.