championswimmer / vuex-persist

A Vuex plugin to persist the store. (Fully Typescript enabled)
http://championswimmer.in/vuex-persist
MIT License
1.67k stars 116 forks source link

Date object not being restored from local storage #33

Closed j-maas closed 6 years ago

j-maas commented 6 years ago

When a Date object is in the state, it is saved to local storage serialized as a string. Upon reload of the page, the date remains a string.

// excerpt from main.js

const vuexLocal = new VuexPersistence({
  storage: window.localStorage,
})

const store = new Vuex.Store({
  state: {
    allDates: [
      new Date(),
    ]
  },
  mutations: {
    'add-date': state => {
      state.allDates.push(new Date())
    }
  },
  plugins: [
    vuexLocal.plugin,
  ]
})
// excerpt from App.js

<template>
    <div id="app">
        <button @click="$store.commit('add-date')">+</button>
        <ul>
            <li v-for="date in $store.state.allDates">
                {{date.toLocaleDateString()}}
            </li>
        </ul>
    </div>
</template>

Excerpt from error in browser console after using the button to add a date and refreshing the page:
TypeError: date.toLocaleDateString is not a function

Reproduction

You can checkout my reproduction repo. It is a minimal Vue app generated with the default vue-cli (v3) scaffold.

To reproduce the bug, follow these steps:

Notes

If you do not hit the "+" button, it works, even though there is a date already initialized from the default state. So if you refresh the page with an empty local storage, everything works as expected. I assume that vuex-persist does not save the state to local storage yet, since nothing has triggered a change.

In the production app I found this in, I use TypeScript. The bug is the same there.

championswimmer commented 6 years ago

It is decision to make here. I am not sure how this will go down.

My 10000 foot overview opinion is this -

Do not store any non-primitive data inside stores that serialize and deserialize in unexpected ways

I would recommend not storing Date objects into vuex. Or if you do, handle serialization and deserialization using your own custom getters and setters.

championswimmer commented 6 years ago

Keeping this open for discussion. If we should deserialize items based on their type or expose an interface to define custom deserializers or not.

I guess we cannot just read a string, and infer if it might be a date or not (without humongous pattern matching penalties)

j-maas commented 6 years ago

I'm fine with manually overwriting the deserialization. How do I do that?

j-maas commented 6 years ago

With localForage it is possible to store objects quite easily. Unfortunately, it will not restore them to the correct type when loading, since it does not support custom types. The prototype will not be set correctly.

To fix this I used cerialize which allows quite easily to make custom classes properly (de-)serializable. It is then only necessary to make vuex-persist execute the deserialization using the restoreState config option.

Example

...
import { autoserialize, autoserializeAs, Deserialize } from 'cerialize';
...

class Apple {
  @autoserialize color = 'red';
}

class Bag {
  @autoserializeAs(Apple) contents = [ new Apple(), new Apple(), new Apple() ];
}

class State {
  @autoserialize primitive: string = 'just a simple string';
  @autoserializeAs(Bag) bag: Bag = new Bag();
}

const mutationTree: MutationTree<State> = {
  'add-apple': (state: State, payload: { apple: Apple }) => { state.bag.contents.push(apple) },
}

const vuexLocal = new VuexPersistence({
  strictMode: true,
  storage: localForage,
  async restoreState(key: string): Promise<State> {
    const state = await localForage.getItem(key)
    const deserialized = Deserialize(state, State)
    return deserialized
  },
  asyncStorage: true,
})
mutationTree['RESTORE_MUTATION'] = vuexLocal.RESTORE_MUTATION  // Add missing mutation for strictMode

const store = new Vuex.Store<State>({
    strict: true,
    state: new State(),
    mutations: mutationTree,
    actions: {},
    plugins: [
      vuexLocal.plugin,
    ],
  })

new Vue({
  ...,
  store,
  ...,
})