Atom APIs
The quick start showed that injectors are like hooks for atoms. But some operations don't fit well into the injector paradigm. Injectors, like hooks, should be composable and usable multiple times throughout an atom state factory and other injectors.
But atoms have a few one-off operations (operations that should only be performed once). For these, we use an Atom API.
How to create an Atom API and use it to:
- Export anything from an atom
- Kick off React suspense
- Make Zedux control promise state
api()
To create an Atom API, simply wrap state in a call to api()
before returning it from a state factory:
import { api, atom } from '@zedux/react'
const exampleAtom = atom('example', () => 'initial state') // before
const exampleAtom = atom('example', () => api('initial state')) // after
This works with stores too:
const exampleAtom = atom('example', () => {
const store = injectStore()
return store // before
return api(store) // after
})
You can create an Atom API anywhere, but only the Atom API returned from the state factory will become the API of the atom.
So now then ... what can an Atom API do?
Exports
Atoms can export anything via a returned Atom API:
Atom APIs have a .setExports()
method that defines an atom instance's exports (when the atom API is returned from the state factory). These exports are set on initial evaluation and ignored on evaluations after that.
The exports are set on instance.exports
. Wherever you have an atom instance, you have access to its exports.
Exports must be an object and can contain anything. As you can imagine, exports are another big feature that sets Zedux apart from other atomic libraries.
Exports are constant! This means exported variables should be stable references. Zedux won't update them on subsequent evaluations. In this example, increment
and decrement
are not stable references, but they don't reference anything except the store, which is stable. You have to be conscious of this.
Think of atom exports just like module exports in JavaScript files. You never change a file's exports. There's no need to! You can, however, mutate objects and change state in the file using exported functions. That's exactly what you can do in atoms with injected refs and stores.
Exports are extremely powerful. Exposing an atom's "public API" like this is just one of their uses.
Exporting Refs
Since you can export anything, refs can be used to great effect:
const gridAtom = atom('grid', () => {
const gridApiRef = injectRef()
// this atom provides universal access to the grid api once it's been set:
return api().setExports({ gridApiRef })
})
function Grid() {
const { gridApiRef } = useAtomInstance(gridAtom).exports
// expose this grid component's api to the atom universe!
return <ThirdPartyGridLibrary onLoad={api => (gridApiRef.current = api)} />
}
injectRef()
is the injector equivalent of useRef()
. Exported refs can be used exactly as React refs!
Exporting Callbacks
Another use is to export callbacks for implementing render-as-you-fetch patterns:
injectCallback
is the injector equivalent of useCallback
. We used it here with empty deps ([]
) to remind us that this is an export and its reference can't change. This means we can't use any dynamic values inside the callback - only stable references. injectCallback
also automatically batches all state updates in the callback.
This is poor man's async handling. Let's upgrade to Suspense.
Suspense
Atoms can be used as suspending resources in React. To suspend, simply pass a promise to a returned Atom API's .setPromise()
method:
return api(val).setPromise(suspensePromise)
An atom that returns such an api will cause React to suspend until the promise resolves.
Unlike exports, the promise can change! Be sure to return a stable reference if you don't want the atom instance to kick off suspense again on reevaluations.
We'll cover much more about suspense in the suspense walkthrough.
Query Atoms
You can also pass a promise directly to api()
. This tells Zedux to take control over the atom's state and update it based on the promise. The state shape is similar to React Query queries. Because of this, we call such atoms "query atoms":
api()
?In Zedux, atoms always evaluate synchronously. This allows Zedux's DI to be as dynamic as possible and simplifies a lot for you. It also means that state factories can't be asynchronous functions.
Since returning a promise and declaring an async function are essentially the same thing (especially in Typescript's eyes), we found it better to disallow returning a promise directly. Just wrap your promise in api(promise)
.
Query atoms get their own walkthrough.
Properties
The object returned from api()
is an instance of the AtomApi
class. It has a few readonly properties that reference the values you set on it:
.value
- The value passed toapi()
..exports
- The exports set viaapi().setExports()
andapi().addExports()
..promise
- The promise set viaapi().setPromise()
..store
- The store passed toapi()
. If a non-store value was passed, this isundefined
.
const myApi = api(myStore).setExports(myExports).setPromise(myPromise)
myApi.value === myStore
myApi.exports === myExports
myApi.promise === myPromise
myApi.store === myStore
Atom API Composition
An Atom API can be cloned by passing it to api()
.
import { api } from '@zedux/react'
const myApi = api('some state').setExports({ something })
const clonedApi = api(myApi) // has the same value and exports
Atom APIs are a convenient way to pass stores, exports, and promises around. You may find yourself working with multiple Atom APIs when using custom injectors.
Atom APIs have a .addExports()
method specifically for merging exports together from multiple sources:
const complexAtom = atom('complex', () => {
// say these two injectors return AtomApis:
const apiA = injectA()
const apiB = injectB()
// say we want to expose apiA's state (or store) as the state of this atom,
// but we want the exports from both APIs:
return api(apiA).addExports(apiB.exports) // merge apiB's exports into apiA's
})
If you have multiple Atom APIs with stores attached, you can also use store composition to create a new Atom API from the injected APIs:
const apiA = injectA()
const apiB = injectB()
const composedStore = injectStore()
composedStore.use({ a: apiA.store, b: apiB.store })
return api(composedStore).setExports({
...apiA.exports,
...apiB.exports,
})
Promises
Unlike Recoil, Zedux doesn't provide any promise utilities like waitForAll
/waitForNone
. This is because Zedux relinquishes all control over async flows to you, the programmer. This means you can use native Promise APIs or any other async tool (read RxJS) to control async flows.
Let's look at an example using Promise.all()
to "compose" promises together from multiple sources:
Click "Reset" in the above sandbox to see the promises cascade.
Recap
- Create an Atom API with
api(stateOrStore)
. Return an Atom API from a state factory to make it the API of the atom. - Use
.setExports()
to "export" an object to the instance's.exports
property. - Make atoms trigger React suspense with
.setPromise(myPromise)
. - Make Zedux control promise state by returning
api(myPromise)
. - Clone Atom APIs with
api(myApi)
.
Next Steps
You've now unlocked some of the stronger powers of Zedux atoms. It's time to zoom out and look at the bigger picture.