Redux persist is a library allowing to save the redux store in the local storage of your browser.But, using redux-persist and changing the store's architecture could trigger issues. This article illustrates, with a basic example, how persistence works with your Redux store, then focus on the persisted store's transformation across code versions and app uses.
How to persist a store and how it works
Let's take, as an example, this redux store from a React project, generated with create-react-app:
- a debounced input with current value = inputValue
- a button GO submitting inputValue in store as key confirmedMessage
- a persisted field named number in redux counting the number of submissions made.
I implemented redux-persist as explained in the official doc and my store. You can find all the code in this repo.
There is a custom function persist(), to define in persistConfig only what's changed for this article.
Steps of persistence:
Persistence with Redux-persist occurs in 3 mains steps. To show it, i use Redux devTools chrome extension : INIT, PERSIST and REHYDRATE.
While visiting the app, the redux store is created as the initial state given through the reducers. This defines the first content of your app, and persistence has not done its job yet.
During persist step, in every persisted store, an object is added with this configuration:
The version has a default value of -1.
The rehydrated value is a boolean used to check if the persistence has been applied yet, it will be used later.
This phase is where the persisted data stored in browser replace the data in Redux store.
Across all reducers, every local state is "rehydrated" and is replaced by the persisted store.
Each reducer replaces their content by the persisted one.
That allows us to keep information during sessions of navigation (or if you refresh your page).
You now have your redux store with value in your storage.
Modifying the architecture of the Redux Store
Developing a new feature impacting the store architecture or refactoring a part of your store could trigger an inconsistency between the persisted store with the old architecture and new pushed architecture.
Indeed, the initial state represents the new version of your code, but during rehydration, every persisted store will be replaced by the same key-named persisted store, keeping the old architecture.
For example illustrated by the image below, if I want to change the architecture of my store to add the last update information to my number of change, steps of persistence are:
@INIT: initial state set the form I need
persist/PERSIST: _persist object is added with rehydrated false
persist/REHYDRATE: persisted data replace the Redux store.
What went wrong here?
The variable 'number', which is supposed to have the structure of the new version of the app, is replaced by the old persisted structure. You lose all your store changes.
This inconsistency can jeopardize the use of your code if you don't anticipate them.
Handling it with a version-controlled store.
Redux-persist allows you to transform your store depending on the version you want for the app: you can control the version of your store.
To do this, use the function createMigrate() and pass in the persistConfig object in migrate" key:
Migrations are applied on your state before replacing your store data in REHYDRATE step.
If your storage comes from the store in version -1, which is the default version, and you deploy version 0 in your app, the migration 0 will be applied on storage state at every user's connexion:
For example, with this definition of the passed migrations, your state will be reset as the initial state and you will have the expected behaviour during REHYDRATE:
You can use createMigrate() function to transform persisted data architecture to fit with your new store: For our previous example, keeping the data will be:
This version 1 of migration will put my previous number of change where I want.
To note changes and debug your migrations, you can set the debug version to true to log which migrations are applied.
To go further and see how migrations are applied, you can look at the source code of createMigrate() function.
In some cases, you may not use migrations.
There is more pragmatic solutions that could solve this problem faster:
- Make users clear their storage. In my case, it would be localStorage.clear('persist:persistedStore') with a condition of version in your reducers.
- Remove the persistence of your store and create a new one with a new key name: you still lose the changes the users had and you have to adapt your code depending on your change.
- Use createTransform() in Redux-persist: You can store your data using createTransform and only change inbound/outbound function when you want to change your code architecture.
To my mind, version controlled redux store should be used when:
- you need to keep the history of your modification
- you often change the form of your store and you have users in production
- local storage of your users are important to be kept.