persist
This helper allows you to persist your store state (by default to sessionStorage
), allowing it to be rehydrated when your application is remounted (e.g. on page refresh).
This API is heavily inspired by
redux-persist
, with the intention of providing a lot of compatibility with it so that we can leverage the packages that exist within it's ecosystem.
To utilise this feature you simply need to wrap your model with the helper.
const store = createStore(
persist({
count: 1,
inc: action(state => {
state.count += 1;
})
})
);
Every time the state changes it will be saved to the configured storage engine (sessionStorage
by default).
When your application is freshly mounted, e.g. on a page refresh, any data that exists within the configured storage engine will be used to rehydrate your state accordingly.
const store = createStore(model); // state is automatically rehydrated
// Application then renders with rehydrated state
const app = (
<StoreProvider store={store}>
<MyApp />
</StoreProvider>
);
API
model
(Object, required)The model that you wish to apply persistence to.
You can surround your entire model, or a nested model. You can even have multiple
persist
configurations scattered throughout your store's model. Feel free to use the API on the parts of your state feel most appropriate for persistence/rehydration.config
(Object, optional)The persistence configuration. It supports the following properties:
blacklist
(Array, optional) A list of keys, representing the parts of the model that should not be persisted. Any part of the model that is not represented in this list will be persisted.
whitelist
(Array, optional) A list of keys, representing the parts of the model that should be persisted. Any part of the model that is not represented in this list will not be persisted.
mergeStrategy
(string, optional)The strategy that should be employed when rehydrating the persisted state over your store's initial state.
The following values are supported:
'merge'
(default)The data from the persistence will be shallow merged with the initial state represented by your store's model.
i.e.
Given the following persisted state:
{ "fruit": "apple", "address": { "city": "cape town" } }
And the following initial state represented by your store's model:
{ "address": { "city": "london", "post code": "e3 1pq" }, "animal": "dolphin" }
The resulting state will be:
{ "fruit": "apple", "address": { "city": "cape town" }, "animal": "dolphin" }
'overwrite'
The data from the persistence will completely overwrite the initial state represented by your store's model.
i.e.
Given the following persisted state:
{ "fruit": "apple", "city": "cape town" }
And the following initial state represented by your store's model:
{ "fruit": "pear", "animal": "dolphin" }
The resulting state will be:
{ "fruit": "apple", "city": "cape town" }
'mergeDeep'
The data from the persistence will be merged deeply, recursing through all object structures and merging.
i.e.
Given the following persisted state:
{ "fruit": "apple", "address": { "city": "cape town" } }
And the following initial state represented by your store's model:
{ "address": { "city": "london", "post code": "e3 1pq" }, "animal": "dolphin" }
The resulting state will be:
{ "fruit": "apple", "address": { "city": "cape town", "post code": "e3 1pq" }, "animal": "dolphin" }
Note: Only plain objects will be recursed and merged; no other types such as Arrays, Maps, Sets, Classes etc.
transformers
(Array, optional) Transformers are use to apply operations to your data during prior it being persisted or hydrated.
One use case for a transformer is to handle data that can't be parsed to a JSON string. For example a
Map
orSet
. To handle these data types you could utilise a transformer that converts theMap
/Set
to/from anArray
orObject
.Transformers are applied left to right during data persistence, and are applied right to left during data rehydration.
redux-persist
already has a robust set of transformer packages that have been built for it. These can be used here.storage
(string | Object, optional)The storage engine to be used. It defaults to
sessionStorage
. The following values are supported:'sessionStorage'
Use the browser's sessionStorage as the persistence layer.
i.e. data is available for rehydration for a single browser session
'localStorage'
Use the browser's localStorage as the persistence layer.
i.e. data is available across browser sessions
Custom engine
A custom storage engine.
redux-persist
already has a robust set of storage engine packages that have been built for it. These can be used here.
Example
In the simple example below we will make our entire store persist.
import { persist } from 'easy-peasy';
// 👆 import the helper
// Then wrap the root model with the helper
// 👇
let model = persist({
counter: 0,
todos: [],
increment: (state) => {
state.counter += 1;
}
});
Example with configuration
The below examples demonstrates a configured persistence instance in which we will only persist the counter
.
const model = persist(
{
counter: 0,
todos: [],
increment: (state) => {
state.counter += 1;
}
},
// 👇 configuration
{
whitelist: ['counter'],
}
);
Nested persistence
The below example demonstrates that you can utilise the persist
utility at any depth of your model.
const model = {
todos: {
todos: [],
addTodo: (state, payload) => {
state.todos.push(payload);
}
},
session: persist({
user: null,
login: thunk(/* ... */)
})
}
There is no restriction on how many persist
instances you can have on your model. Provide as many configurations as you require and the respective models will have their state persisted and rehydrated accordingly.
Working with asynchronous storage engines
When utilising an asynchronous storage engine (i.e. their storage APIs return Promise
s) you may want to wait for their asynchronous operations to complete prior to rendering your application. This would help to avoid a flash of content change, where your site would initially render with the default store state, and then suddenly rerender with the rehydrated state after it is resolved from the asynchronous storage engine.
There are two strategies that you can employ to deal with this case.
Option 1: Wait for the rehydration to complete prior to rendering your application
The store instance contains an API allowing to access a Promise
that represents the resolution of the asynchronous storage state being resolved during state rehydration. You can wait on this Promise
prior to rendering your application, which would ensure that your application is rendered with the expected rehydrated state.
const store = createStore(persist(model, { storage: asyncStorageEngine });
store.persist.resolveRehydration().then(() => {
ReactDOM.render(
<StoreProvider store={store}>
<App />
</StoreProvider>,
document.getElementById('app')
);
});
Option 2: Eagerly render your application and utilise the useStoreRehydrated
hook
You can alternatively render your application immediately, i.e. not wait for the async rehydration to resolve.
To improve your user's experience you can utilise the useStoreRehydrated
hook. This hook returns a boolean flag indicating when the rehydration has completed.
import { useStoreRehydrated } from 'easy-peasy';
const store = createStore(persist(model, { storage: asyncStorageEngine });
function App() {
const rehydrated = useStoreRehydrated();
return (
<div>
<Header />
{rehydrated ? <Main /> : <div>Loading...</div>}
<Footer />
</div>
)
}
ReactDOM.render(
<StoreProvider store={store}>
<App />
</StoreProvider>,
document.getElementById('app')
);
In the example above, the <Main />
content will not render until our store has been successfully updated with the rehydration state.
Persisting multiple stores
If you utilise multiple stores, each with their own persistence configuration, you will need to ensure that each store is configured to have a unique name. The store name for each instance of your stores will be used within the persistence layer cache keys.
Creating a custom storage engine
A storage engine is an object structure that needs to implement the following interface:
getItem(key) => any | Promise<any> | void
This function will receive the key, i.e. the key of the model item being rehydrated, and should return the associated data from the persistence if it exists. It can alternatively return a
Promise
that resolves the data, orundefined
if no persisted data was found.setItem(key, data) => void | Promise<void>
This function will receive the key, i.e. the key of the model data being persisted, as well as the associated data value. It should then store the respective data. It can alternatively return a
Promise
which indicates when the item has been successfully persisted.removeItem(key) => void | Promise<void>
This function will receive the key, i.e. the key of the model item that exists in the persistence, and should remove any data that is currently being stored within the persistence. It can alternatively return a
Promise
which indicates when the item has been successfully removed from the persistence.
Creating a custom transformer
Easy Peasy outputs a createTransformer
function, which has been directly copied from redux-persist
in order to maximum compatiblity with it's ecosystem.