React Context
The atomic model naturally fixes all the problems with React context. With Zedux atoms, you can use React context with all of its benefits and none of its downsides.
The important philosophy here is that Zedux uses React context for Dependency Injection, not State Management. This article is an excellent breakdown of this technique.
- How to provide and consume atom instances over React context.
- How to control renders in both the providing and consuming component.
Providing
An atom instance can be provided over React context via <AtomProvider>
.
import { AtomProvider, useAtomInstance } from '@zedux/react'
function Parent() {
const instance = useAtomInstance(myAtom)
return (
<AtomProvider instance={instance}>
<Child />
</AtomProvider>
)
}
Multiple Instances
To provide instances of multiple atoms from the same component, you could nest a bunch of <AtomProvider>
s. But that isn't very aesthetically pleasing now, is it.
To this end, AtomProvider
accepts an instances
prop, whose value is an array of instances to provide. Only provide either an instance
or an instances
prop, not both.
return (
<AtomProvider instances={[instanceA, instanceB]}>{children}</AtomProvider>
)
Consuming
Consume provided instances with useAtomContext()
import { useAtomContext } from '@zedux/react'
function Child() {
const instance = useAtomContext(myAtom)
...
}
If an Instance Wasn't Provided
If a component uses useAtomContext()
but no instance was provided by any parent, useAtomContext()
returns undefined.
const instance = useAtomContext(myAtom)
instance.invalidate() // error! Cannot read properties of undefined
TypeScript users will be warned of this. But to get around it, you'd have to put checks before everything you try to do with that instance. Sounds like a fast-track to annoyance.
Fortunately (on purpose), useAtomContext()
has two overloads that help with this:
Case #1: I want a default atom instance to be created, if none was provided.
You can provide an array of params to useAtomContext()
. These params must match the params of the atom. If no atom instance was provided, Zedux will use the passed default params to locate an existing atom instance or create a new instance if it doesn't exist yet.
const instance = useAtomContext(myAtom, ['default instance params'])
instance.invalidate() // all good! Even TS is happy
If the atom doesn't take params, you must still pass an empty array for Zedux to find/create a default instance.
const paramlessInstance = useAtomContext(myAtom, [])
instance.invalidate() // 😊
Case #2: I don't ever want an instance to not be provided. Throw an error if I forget!
Instead of an array of default parameters, you can pass true
as the second param to useAtomContext()
. This tells Zedux to throw an error if no atom instance was provided.
const instance = useAtomContext(myAtom, true)
instance.invalidate() // all good again! TS smiles upon you
#failfast! This overload is recommended in almost every situation.
Subscribing
The amazing thing about using atoms for React context is that neither the providing nor consuming component subscribes to the atom instance by default. This gives you full control over rerenders.
Any component can set the state of the atom without subscribing to the state itself. And, of course, any component can subscribe itself to the atom instance using useAtomValue()
or similar.
function Parent() {
const instance = useAtomInstance(myAtom) // doesn't subscribe
const value = useAtomValue(instance) // subscribes
return (
<AtomProvider instance={instance}>
<Child />
</AtomProvider>
)
}
function Child() {
const instance = useAtomContext(myAtom, true) // doesn't subscribe
const value = useAtomValue(instance) // subscribes
...
}
Full live example:
Try changing the Parent
component above to be dynamic with useAtomValue(instance)
.
Using Selectors
A common pattern is to pass the provided atom instance to an Atom Getter inside useAtomSelector()
to limit component rerenders.
function Child() {
const instance = useAtomContext(myAtom, true) // doesn't subscribe
// subscribe, but only cause a rerender when `someField` changes:
const someField = useAtomSelector(({ get }) => get(instance).someField)
// or pass the instance as an argument:
useAtomSelector(mySelector, instance)
}
Cool But Why?
Ecosystems essentially make all atom instances "global" to the whole component tree under an <EcosystemProvider>
. So why would you need to provide an atom instance to a subtree?
The primary purpose of providing atom instances over React context is to give a reusable subtree an atom instance with specific parameters. The children in the subtree don't need to know what params to pass to the atom.
function Child() {
// without useAtomContext, you need to pass the right params every time you use the atom:
const instance = useAtomInstance(myAtom, [
'my',
{ specific: { params: 'here' } },
])
// compare:
const instance = useAtomContext(myAtom, true)
}
This is particularly useful for lists. Say we have a component that's rendered many times with different props:
function UserThumbnail({ id }) {
return (
<>
<Avatar id={id} />
<Nickname id={id} />
<OnlineStatus id={id} />
</>
)
}
function UserList({ users }) {
return users.map(user => <UserThumbnail key={user.id} id={user.id} />)
}
You can see how all the children of UserThumbnail
might need to use the id to fetch/maintain some state - their current profile picture, their preferred display name, and whether they're online. For example:
function OnlineStatus({ id }) {
const userData = useAtomValue(userData, [id])
return <div>{userData.isOnline ? <GreenDot /> : <RedDot />}</div>
}
But with context, we can instead give the entire subtree its own isolated state:
function UserThumbnail({ id }) {
const instance = useAtomInstance(userData, [id])
return (
<AtomProvider instance={userInstance}>
<Avatar />
<Nickname />
<OnlineStatus />
</AtomProvider>
)
}
function OnlineStatus() {
// no need to pass `id` prop!
const userData = useAtomContext(userData, true)
return <div>{userData.isOnline ? <GreenDot /> : <RedDot />}</div>
}
This is a simple example and doesn't fully demonstrate how powerful this can be for a big component tree. Jotai has documented use cases for this pattern in their docs about molecules and atoms in atoms if you're curious.
Here's a very contrived, in-depth, live example if you want to review a little of what we've learned so far. Otherwise, we're done here. Go next!
Recap
- Atom instances can be provided over React context via
<AtomProvider>
. - Atom instances can be consumed from React context via
useAtomContext()
.useAtomContext(myAtom, [...defaultParams])
creates an atom instance withdefaultParams
if no instance was provided.useAtomContext(myAtom, true)
throws an error if no atom instance was provided.
- You can subscribe any component to a consumed atom instance by using
useAtomValue()
or any other hook that creates a dynamic graph dependency. - Use
useAtomSelector()
with a provided instance to selectively subscribe to updates.
Next Steps
With React context mastered, there is only a little more about using atoms in React. Next up: React suspense.