Skip to main content

Ecosystems

All atoms live in an atom ecosystem. An ecosystem is like an isolated group of atoms. Atoms in an ecosystem can interact with each other, but are unaware of atoms in other ecosystems.

Ecosystems are a huge part of Zedux. We call Zedux a "molecular state engine for React". Ecosystems are the molecular part.

you will learn:
  • How to create and configure an ecosystem.
  • How to provide an ecosystem to your app.
  • What ecosystems look like

There are 2 types of ecosystems:

  • Global
  • Custom

Global

There is exactly one global atom ecosystem. You don't need to create it; it is created automatically if you don't create an ecosystem.

Zedux is designed to allow an app to use only the global ecosystem. And for small apps and simple examples, that's fine. However, this approach can only get you so far. The global ecosystem is not configurable. Thus it lacks some advanced features like dependency injection overrides and preloading. The global ecosystem can also be unruly in tests, since it requires manual cleanup.

Custom

The most common. You will probably want to create exactly one custom ecosystem in your app.

There are 2 ways to create custom ecosystems:

EcosystemProvider

The simplest way to create an ecosystem is by rendering an EcosystemProvider:

import { EcosystemProvider } from '@zedux/react'

function App() {
return (
<EcosystemProvider>
<Routes />
</EcosystemProvider>
)
}

This is similar to Recoil's RecoilRoot or Redux' Provider or React Query's QueryClientProvider. In fact, it's kind of a combination of all of those.

Rendering an EcosystemProvider without passing an ecosystem prop creates a new ecosystem. When such an EcosystemProvider is unmounted, the ecosystem and all atom instances in it are destroyed.

note

Nesting EcosystemProviders currently has no special functionality. Rendering an EcosystemProvider inside another EcosystemProvider will create a new, completely isolated ecosystem environment.

Most apps will want to use custom ecosystems.

createEcosystem()

For even more control, you can can create an ecosystem using the exported createEcosystem() factory.

import { EcosystemProvider, createEcosystem } from '@zedux/react'

const rootEcosystem = createEcosystem({ id: 'root' })

function App() {
return (
<EcosystemProvider ecosystem={rootEcosystem}>
<Routes />
</EcosystemProvider>
)
}

Ecosystems created this way can be controlled outside React. This means you can preload atoms, manipulate atoms, analyze the graph, and destroy the ecosystem without ever rendering a component. This is extremely useful for testing and for isomorphic codebases.

If you don't need to access the ecosystem outside React, it's recommended to create the ecosystem inside useMemo() at top of your component tree:

function App() {
const ecosystem = useMemo(() => createEcosystem({ id: 'root' }), [])

return (
<EcosystemProvider ecosystem={rootEcosystem}>
<Routes />
</EcosystemProvider>
)
}

Getting the Ecosystem

useEcosystem()

Every component below an <EcosystemProvider> in the tree can access the ecosystem via useEcosystem().

import { EcosystemProvider, useEcosystem } from '@zedux/react'

function Child() {
const ecosystem = useEcosystem()
...
}

function App() {
return (
<EcosystemProvider id="main">
<Child />
</EcosystemProvider>
)
}

If no ecosystem was provided above the current component, useEcosystem() returns the global ecosystem (creating it if it doesn't exist yet).

injectAtomGetters()

All atom instances created in an ecosystem can access the ecosystem via injectAtomGetters().

import { atom, injectAtomGetters } from '@zedux/react'

const exampleAtom = atom('example', () => {
const { ecosystem } = injectAtomGetters()
})

More on this hook in the Atom Getters walkthrough.

getEcosystem()

You can get an ecosystem completely outside React or atoms with the exported getEcosystem function:

import { getEcosystem } from '@zedux/react'

// get the ecosystem created via `createEcosystem({ id: 'root' })`
const ecosystem = getEcosystem('root')

Configuring Ecosystems

Ecosystems created manually can take configuration options. We'll touch on a few here. See the Ecosystem API for more info.

All config options are optional, but it's recommended to at least pass an id.

id

A string that uniquely identifies the ecosystem. The string '@@global' is reserved for the global ecosystem.

tip

This is a general rule with id/key strings used with Zedux - The @@ prefix is reserved for internal use.

return <EcosystemProvider id="root">...</EcosystemProvider>
// or
const ecosystem = createEcosystem({ id: 'root' })

context

An object containing absolutely anything. This can be used to supply some outside data to the atoms universe. For example, if you're migrating to Zedux from a different state management tool, you could pass a reference to the other library's state container (e.g. a Redux store).

This will be set as the .context property on the ecosystem. It's also passed as the 2nd param to the onReady function.

return <EcosystemProvider context={{ reduxStore }}>...</EcosystemProvider>
// or
const ecosystem = createEcosystem({ context: { reduxStore } })

ecosystem.context // { reduxStore }

Context is also useful when resetting the ecosystem. More on that in the resets walkthrough.

onReady

A function that will run immediately (when the ecosystem is created) and whenever the ecosystem is reset. Used to bootstrap the initial state of the app, kick off initial side effects, alleviate render waterfalls, set up state hydration, and other startup-y things.

const onReady = (ecosystem, context) => {
if (context.reduxStore) {
// preload a theoretical atom that hooks into a Redux store
ecosystem.getInstance(reduxAtom)
}
}

return <EcosystemProvider onReady={onReady}>...</EcosystemProvider>
// or
const ecosystem = createEcosystem({ onReady })

Methods & Properties

The ecosystem object itself has many useful methods. You can use these to inspect the dependency graph, get the current state of all atoms, set atom overrides, and a lot more. We'll get to most of these throughout the walkthrough. See the Ecosystem API doc for full details.

getInstance

Returns the atom instance for the given atom template + params combo. Creates the instance if it doesn't exist yet. This is the simplest way to preload atoms.

const rootEcosystem = createEcosystem({
id: 'root',
onReady: ecosystem => {
ecosystem.getInstance(myAtom)
ecosystem.getInstance(myAtom, ['with', 'params'])
},
})

findAll

Returns an object of all atom instances in the ecosystem, each instance keyed by a string that consists of its atom template's key + its serialized parameters.

Live Sandbox
123456789101112131415
const ecosystem = createEcosystem({ id: 'findAll-test' })
const testAtom = atom('test', (key?: number) => key)

ecosystem.getInstance(testAtom)
ecosystem.getInstance(testAtom, [1])

function Logger() {
const instances = ecosystem.findAll()

return (
<button onClick={() => console.log('Instances:', instances)}>
Log Instances
</button>
)
}

dehydrate

Returns an object of the current state of all atom instances in the ecosystem, each value keyed by a string that consists of the instance's atom template's key + its serialized parameters.

Pass transform: false to prevent any dehydrate atom config options from transforming atom instance values.

Live Sandbox
123456789101112131415
const ecosystem = createEcosystem({ id: 'dehydrate-test' })
const testAtom = atom('test', (key?: number) => key)

ecosystem.getInstance(testAtom)
ecosystem.getInstance(testAtom, [1])

function Logger() {
const instances = ecosystem.dehydrate({ transform: false })

return (
<button onClick={() => console.log('Instance Values:', instances)}>
Log Instance Values
</button>
)
}

More on this in the persistence guide.

context

A reference to the context object passed to createEcosystem or <EcosystemProvider> (if any).

import { atom, injectAtomGetters } from '@zedux/react'

const reduxAtom = atom('redux', () => {
const { reduxStore } = injectAtomGetters().ecosystem.context
})
note

When accessing ecosystem context like this, it can't be strongly typed for TS users :(. This is because atoms don't know what ecosystem they'll be used in.

You'll have to cast it (make sure to add type guards) or use a different approach like exporting a callback from an atom that you call in ecosystem.onReady:

const onReady = (ecosystem, context) => {
ecosystem.getInstance(reduxAtom).exports.setStore(context.reduxStore)
}

Global Only

It's possible to use nothing but the global ecosystem. This is done by never rendering an EcosystemProvider. If no EcosystemProvider is rendered, all atoms are added to the global ecosystem by default.

The global ecosystem can be nice for simplicity. Some apps don't need any of the features custom ecosystems offer. And that's fine!

Isolation Example

Ecosystems are completely isolated atom environments. The quick start showed that atom instances can be reused by passing the same params to certain hooks/injectors. Since atom instances live in an ecosystem, reuse can only happen inside that ecosystem.

In the following example, the counterAtom doesn't take any params. But since it's used in two different ecosystems, two different instances are created.

Live Sandbox
123456789101112131415161718192021222324252627
const counterAtom = atom('counter', 0)

function Counter() {
const [state, setState] = useAtomState(counterAtom)

return (
<>
<div>Value: {state}</div>
<button onClick={() => setState(state => state + 1)}>Increment</button>
</>
)
}

function Apps() {
return (
<>
<EcosystemProvider>
<h3>Counter 1:</h3>
<Counter />
</EcosystemProvider>
<EcosystemProvider>
<h3>Counter 2:</h3>
<Counter />
</EcosystemProvider>
</>
)
}

Recap

  • All atom instances live in an isolated atom ecosystem.
  • There is a default ("global") ecosystem, but you'll usually want to create your own.
  • Ecosystems can be configured with various options passed to the createEcosystem() factory or as props to <EcosystemProvider>.
  • The current ecosystem can be retrieved with useEcosystem() in components and injectAtomGetters() in atoms.

Next Steps

Ecosystems are capable of some pretty cool stuff. One of the coolest of these stuffs is the graph.