1

I have a view that is a profile page, which facilitates the user to update their profile information. I initially pulled the data from the store with:

this.$store.state.auth.user

But when I bound it to the input controls with v-model, the <input> changed the Vuex state properties on the input event. Based on a solution from the docs, I altered the controls to use the :value attribute to show their contents, and merged the state data into a local property with Object.assign():

user: Object.assign({
    name: ''
    ...
}, this.$store.state.auth.user),

This partially worked, as now once the data has been saved, it appears that the data in my merged object is referencing the state object, so if the user tries to update the data again before the component is destroyed, I get:

"Error: [vuex] do not mutate vuex store state outside mutation handlers."

I realise I could probably re-initialise the local property after submitting, and I could always run:

this.$router.go()

But I have this nagging feeling that I'm making a school-boy error. What's the idiomatic way to fix this?

tony19
  • 125,647
  • 18
  • 229
  • 307
Mark Bell
  • 316
  • 5
  • 14

1 Answers1

2

Shallow copy vs. Deep copy

Object.assign only creates a shallow copy, so your copy of user would still refer to any nested object values of the original user object, as shown in the demo below:

const orig = {
  age: 21,
  name: {
    first: 'John',
    last: 'Doe'
  },
  username: 'jdoe_11',
}

const copy = Object.assign({}, orig) // or: const copy = { ...orig }

// both `name` properties refer to the *same* nested object
console.log('copy.name === orig.name =>', copy.name === orig.name) // => true

copy.name.first = 'Bob'
console.log('orig.name.first =>', orig.name.first) // => 'Bob'

If you want to make a deep copy that is isolated from the original object, I recommend using a utility, such as lodash.cloneDeep, which recursively copies objects, including all nested properties:

const orig = {
  age: 21,
  name: {
    first: 'John',
    last: 'Doe'
  },
  username: 'jdoe_11',
}

const copy = _.cloneDeep(orig)
console.log('copy.name === orig.name =>', copy.name === orig.name) // => false

copy.name.first = 'Bob'
console.log('orig.name.first =>', orig.name.first) // => 'John'
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.15.0/lodash.min.js"></script>

Vuex solution

The Vuex docs discuss the solution to your scenario.

The "Vuex way" to deal with it is binding the <input>'s value and call an action on the input or change event:

<input :value="message" @input="updateMessage">
// ...
computed: {
  ...mapState({
    message: state => state.obj.message
  })
},
methods: {
  updateMessage (e) {
    this.$store.commit('updateMessage', e.target.value)
  }
}

And here's the mutation handler:

// ...
mutations: {
  updateMessage (state, message) {
    state.obj.message = message
  }
}

Two-way Computed Property

Admittedly, the above is quite a bit more verbose than v-model + local state, and we lose some of the useful features from v-model as well. An alternative approach is using a two-way computed property with a setter:

<input v-model="message">
// ...
computed: {
  message: {
    get () {
      return this.$store.state.obj.message
    },
    set (value) {
      this.$store.commit('updateMessage', value)
    }
  }
}

If the recommended solution above were infeasible, you could disable Vuex's strict mode as a last resort. Note that strict mode is a development tool that prevents unintentional state changes outside of mutation handlers, so care should be taken in disabling it:

// store.js
export default new Vuex.Store({
  // ...
  strict: false //  disable strict mode
})
tony19
  • 125,647
  • 18
  • 229
  • 307