Skip to main content

Persisting State

StateManager and AsyncStateManager provides three lifecycle hooks:

  • init — triggers when the State Manager is instantiated
  • didSet — triggers each time the state is changed with .set, even if the value remains the same
  • didReset — triggers each time .reset is called
tip

These lifecycle hooks are are independent of each other and completely optional, but as far as lifecycle management is concerned, it is generally recommended to use all three.

Persisting & resetting state

To begin, we can start by defining didSet and didReset. By doing so, we would have a better idea of how the state is being persisted, which in return makes it easier for us to figure out how to initialize the state. In this example, we will serialize the state using JSON.stringify and save it to localStorage.

const UserState = new StateManager({
firstName: 'John',
lastName: 'Smith',
luckyNumber: 42,
}, {
lifecycle: {
didSet({ state, defaultState }) {
localStorage.removeItem('key', JSON.stringify(state))
},
didReset() {
localStorage.removeItem('key')
},
},
})
Caution!

Do not call .set inside the didSet lifecycle hook as this will result in infinite recursion. The same applies to calling .reset in the didReset lifecycle hook and calling .init in the init lifecycle hook.

Retrieving state

Now that we know how the state is being persisted, all we need to do is to parse it back with JSON.parse in the init lifecycle hook.

const UserState = new StateManager({
firstName: 'John',
lastName: 'Smith',
luckyNumber: 42,
}, {
lifecycle: {
init({ commit }) {
const persistedState = JSON.parse(localStorage.getItem('key'))
commit(persistedState)
},
didSet({ state, defaultState }) {
localStorage.removeItem('key', JSON.stringify(state))
},
didReset() {
localStorage.removeItem('key')
},
},
})

Putting everything together

The code in the section above is a simple example of we can persist data and use it to initialize a State Manager. In reality, however, it could be a little bit more complicated. The code below shows what it might look like in a more realistic scenario.

const UserState = new StateManager({
firstName: 'John',
lastName: 'Smith',
luckyNumber: 42,
}, {
// 0. Do not run any lifecycle hooks in server environment
// (for projects that uses server-side rendering)
lifecycle: typeof window === 'undefined' ? {} : {
init({ commit, commitNoop, defaultState }) {
// 1. Get raw (string) state.
const rawState = localStorage.getItem('key')
if (rawState) {
try {
// 2. If it exists, attempt to parse it.
const persistedState = JSON.parse(rawState)
// 3. Commit the state by merging the default and the persisted states
// using the spread operator.
commit({
...defaultState,
...persistedState,
})
// 4. Initialization has completed,
// execution of the function can be ended early.
return
} catch (error) {
// 5. try-catch is used in case of corrupted JSON string.
console.error(error)
}
}
commitNoop() // pronounced as "commit no-op"
// 6. This indicates that initialization has been completed but without
// the need to change the state (state remains to be the default value).
},
didSet({ state, defaultState }) {
localStorage.removeItem('key', JSON.stringify(state))
},
didReset() {
localStorage.removeItem('key')
},
},
})