How to "Avoid mutating a prop directly" in a recursive component

Viewed 676

I understand the concept: the child works on its own copy of the prop data, and when it's changed it it can $emit that change so the parent can update itself.

However, I'm dealing with a recursive tree structure, e.g. a filesystem, is a good analogy.

[
 { type: 'dir', name: 'music', children: [
    { type: 'dir', name: 'genres', children: [
       { type: 'dir', name: 'triphop', children: [
          { type: 'file', name: 'Portishead' },
          ...
       ]},
     ]}
   ]}
]}

I have a recursive component called Thing that takes a childtree prop and looks a bit like this:

<template>
  <input v-model="tree.name" />
  <thing v-for="childtree in tree.children" :tree="childtree" ></thing>
</template>

Modifying a name is obviously going to modify the prop directly, which is to be avoided, and Vue emits a warning about it.

However, the only way I can see to avoid that would be for each component to do a deep copy of childtree; then $emit a copy of our copy and have a @input (or such) copy it back to the original; all the way up the tree, which would then change the props all the way down the tree causing another deep copy of everything!

That feels like it's going to be really inefficient on a tree of any size.

Is there a better way? I know you can trick Vue into not issuing the error/warning example jsfiddle.

3 Answers

Fix for your problem is passing just event from the child component. https://jsfiddle.net/kv1w72pg/2/

<input @input="(e) => $emit('input', e.target.value)" />

That being said I highly suggest you use other solution such as:

  1. Vuex,
  2. Event bus

It will make code clear and ensure one source of truth.

Edit: In the case of recursive inheritance, using provide inject would be easier: https://jsfiddle.net/s0b9fpr8/4/

This way you provide a function that can change the state of base component to all children components (function must be injected).

One-Way Data Flow

Every time the parent component is updated, all props in the child component will be refreshed with the latest value. This means you should not attempt to mutate a prop inside a child component. If you do, Vue will warn you in the console.

Note that objects and arrays in JavaScript are passed by reference, so if the prop is an array or object, mutating the object or array itself inside the child component will affect parent state.

Here is an example with lodash

when we just clone.

var objects = [{ 'a': 1 }, { 'b': 2 }];

var shallow = _.clone(objects);
console.log(shallow[0] === objects[0]);
// => true

When we use cloneDeep

var objects = [{ 'a': 1 }, { 'b': 2 }];

var deep = _.cloneDeep(objects);
console.log(deep[0] === objects[0]);
// => false

So, if you want to update any props value in your child component, you need to use cloneDeep of lodash.

Here's roughly how I've handled this:

<template>
  <div>
    <input v-model="myName" @input="updateParent()" />
    <thing v-for="(child, i) in myChildren" :key="child.uniqueId"
     @input="setChild(i, $event)"
     >
    </thing>
  </div>
</template>
<script>
export default {
props: ['node'],
data() {
  return {
     myName: this.node.name,
     myChildren: this.node.children.slice(0),
  };
},
methods: {
  updateParent() {
    this.$emit('input', {name: this.myName, children: this.myChildren});
  },
  setchild(i, newChild) {
    this.$set(this.myChildren, i, newChild);
  }
}
}
</script>

This means each node of the tree:

  • works on it's own copy of 'name'
  • holds its own array of children (even though the children are 'real' - they're references belonging to the object that is not 'ours' to change, but the array is ours).
  • tells the parent to replace it when there's an update.
  • listens for child nodes that have been updated.

This seems to work and seems to avoid mutating props. One thing not shown here, but I found was crucial, was setting a unique ID on each child. I did this by creating Ids sequentially from a global counter (on $root).

Related