vuex数据持久化插件vuex-persist
# vuex-persist
A Typescript-ready Vuex (opens new window) plugin that enables you to save the state of your app to a persisted storage like Cookies or localStorage.
# 目录
# 兼容性
- VueJS (opens new window) - >=v2.0
- Vuex (opens new window) - >=v2.1
# 安装
# Vue CLI Build Setup (using Webpack or some bundler)
npm install --save vuex-persist
or
yarn add vuex-persist
# Transpile for target: es5
This module is distributed in 3 formats
- umd build
/dist/umd/index.js
in es5 format - commonjs build
/dist/cjs/index.js
in es2015 format - esm build
/dist/esm/index.js
in es2015 format
When using with Webpack (or Vue CLI 3), the esm file gets used by default.
If your project has a es6
or es2015
target, you're good, but if
for backwards compatibility, you are compiling your project to es5
then
this module also needs to be transpiled.
To enable transpilation of this module
// in your vue.config.js
module.exports = {
/* ... other config ... */
transpileDependencies: ['vuex-persist']
}
2
3
4
5
# Directly in Browser
<!-- We need lodash.merge so get lodash first -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuex-persist"></script>
2
3
# Tips for NUXT
This is a plugin that works only on the client side (opens new window). So we'll register it as a ssr-free plugin.
// Inside - nuxt.config.js
export default {
plugins: [
{ src: '~/plugins/vuex-persist', ssr: false }
]
}
2
3
4
5
6
// ~/plugins/vuex-persist.js
import VuexPersistence from 'vuex-persist'
export default ({ store }) => {
new VuexPersistence({
/* your options */
}).plugin(store);
}
2
3
4
5
6
7
8
# Usage
# Steps
Import it
import VuexPersistence from 'vuex-persist'
NOTE: In browsers, you can directly use
window.VuexPersistence
Create an object
const vuexLocal = new VuexPersistence({
storage: window.localStorage
})
// or in Typescript
const vuexLocal = new VuexPersistence<RootState>({
storage: window.localStorage
})
2
3
4
5
6
7
8
9
10
Use it as Vue plugin. (in typescript)
const store = new Vuex.Store<State>({
state: { ... },
mutations: { ... },
actions: { ... },
plugins: [vuexLocal.plugin]
})
2
3
4
5
6
(or in Javascript)
const store = {
state: { ... },
mutations: { ... },
actions: { ... },
plugins: [vuexLocal.plugin]
}
2
3
4
5
6
# Constructor Parameters -
When creating the VuexPersistence object, we pass an options
object
of type PersistOptions
.
Here are the properties, and what they mean -
Property | Type | Description |
---|---|---|
key | string | 在存储器中存储状态的键 Default: 'vuex' |
storage | Storage (Web API) | localStorage, sessionStorage, localforage or your custom Storage object. 必须实现 getItem, setItem, clear 等方法. 默认: window.localStorage |
saveState | function (key, state[, storage]) | 如果不使用存储,则由此自定义函数处理 saving state to persistence |
restoreState | function (key[, storage]) => state | 如果不使用存储,则由此自定义函数处理 retrieving state from storage |
reducer | function (state) => object | State reducer. reduces state to only those values you want to save. By default, saves entire state |
filter | function (mutation) => boolean | Mutation filter. Look at mutation.type and return true for only those ones which you want a persistence write to be triggered for. Default returns true for all mutations |
modules | string[] | List of modules you want to persist. (Do not write your own reducer if you want to use this) |
asyncStorage | boolean | Denotes if the store uses Promises (like localforage) or not (you must set this to true when suing something like localforage) Default: false |
supportCircular | boolean | Denotes if the state has any circular references to itself (state.x === state) Default: false |
# Usage Notes
# Reducer
Your reducer should not change the shape of the state.
const persist = new VuexPersistence({
reducer: (state) => state.products,
...
})
2
3
4
Above code is wrong You intend to do this instead
const persist = new VuexPersistence({
reducer: (state) => ({products: state.products}),
...
})
2
3
4
# Circular States
If you have circular structures in your state
let x = { a: 10 }
x.x = x
x.x === x.x.x // true
x.x.x.a === x.x.x.x.a //true
2
3
4
JSON.parse()
and JSON.stringify()
will not work.
You'll need to install flatted
npm install flatted
And when constructing the store, add supportCircular
flag
new VuexPersistence({
supportCircular: true,
...
})
2
3
4
# Examples
# Simple
Quick example -
import Vue from 'vue'
import Vuex from 'vuex'
import VuexPersistence from 'vuex-persist'
Vue.use(Vuex)
const store = new Vuex.Store<State>({
state: {
user: { name: 'Arnav' },
navigation: { path: '/home' }
},
plugins: [new VuexPersistence().plugin]
})
export default store
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Detailed
Here is an example store that has 2 modules, user
and navigation
We are going to save user details into a Cookie (using js-cookie)
And, we will save the navigation state into localStorage whenever
a new item is added to nav items.
So you can use multiple VuexPersistence instances to store different
parts of your Vuex store into different storage providers.
Warning: when working with modules these should be registered in
the Vuex constructor. When using store.registerModule
you risk the
(restored) persisted state being overwritten with the default state
defined in the module itself.
import Vue from 'vue'
import Vuex, { Payload, Store } from 'vuex'
import VuexPersistence from 'vuex-persist'
import Cookies from 'js-cookie'
import { module as userModule, UserState } from './user'
import navModule, { NavigationState } from './navigation'
export interface State {
user: UserState
navigation: NavigationState
}
Vue.use(Vuex)
const vuexCookie = new VuexPersistence<State, Payload>({
restoreState: (key, storage) => Cookies.getJSON(key),
saveState: (key, state, storage) =>
Cookies.set(key, state, {
expires: 3
}),
modules: ['user'], //only save user module
filter: (mutation) => mutation.type == 'logIn' || mutation.type == 'logOut'
})
const vuexLocal = new VuexPersistence<State, Payload>({
storage: window.localStorage,
reducer: (state) => ({ navigation: state.navigation }), //only save navigation module
filter: (mutation) => mutation.type == 'addNavItem'
})
const store = new Vuex.Store<State>({
modules: {
user: userModule,
navigation: navModule
},
plugins: [vuexCookie.plugin, vuexLocal.plugin]
})
export default store
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# Support Strict Mode
This now supports Vuex strict mode (opens new window)
(Keep in mind, NOT to use strict mode in production)
In strict mode, we cannot use store.replaceState
so instead we use a mutation
You'll need to keep in mind to add the RESTORE_MUTATION
to your mutations
See example below
To configure with strict mode support -
import Vue from 'vue'
import Vuex, { Payload, Store } from 'vuex'
import VuexPersistence from 'vuex-persist'
const vuexPersist = new VuexPersistence<any, any>({
strictMode: true, // This **MUST** be set to true
storage: localStorage,
reducer: (state) => ({ dog: state.dog }),
filter: (mutation) => mutation.type === 'dogBark'
})
const store = new Vuex.Store<State>({
strict: true, // This makes the Vuex store strict
state: {
user: {
name: 'Arnav'
},
foo: {
bar: 'baz'
}
},
mutations: {
RESTORE_MUTATION: vuexPersist.RESTORE_MUTATION // this mutation **MUST** be named "RESTORE_MUTATION"
},
plugins: [vuexPersist.plugin]
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Some of the most popular ways to persist your store would be -
- js-cookie (opens new window) to use browser Cookies
- window.localStorage (remains, across PC reboots, untill you clear browser data)
- window.sessionStorage (vanishes when you close browser tab)
- localForage (opens new window) Uses IndexedDB from the browser
# Note on LocalForage and async stores
There is Window.Storage API as defined by HTML5 DOM specs, which implements the following -
interface Storage {
readonly length: number
clear(): void
getItem(key: string): string | null
key(index: number): string | null
removeItem(key: string): void
setItem(key: string, data: string): void
[key: string]: any
[index: number]: string
}
2
3
4
5
6
7
8
9
10
As you can see it is an entirely synchronous storage. Also note that it saves only string values. Thus objects are stringified and stored.
Now note the representative interface of Local Forage -
export interface LocalForage {
getItem<T>(key: string): Promise<T>
setItem<T>(key: string, data: T): Promise<T>
removeItem(key: string): Promise<void>
clear(): Promise<void>
length(): Promise<number>
key(keyIndex: number): Promise<string>
_config?: {
name: string
}
}
2
3
4
5
6
7
8
9
10
11
You can note 2 differences here -
- All functions are asynchronous with Promises (because WebSQL and IndexedDB are async)
- It works on objects too (not just strings)
I have made vuex-persist
compatible with both types of storages, but this comes at a slight cost.
When using asynchronous (promise-based) storages, your state will not be
immediately restored into vuex from localForage. It will go into the event loop
and will finish when the JS thread is empty. This can invoke a delay of few seconds.
# How to know when async store has been replaced
As noted above, the store is not immediately restored from async stores like localForage. This can have the unfortunate side effect of overwriting mutations to the store that happen before vuex-persist
has a chance to do its thing. In strict mode, you can create a plugin to subscribe to RESTORE_MUTATION
so that you tell your app to wait until the state has been restored before committing any further mutations. (Issue #15 demonstrates how to write such a plugin. (opens new window)) However, since you should turn strict mode off in production, and since vuex
doesn't currently provide any kind of notification when replaceState()
has been called (opens new window), starting with v2.1.0
vuex-persist
will add a restored
property to the store
object to let you know the state has been restored and that it is now safe to commit any mutations that modify the stored state. store.restored
will contain the Promise returned by calling the async version of restoreState()
.
Here's an example of a beforeEach()
hook in vuex-router
that will cause your app to wait for vuex-persist
to restore the state before taking any further actions:
// in src/router.js
import Vue from 'vue'
import Router from 'vue-router'
import { store } from '@/store' // ...or wherever your `vuex` store is defined
Vue.use(Router)
const router = new Router({
// define your router as you normally would
})
const waitForStorageToBeReady = async (to, from, next) => {
await store.restored
next()
}
router.beforeEach(waitForStorageToBeReady)
export default router
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Note that on the 2nd and subsequent router requests to your app, the Promise in store.restored
should already be in a "resolved" state, so the hook will not force your app to wait for additional calls to restoreState()
.
# Unit Testing
# Jest
When testing with Jest, you might find this error -
TypeError: Cannot read property 'getItem' of undefined
This is because there is no localStorage in Jest. You can add the following Jest plugins to solve this https://www.npmjs.com/package/jest-localstorage-mock (opens new window)