1

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

3
  • What you've posted so far requires a bit of work into recreating a working demo, which could then be made to work as you want. Could you create a minimal reproducible example in codesandbox.io or similar with what you have so far? Commented May 30, 2020 at 0:47
  • 1
    @tao Sure thing, will do. Thanks for the heads-up. Commented May 30, 2020 at 0:49
  • Ha! Sorry - was a little slow on the draw. Here's my attempt anyway codesandbox.io/s/keen-moser-oe63d Commented May 30, 2020 at 1:32

2 Answers 2

1

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.

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks for your answer - I was playing with your sample and couldn't get it to work, but it looks like in the time it took me to write out a comment, you'd fixed it! Using the index as a key and passing it along with the update signal, while that index never touches the child, feels like a very clean solution to me.
@snazzy, in all fairness, using index for key-ing is brittle. You can't reorder them if you want to and you can't update them individually (consider saving only one by sending a request to a back-end). Admittedly, none of these problems are show-stoppers in your case, but in a real world use-case, providing uuids for them is the first thing you do. Also note the warning in Vue documentation: ".keys() are not guaranteed to be consistent across JavaScript engine implementations".
Those concerns are noteworthy, so thank you for making me aware of them. For this application I will eventually be wanting to reorder these. It's possible that my immediate acceptance of this method is my AngularJS habits leaping to the foreground. I think taking the path of using a UUID for each Note would lead to @ssc-hrep3's answer?
Well, not really, I would personally never make notes an object. I'd make it an array of objects: [{ id: '9b10fd9e-4837-4a51-b466-1c74c0e6498a', contents: ""}, {id: '64ad38c0-9bd8-4a11-b9a3-cccc35ad1fa3', contents: ""}], etc... First and foremost, arrays are faster. Secondly, they have handy methods. Like .length :). I just wouldn't count on their index, but use a unique identifier for each object.
And updating the note would be const note = state.notes.findIndex(n => n.id === id); if (index > -1) { note.contents = contents }.
1

That's the classical problem of the Vuex state pattern when binding array values (or any complex values) directly into components. This is discussed in detail in the Vue forum (https://forum.vuejs.org/t/vuex-best-practices-for-complex-objects/10143).

Basically, the idea here is to only store an array with ID's. These ID's then reference to an object, where the object key is the ID and the object value is the actual data. If you break down your data structure like this, you are able to directly bind to the store. The main goal is to have flat structures in the store and only arrays with ID's in it.

In your example, you don't actually need the array, you can directly use an object. This would then look like this:

Store:

const state = {
  notes: {
    1: {
      contents: 'test'
    },
    2: {
      contents: 'hi, I\'m a note.'
    }
  }
};

To bind directly to the store, it is recommended to use computed properties with defined setters and getters. Here is an example:

props: {
  noteId: {
    type: Number,
    required: true
},
computed: {
  note: {
    get () {
      return this.$store.state.notes[this.noteId];
    },
    set (value) {
      this.$store.commit('setNote', {
        id: this.noteId,
        value: value
      });
    }
  }
}

This value (note) is now two-way bound to the Vuex store. It automatically refreshes the value in the store. If you are returning an object in the getter, you should probably first create a copy of this object (deep-clone), otherwise you are directly mutating that object.

There are many utility tools to map this two-way binding behavior into a component (see e.g. https://vuex.vuejs.org/guide/state.html#the-mapstate-helper).

3 Comments

Thanks for your answer. I'm not sure I could live with myself if I was forced to convert my array to an object - I feel like I'm losing a lot of syntactic sugar there. I'm very much trying to do things "the right way" with this project and this feels like an antipattern. Thanks for your suggestion, though.
As soon as you'll work with larger Vue/Vuex applications, you will see the benefit of this pattern. It is of course not an anti-pattern. The linked forum article about the data normalization was written by a core member of the Vue which recommended this as best practice for complex data structures. But at the end you'll have to decide about the best data structure for your application.
Looking at this from another angle, particularly taking @tao's advice into account, I apologise for dismissing this approach so quickly. It's possible that this method may be the more useful one. I'll have to try a few different things, including this one, and see what works - as well as try to predict what will work in the future.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.