Persistence
There are many ways to persist and restore state "snapshots" in Zedux. It can be done at the atom instance, ecosystem, or plugin level.
- How to persist and restore a single atom instance's state
- How to dehydrate and rehydrate an entire ecosystem
- How to transform non-serializable atom values
- One of many ways to accomplish this with plugins
Single Atom
Let's start simple. We'll use localStorage
for these examples, but you can use whatever means of storage you want.
One way to persist the state of a single atom is to use a separate atom to handle the persist/restore functionality.
Here's an example using a separate localStorageAtom
that we give a key
param, creating a different localStorageAtom
instance for every localStorage key we want to persist data to/from:
Another way is to use an injector:
Yep, these approaches are very similar.
Ecosystem Persistence
There are a few ways to get a state snapshot of an entire ecosystem. For example, you could call ecosystem.findAll()
and map each instance to a value yourself. But ecosystems have a method that's specifically designed for persistence:
ecosystem.dehydrate()
You can get a snapshot of the state of all atoms in an ecosystem with ecosystem.dehydrate({ transform: false })
import { atom, createEcosystem } from '@zedux/react'
const exampleAtom = atom('example', () => 'Hello, world!')
const ecosystem = createEcosystem({ id: 'root' })
ecosystem.getInstance(exampleAtom)
const snapshot = ecosystem.dehydrate({ transform: false })
This may be all you need. However, passing { transform: false }
makes ecosystem.dehydrate()
very "dumb" - it doesn't do anything special to transform the state, it just returns it as-is. Since atoms can hold anything, you may have a situation where you're storing non-serializable values (namely functions) in atoms. Data types from Immutable.js, for example, require an extra step to serialize/deserialize values.
For these atoms, you have two options:
- Skip the non-serializable atom.
- Transform the atom's value to something that can be serialized.
First things first:
Skipping Atoms
You can blacklist atoms you don't want to dehydrate by passing exclude
and/or excludeFlags
options:
Similarly, you can specify a whitelist of atoms you want to be dehydrated by passing include
and/or includeFlags
options:
Excludes take precedence over includes if you pass both.
Now for transforming values:
dehydrate
Atom Config
Atoms can be configured with a dehydrate
config option:
const exampleAtom = atom('example', () => initialState, {
dehydrate: state => transform(state),
})
To enable value transforming, simply omit { transform: false }
in the ecosystem.dehydrate()
call. Transforms are enabled by default.
ecosystem.dehydrate()
returns an object mapping atom instance ids to their current value. But with transforms enabled, .dehydrate()
does a little more magic.
ecosystem.dehydrate()
calls the dehydrate
atom config option (when specified) to transform the state of individual atom instances.
Here's an example of an atom whose value is a JS Map, which can't be directly stringified. We use the dehydrate
atom config option to tell Zedux how to transform this atom instance's state to a serializable value for dehydration:
Now calls to ecosystem.dehydrate()
call the dehydrate
atom config function to transform the state of all instances of the transforming
atom.
ecosystem.hydrate()
The inverse of ecosystem.dehydrate()
. After you get a state snapshot, use ecosystem.hydrate()
to restore it.
After hydration, any newly-initialized atom instances that have a matching key in the hydrated snapshot will receive their hydrated state after initializing.
Hydration will also immediately update any existing atom instances with ids in the snapshot. Pass { retroactive: false }
as the second parameter to ecosystem.hydrate()
to disable this. With this option, only instances that are created after hydration will have their state hydrated.
hydrate
Atom Config
Atoms can be configured with a hydrate
transformation function:
const exampleAtom = atom('example', initialState, {
hydrate: rawVal => transform(rawVal),
})
This can be used to transform dehydrated values back to non-serializable form. Let's plug that into the JS Map example:
Store Hydration
The default hydration flow hydrates the store after the atom instance's initial evaluation ends. This means that if your state factory's main store was injected without subscribe: false
, it will immediately trigger a second evaluation.
This is usually fine. But you can change this behavior in 2 steps:
- Hydrate the injected store's state yourself.
- Tell Zedux not to automatically hydrate this atom.
To hydrate the store's state, use the hydrate
option of injectStore
:
const store = injectStore(defaultValue, { hydrate: true })
The store's state will be set to the hydration matching the current atom instance, after passing it to the hydrate
atom config option (if any) to transform the value. If there is no hydration for the current atom instance, passing { hydrate: true }
does nothing.
When injectStore()
's function overload is used, the hydration will be passed to your storeFactory
function. It's up to you to use the hydration to set your store's initial state.
const store = injectStore(
hydration => createStore(null, hydration ?? defaultValue),
{ hydrate: true }
)
// (this specific example is exactly equivalent to:)
const store = injectStore(defaultValue, { hydrate: true })
If there is no hydration, this parameter will be undefined.
Now for the 2nd step:
manualHydration
Set this atom config option to true
to prevent Zedux from automatically hydrating instances of the atom. You'll usually want to pair this with passing hydrate: true
to a single injectStore
in the atom's state factory.
const exampleAtom = atom(
'example',
() => {
const store = injectStore(defaultValue, { hydrate: true })
const internalStore = injectStore() // can inject other, non-hydrated stores
return store // Zedux won't hydrate this store's state after init now
},
{
manualHydration: true, // prevent automatic hydration
}
)
Hydration consumption
When Zedux hydrates an atom instance, it deletes the hydration from the ecosystem. This makes it easy to see which hydrations have been "consumed" by simply accessing the ecosystem's .hydration
property:
Hydrations will also be consumed for retroactively-hydrated atom instances.
This also means that if an atom instance is destroyed and recreated, the new atom instance will not be hydrated again. This is usually the desired behavior, but if you do need this you can accomplish it by "rehydrating" the ecosystem before the instance is destroyed:
const rehydratingAtom = atom(
'rehydrating',
(...params: any[]) => {
const { ecosystem } = injectAtomGetters()
const store = injectStore('default state', { hydrate: true })
const hydrationRef = injectRef(store.getState()) // capture the hydrated state
injectEffect(() => {
// this effect does nothing but setup cleanup
return () => {
// one way to get the current atom instance's id:
const instance = ecosystem.getInstance(rehydratingAtom, [...params])
// rehydrate!
ecosystem.hydrate({ [instance.id]: hydrationRef.current })
}
}, []) // [] - cleanup will only run on instance destruction
return store
},
{ ttl: 0 }
)
Reactivity Via Plugins
The localStorage
single atom examples at the beginning of this page were beautifully reactive - they would persist the atom's state immediately on state change. None of the ecosystem examples so far do that. You have to manually call ecosystem.dehydrate()
e.g. in response to a button press or on an interval.
By default, ecosystems don't receive any kind of update when individual atom instances change state. This is by design, for performance reasons. However, it is possible to turn this behavior on with plugins.
Just turn on the stateChanged
mod.
import { createEcosystem, ZeduxPlugin } from '@zedux/react'
const plugin = new ZeduxPlugin({
initialMods: ['stateChanged'],
registerEcosystem: ecosystem => {
const subscription = ecosystem.modsMessageBus.subscribe({
effects: ({ action }) => {
if (action.type !== ZeduxPlugin.actions.stateChanged.type) return
const snapshot = ecosystem.dehydrate({ includeFlags: ['persist'] })
localStorage.setItem('snapshot', snapshot)
},
})
return () => subscription.unsubscribe()
},
})
const ecosystem = createEcosystem({ id: 'root' })
ecosystem.registerPlugin(plugin)
Recap
- Use
ecosystem.dehydrate()
to get a state snapshot of all or some of the atoms in the ecosystem.- Use the
dehydrate
atom config option to transform the state of individual atoms to a serializable or shortened form.
- Use the
- Use
ecosystem.hydrate()
to restore a state snapshot.- Use the
hydrate
atom config option to transform hydrations for individual atoms.
- Use the
- The
{ hydrate: true }
injectStore
option sets the store's initial state to the hydrated value. - Use the
manualHydration
atom config option to prevent Zedux from automatically hydrating individual atoms. - Use a plugin with the
stateChanged
mod to reactively persist snapshots.
Next Steps
With the persistence tools mastered, you can use these tools to implement SSR.