Skip to main content

Atom Getters

This walkthrough has introduced several hooks and injectors that let you manipulate the dependency graph. These are easy enough. But it gets even easier.

you will learn:

How to use Atom Getters to dynamically and efficiently update the graph.

The Atom Getters

For your convenience when working with atoms, Zedux provides a single, uniform object structure in many places. Objects with this shape are called Atom Getters objects. Every Atom Getters object has the following 3 "Atom Getters":

  • get
  • getInstance
  • select

Most Atom Getters objects also have a fourth property:

  • ecosystem - a reference to the current ecosystem.

Atom Getters behave differently depending on how and when they're used. Mastering these hows and whens will make Zedux an extremely efficient tool for you.

Let's take a quick look at each Atom Getter.

get

Returns the current value of an atom instance. Creates the instance using the passed template if it doesn't exist.

const value = get(myAtom)
// or, if the atom takes params:
get(myAtom, ['param 1', 'param 2'])

getInstance

Returns an atom instance. Creates the instance using the passed template if it doesn't exist.

const instance = getInstance(myAtom)
// or, if the atom takes params:
getInstance(myAtom, ['param 1', 'param 2'])

select

Runs an Atom Selector (we'll learn about these in the selectors walkthrough).

const selectorResult = select(myAtomSelector)
// or, if the Atom Selector takes params:
select(myAtomSelector, 'param 1', 'param 2') // note params isn't an array here

Atom Getter Habitats

Where exactly might one encounter Atom Getters in the wild?

Ecosystems

That's right, every ecosystem is an Atom Getters object.

const ecosystem = useEcosystem()

// get
const instanceValue = ecosystem.get(myAtom, ['param 1', 'param 2'])

// getInstance
const instance = ecosystem.getInstance(myAtom, ['param 1', 'param 2'])

// select
const selectorResult = ecosystem.select(myAtomSelector)

Ecosystems are the only Atom Getters objects that don't have an ecosystem field (they don't need it of course!). The ecosystem's Atom Getters are also unique in that they never register graph dependencies. More on this shortly.

injectAtomGetters

You can inject an Atom Getters object in any atom using injectAtomGetters():

const exampleAtom = atom('example', () => {
const { ecosystem, get, getInstance, select } = injectAtomGetters()
...
})

Atom Selectors

The first argument passed to Atom Selectors is an Atom Getters object:

function myAtomSelector({ ecosystem, get, getInstance, select }: AtomGetters) {
...
}

(More on these in the selectors walkthrough).

Ions

The first argument passed to the state factory of Ions is an Atom Getters object:

const exampleIon = ion(
'example',
({ ecosystem, get, getInstance, select }) => {}
)

(We'll learn about these in the selectors walkthrough too).

Registering Dependencies

When get, getInstance, and select are called synchronously during atom or selector evaluation, they register graph dependencies.

  • get registers dynamic dependencies.
  • getInstance registers static dependencies.
  • select registers a dynamic dependency on the selector itself, which in turn registers its own dynamic and static deps.

This graph automation only happens during evaluation. When called after evaluation ends (e.g. in an effect or exported callback), Atom Getters never register graph dependencies.

The Atom Getters on the ecosystem also never register graph dependencies. You can use this to purposefully prevent dependencies from being registered:

// toggling this atom's value changes maybeDynamicAtom's dynamicity!
const subscribeAtom = atom('subscribe', true)

const maybeDynamicAtom = ion('maybeDynamic', ({ ecosystem, get, select }) => {
const subscribe = get(subscribeAtom)

const val = subscribe
? select(myAtomSelector) // dynamic
: ecosystem.select(myAtomSelector) // static
})

Static Evaluation

Since the ecosystem's Atom Getters never register graph dependencies, they can be used to statically create, evaluate, and analyze atom instances and Atom Selectors outside React (i.e. anywhere).

// create an atom instance outside React or atoms or anything:
const instance = ecosystem.getInstance(myAtom)

// run a selector anywhere:
const val = ecosystem.select(myAtomSelector)
tip

The ecosystem's Atom Getters make it easy to test atoms and Atom Selectors!

Cheatsheet

Here's a Little Ultimate Atom Getters Cheatsheet of Which Atom Getters Register Dependencies When:

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

// registers a dynamic graph dependency:
get(myAtom) // essentially an alias of injectAtomValue

// registers a static graph dependency:
getInstance(myOtherAtom) // essentially an alias of injectAtomInstance

// registers a dynamic dependency on myAtomSelector's result:
select(myAtomSelector) // essentially an alias of injectAtomSelector

// doesn't update the graph; just returns the current value:
ecosystem.get(myAtom)

// doesn't update the graph; just returns the current atom instance:
ecosystem.getInstance(myOtherAtom)

// doesn't update the graph; just returns the selector result:
ecosystem.select(myAtomSelector)

// effects always run later (after evaluation)
injectEffect(() => {
// doesn't update the graph; essentially an alias of ecosystem.get
get(myAtom)

// doesn't update the graph; essentially an alias of ecosystem.getInstance
getInstance(myOtherAtom)

// doesn't update the graph; essentially an alias of ecosystem.select
select(myAtomSelector)
}, []) // AtomGetters are stable references - no need to pass them here

// callbacks are usually called after evaluation, but *it depends!*
const myCallback = injectCallback(() => {
get(myAtom)
getInstance(myOtherAtom)
select(myAtomSelector)
}, [])

// calling the callback here makes all its Atom Getter calls register deps:
myCallback()

return api().setExports({
myCallback, // dependents that call this callback will *not* register deps
})
})
tip

Use ecosystem.get, ecosystem.getInstance, and ecosystem.select to purposefully avoid creating dependencies during evaluation.

Buffered Updates

While an atom instance or Atom Selector is evaluating, Zedux buffers graph updates caused by Atom Getters and flushes the buffer once evaluation is over. This has a few implications:

  1. It means that you won't actually see graph updates happen immediately:
// bad:
const exampleAtom = atom('example', () => {
const { ecosystem, get } = injectAtomGetters()

const otherVal = get(myOtherAtom) // create a dep

ecosystem.viewGraph() // the dep won't be there (it doesn't exist yet!)
})

// good:
const exampleAtom = atom('example', () => {
const { ecosystem, get } = injectAtomGetters()

const otherVal = get(myOtherAtom) // create a dep

injectEffect(() => {
ecosystem.viewGraph() // effects are deferred; the dep exists now!
}, [])
})
  1. It allows Zedux to be highly efficient with its graph updates:
const exampleAtom = atom('example', () => {
const { get, getInstance } = injectAtomGetters()

// registers a dynamic graph dependency on myAtom:
const someField = get(myAtom).someField

// doesn't register anything! (We've already registered this dep):
const anotherField = get(myAtom).anotherField

// also registers nothing (can't downgrade an edge from dynamic to static):
const instance = getInstance(myAtom)
})

Conditional Injectors

Remember how injectors are like hooks? As such, injectAtomValue, injectAtomInstance, etc. can't be used in if statements or loops. Atom Getters, however, are not injectors. This means they can be used conditionally!

Live Sandbox
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
// Let's do a bigger example. This is a good opportunity to review what
// we've covered in the walkthrough so far!
const counterAtom = atom('counter', (id: number) => 0)

const counterIdsAtom = atom('counterIds', () => {
const store = injectStore([0, 1])

const addCounter = () => store.setState(state => [...state, state.length])

return api(store).setExports({ addCounter })
})

const countersSumAtom = atom('countersSum', () => {
const { get, getInstance } = injectAtomGetters()
const counterIds = get(counterIdsAtom)

let sum = 0
for (let counterId of counterIds) {
sum += get(counterAtom, [counterId]) // loops are fine!
}

return sum
})

function Counter({ id }: { id: number }) {
const counterInstance = useAtomInstance(counterAtom, [id])
const [state, setState] = useAtomState(counterInstance) // dynamicize the dep!

return (
<div>
<span>
Counter #{counterInstance.params[0]}: {state}{' '}
</span>
<button onClick={() => setState(state => state + 1)}>Increment</button>
<button onClick={() => setState(state => state - 1)}>Decrement</button>
</div>
)
}

function CounterList() {
const sum = useAtomValue(countersSumAtom)
const counterIdsInstance = useAtomInstance(counterIdsAtom)
const [ids, setIds] = useAtomState(counterIdsInstance) // dynamicize the dep!
const { addCounter } = counterIdsInstance.exports

return (
<>
{ids.map(id => (
<Counter id={id} key={id} />
))}
<div>Sum: {sum}</div>
<button onClick={addCounter}>Add Counter</button>
</>
)
}

Weak Getters

Just like hooks and injectors, the get and getInstance Atom Getters create the atom instance if it doesn't exist yet. This is almost always what you want. But in rare cases, you may want to see if the atom instance exists before creating it. Or you may not need the atom instance at all if it hasn't been created somewhere else.

For these use cases, ecosystems (and only ecosystems) have 2 "Weak Getters":

find

Returns an atom instance if the atom instance exists. Otherwise returns undefined.

const instance = ecosystem.find(myAtom)
const paramsExample = ecosystem.find(myAtom, ['param 1', 'param 2'])
const fuzzySearchString = ecosystem.find('myAtomKey')

When a string is passed, ecosystem.find() returns the first atom instance it finds whose id contains the passed string (case-insensitive).

findAll

Returns an object containing all instances of the passed atom, keyed by each instance's id.

const instances = ecosystem.findAll(myAtom)
const fuzzySearchString = ecosystem.findAll('myAtomKey')

When a string is passed, ecosystem.findAll() returns all atom instances whose id contains the passed string (case-insensitive).

tip

This can make finding your atoms a breeze. For example, if you're creating a dashboard component, give your atoms keys like dashboardFilter, dashboardSelection then use ecosystem.findAll('dashboard') to find all the atom instances you're working with.

Pass nothing to ecosystem.findAll() to get every atom instance in the ecosystem.

const allAtomInstances = ecosystem.findAll()

Recap

  • get returns an atom instance's value. It registers dynamic graph dependencies when called synchronously during evaluation.
  • getInstance returns an atom instance. It registers static graph dependencies when called synchronously during evaluation.
  • select returns an Atom Selector's result. It registers dependencies dynamically depending on the Atom Selector.
  • Zedux passes an Atom Getters object to Atom Selectors and Ions. Or get them manually with injectAtomGetters(). Every ecosystem is also an Atom Getters object.
  • The ecosystem's Atom Getters never register graph dependencies - and thus are your static analysis and testing friends.
  • When called after evaluation, Atom Getters do not register graph dependencies.
  • Atom Getters can be safely used in loops and conditional statements.
  • The Weak Getters (find and findAll) never create atom instances.

Next Steps

Alright, alright, we keep hearing about these Atom Selector things. Let's learn all about them.