Skip to main content

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.

you will learn

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.

tip

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]'
...
}
tip

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.