Custom Injectors
If you use React, you've probably written custom hooks. These are functions whose names start with use
and that compose other hooks.
Zedux injectors have the same composability. Custom injectors are just functions whose names start with inject
and that compose other injectors.
How to create, compose, and use custom injectors
Rules
Custom injectors can do almost anything you want. They're Just JavaScript. Just remember that the function name should start with the word "inject" - e.g. injectUserStream
or injectFetchUser
.
An injector will also always call other injectors - if it doesn't, it isn't an injector!
An Example
The quick start referenced a theoretical custom injectFetch
injector. Let's create it for real:
const injectFetch = (url: string) => {
const store = injectStore({ status: 'loading' })
injectEffect(async () => {
try {
const result = await fetch(url)
const data = await result.json()
store.setState({ data, status: 'success' })
} catch (err) {
store.setState({ error: err, status: 'error' })
}
}, [url])
return api(store).setPromise(promise)
}
Now we can use this injector to fetch data in any other atom:
const usersAtom = atom('users', () => {
const { store } = injectFetch('/api/users')
return store
})
const friendsAtom = atom('friends', () => {
return injectFetch('/api/friends')
})
Notice that usersAtom
returns only the store, while friendsAtom
returns the entire Atom API. Let's take a closer look at this flexibility afforded by Atom APIs:
Using Atom APIs
Atom instances have 3 "meta data types" that you'll find yourself working with a lot in your state factories and custom injectors:
- State (usually inside stores)
- Exports
- Promises
The Atom API is a standard way to pass these 3 things around.
// This Atom API is essentially a container for the 3 meta data types:
const myApi = api(myStore).setExports(myExports).setPromise(myPromise)
// we can access all 3 easily:
const { exports, promise, store } = myApi
Atom APIs are designed to be passed around between injectors and ultimately composed together into a single Atom API that gets returned from your state factory. You can of course pass stuff around however you want, but the Atom API is a useful standard for this.
injectPromise()
itself uses this pattern - returning an Atom API with a store and promise attached.
injectSelf()
Zedux's built-in injectors (injectMemo
, injectAtomInstance
, etc) have access to the currently-evaluating atom instance before it's fully initialized. When accessed during initial evaluation, the atom instance doesn't have its exports
, promise
, or store
properties set yet - that's the purpose of the initial evaluation!
With that in mind, Zedux exports an injector that allows you to access the not-yet-initialized instance - injectSelf()
:
import { atom, injectSelf } from '@zedux/react'
const exampleAtom = atom('example', () => {
const partialInstance = injectSelf()
partialInstance.store.setState() // ERROR! Store not set yet
partialInstance.promise.then(...) // ERROR! Promise not set yet
partialInstance.exports.myExportedFn() // ERROR! Exports not set yet
...
})
On subsequent evaluations, those properties will exist. It is safe to use them after a check that they exist:
const exampleAtom = atom('example', () => {
const partialInstance = injectSelf()
if (partialInstance.store) {
partialInstance.store.getState()
}
...
})
TypeScript users will see that injectSelf()
returns a PartialAtomInstance | AnyAtomInstance
type. Type casts to improve this are safe enough after a type guard like in the above example. To get full type support, type-cast the value using the AtomInstanceType
TS helper:
if ((partialInstance as AtomInstanceType<typeof exampleAtom>).store) {
;(partialInstance as AtomInstanceType<typeof exampleAtom>).store.setState()
}
All other AtomInstance properties and methods are free game. For example, injectSelf()
is often used to get the fully-qualified id of the current atom instance:
const exampleAtom = atom('example', (param1: string, param2: number) => {
const { id } = injectSelf()
return id
})
function ExampleComponent() {
const value = useAtomValue(exampleAtom, ['a', 1])
value // 'example-["a",1]'
...
}
Watch out for reevaluation loops when calling instance.invalidate()
- make sure you gate it behind if
statements or callback functions.
Recap
- Custom injectors can encapsulate any logic you want
- Atom APIs are a convenient standard for passing stores, exports, and promises around
- Use
injectSelf()
to get a reference to the currently-evaluating atom instance.
Next Steps
Custom injectors have many uses. Let's look at using them to reset atoms.