Skip to main content

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.

Live Sandbox
1234567891011121314151617
const one = atom('common-key', () => 'Numero Uno')
const two = atom('common-key', () => 'I am the best')
const three = atom('common-key', () => 'Two is not the best')

function Swapper() {
const ecosystem = useEcosystem()
const state = useAtomValue(one)

return (
<>
<div>Current State: {state}</div>
<button onClick={() => ecosystem.setOverrides([one])}>Use One</button>
<button onClick={() => ecosystem.setOverrides([two])}>Use Two</button>
<button onClick={() => ecosystem.setOverrides([three])}>Use Three</button>
</>
)
}

Properties

All properties are readonly!

atomDefaults

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.

complexParams

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.

context

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.

destroyOnUnmount

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.

id

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.

flags

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.

hydration

An object. May be undefined. The shallowly merged result of all calls to ecosystem.hydrate() minus any keys that have been consumed already.

modBus

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.

onReady

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
ecosystem
A reference to this ecosystem
prevContext

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' })
Returns
Either undefined or a cleanup function that will be called when the ecosystem is reset or destroyed.
overrides

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.

selectors

The Selectors class instance that tracks all cached atom selectors in this ecosystem.

ssr

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

addOverrides

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
overrides
Required. An array of atom templates.
batch

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.

dehydrate

Returns a snapshot of the current state of instances in the ecosystem.

Signature:

dehydrate = ({
exclude?,
excludeFlags?,
include?,
includeFlags?,
transform?
}) => snapshot
exclude

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.

excludeFlags

Optional. An array of strings.

Any atom instance whose template's flags contain any of the passed strings will be excluded from the snapshot.

include

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.

includeFlags

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.

transform

Optional. A boolean. Default: true.

Whether to transform state by passing it to dehydrate atom config options.

Returns

An object mapping atom instance ids to the transformed form (unless transform: false) of their current state.

destroy

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
force

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.

find

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.

findAll

Get all atom instances in the ecosystem.

Signature:

findAll = (atom?) => instances
atom
An atom template or fuzzy search string. If an atom template is passed, .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).
Returns
An object of all Active and Stale atom instances in the ecosystem, keyed by the instance's id.
get

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.

getInstance

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.

hydrate

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
snapshot

Required. An object. The keys of this object are ids corresponding to atom instances that may or may not exist in the ecosystem yet.

config

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 }.

registerPlugin

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.

removeOverrides

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
overrides
Required. An array of atom templates and/or template key strings. If any haven't been set as overrides previously, they'll be ignored.
reset

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' }
select

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.

setOverrides

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.

unregisterPlugin

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.

viewGraph

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
view
Optional. One of 'flat', 'top-down', or 'bottom-up'. Default: 'flat'.
Returns
An object whose structure depends on the requested view. See the graph walkthrough.
why

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.

See Also