In my Vuex store
there is an array of Notes. Each Note is represented by a <textarea>
. I have a NoteArray component that displays each Note:
// NoteArray.vue
export default {
name: "NoteArray",
components: { Note },
computed: {
...mapState({
notes: state => state.notes // get array of Notes from store
})
},
template: `
<div v-for="note in notes">
<!-- make one Note per note in array -->
<Note :contents.sync="note.contents"></Note>
</div>`
}
// Note.vue
export default {
name: "Note",
props: ["contents"], // recieve contents from NoteArray
template: `<textarea v-model="contents"></textarea>`
}
This setup would probably work fine if I weren't using Vuex, but I want the contents of each Note to be represented by a single array in my store:
// index.ts
let store = new Vuex.Store({
state: {
notes: [{contents: ""}] // will have a mutation to add/remove notes
}
}
Right now I'm using v-model
to attach the contents of each Note to itself. This works fine for a one-way binding - the initial state of the Notes propagates nicely down. The problem arises upon attempting to change a Note. The sync
modifier would be used here to establish a two-way binding without me needing to define any input events.
Not so for Vuex - I can only modify the state using a mutation. With strict mode enabled the above example results in the error [vuex] do not mutate vuex store state outside mutation handlers
.
The fix here is to define a mutation that is called by a given Note on @input
that changes the value of that Note's contents
. The only way I can think of would be to define a mutation that accesses the content and changes it (instead of v-model
and sync
):
// index.ts
...
mutations: {
update_note(state, payload) {
state.notes[payload.index] = payload.context
}
}
...
...but that requires that each Note knows, and is able to pass to the mutation, its own index in the state.notes
array. Each Note is entirely unaware of its context, though - they don't have this information.
I'm not sure where to go from here - how can I have the value of each Note's contents
be updated in the store
when they're changed by the user? I want NoteArray and Note to remain their own components.
Implementation of the above sample: https://codesandbox.io/s/keen-moser-oe63d
After a bit of digging, it turns out all you have to do is ditch v-model
and $emit('update:contents', $event.target.value)
on @input
event of <textarea>
. Everything else my initial answer contained is not actually needed.
Here's a working example.
As you can see, the notes are updated without any commit
and they are displayed in App.vue
correctly. I placed the test in App.vue
to make sure they're updated in the state, not only in the vm
of NoteList.vue
.
I added unique identifiers because I discovered that, without them, when removing a note <textarea>
s would display the wrong contents (from the next note in the notes array).
This is precisely why key-ing by index is to be avoided. (Read the warning at the end of this documentation section).
Now, to be totally fair, I don't really understand why modifying through .sync
doesn't trigger the "don't mutate outside the store" warning. To answer that, one would have to dig into what exactly does .sync
do. Or maybe it has to do with not changing the structure of the object. Not really sure.
Anyways, the correct way of doing it would be to dispatch an action on update:contents
which would commit a mutation which would update the store:
Example: https://codesandbox.io/s/frosty-feather-lhurk?file=/src/components/NoteList.vue.
Another note: as shown by this discussion, prop.sync
didn't use to "magically work" out of the box before on state properties, so it did need the dispatch
+ commit
which apparently are no longer needed.