Vue 2 - Accessoires mutants vue-warn


181

J'ai commencé la série https://laracasts.com/series/learning-vue-step-by-step . Je me suis arrêté sur la leçon Vue, Laravel et AJAX avec cette erreur:

vue.js: 2574 [Vue warn]: évitez de muter directement un accessoire car la valeur sera écrasée à chaque fois que le composant parent sera à nouveau rendu. Au lieu de cela, utilisez une donnée ou une propriété calculée en fonction de la valeur de l'accessoire. Prop en cours de mutation: "liste" (trouvée dans le composant)

J'ai ce code dans main.js

Vue.component('task', {
    template: '#task-template',
    props: ['list'],
    created() {
        this.list = JSON.parse(this.list);
    }
});
new Vue({
    el: '.container'
})

Je sais que le problème est dans created () lorsque j'écrase le prop de liste, mais je suis un débutant dans Vue, donc je ne sais absolument pas comment le résoudre. Quelqu'un a-t-il une idée de la façon (et expliquez pourquoi) de le résoudre?


2
Je suppose que c'est juste un message d'avertissement et non une erreur.
David R

Réponses:


264

Cela a à voir avec le fait que la mutation locale d'un accessoire est considérée comme un anti-motif dans Vue 2

Ce que vous devez faire maintenant, au cas où vous voudriez muter un accessoire localement , est de déclarer un champ dans votre dataqui utilise la propsvaleur comme valeur initiale, puis de muter la copie:

Vue.component('task', {
    template: '#task-template',
    props: ['list'],
    data: function () {
        return {
            mutableList: JSON.parse(this.list);
        }
    }
});

Vous pouvez en savoir plus à ce sujet sur le guide officiel de Vue.js


Remarque 1: Veuillez noter que vous ne devez pas utiliser le même nom pour votre propetdata , c'est-à-dire:

data: function () { return { list: JSON.parse(this.list) } // WRONG!!

Note 2: Puisqu'il me semble qu'il y a une certaine confusion concernant propset la réactivité , je vous suggère de jeter un œil sur ce fil


3
C'est une excellente réponse, mais je pense également qu'elle devrait être mise à jour pour inclure la liaison d'événements. Un événement déclenché par l'enfant peut mettre à jour le parent, qui transmettra les accessoires mis à jour à l'enfant. De cette façon, la source de la vérité est toujours maintenue dans tous les composants. Il peut également être intéressant de mentionner Vuex pour la gestion de l'état partagé entre plusieurs composants.
Wes Harper le

@WesHarper, il est également possible d'utiliser le modificateur .sync comme raccourci pour obtenir automatiquement l'événement de mise à jour du parent.
danb4r

48

Le modèle Vue est propsvers le bas et vers le eventshaut. Cela semble simple, mais il est facile de l'oublier lors de l'écriture d'un composant personnalisé.

Depuis Vue 2.2.0, vous pouvez utiliser v-model (avec des propriétés calculées ). J'ai trouvé que cette combinaison crée une interface simple, propre et cohérente entre les composants:

  • Tout ce qui est propspassé à votre composant reste réactif (c'est-à-dire qu'il n'est pas cloné et ne nécessite pas de watchfonction pour mettre à jour une copie locale lorsque des changements sont détectés).
  • Les modifications sont automatiquement envoyées au parent.
  • Peut être utilisé avec plusieurs niveaux de composants.

Une propriété calculée permet au setter et au getter d'être définis séparément. Cela permet au Taskcomposant d'être réécrit comme suit:

Vue.component('Task', {
    template: '#task-template',
    props: ['list'],
    model: {
        prop: 'list',
        event: 'listchange'
    },
    computed: {
        listLocal: {
            get: function() {
                return this.list
            },
            set: function(value) {
                this.$emit('listchange', value)
            }
        }
    }
})  

La propriété model définit ce qui propest associé v-modelet quel événement sera émis lors des modifications. Vous pouvez ensuite appeler ce composant depuis le parent comme suit:

<Task v-model="parentList"></Task>

La listLocalpropriété calculée fournit une interface de lecture et de définition simple dans le composant (pensez-y comme étant une variable privée). À l'intérieur, #task-templatevous pouvez effectuer le rendu listLocalet il restera réactif (c'est-à-dire que si des parentListmodifications sont apportées, le Taskcomposant sera mis à jour ). Vous pouvez également muter listLocalen appelant le setter (par exemple, this.listLocal = newList) et il émettra le changement au parent.

Ce qui est génial avec ce modèle, c'est que vous pouvez passer listLocalà un composant enfant de Task(using v-model), et les modifications du composant enfant se propageront au composant de niveau supérieur.

Par exemple, disons que nous avons un EditTaskcomposant séparé pour effectuer un type de modification sur les données de la tâche. En utilisant le même v-modelmodèle de propriétés calculées, nous pouvons passer listLocalau composant (en utilisant v-model):

<script type="text/x-template" id="task-template">
    <div>
        <EditTask v-model="listLocal"></EditTask>
    </div>
</script>

Si EditTaskémet un changement , il appellera de façon appropriée set()sur listLocalet ainsi propager l'événement au niveau supérieur. De même, le EditTaskcomposant peut également appeler d'autres composants enfants (tels que des éléments de formulaire) à l'aide de v-model.


1
Je fais quelque chose de similaire sauf avec la synchronisation. Le problème est que je dois exécuter une autre méthode après avoir émis mon événement de modification, mais lorsque la méthode s'exécute et atteint le getter, j'obtiens l'ancienne valeur car l'événement de modification n'a pas encore été capté par l'auditeur / propagé à l'enfant. C'est un cas où je veux muter l'accessoire afin que mes données locales soient correctes jusqu'à ce que la mise à jour parente se propage. Des pensées à ce sujet?
koga73 le

Je soupçonne que d'appeler le getter du setter n'est pas une bonne pratique. Ce que vous pourriez envisager, c'est de surveiller votre propriété calculée et d'y ajouter votre logique.
chris le

les accessoires vers le bas et les événements sont ce qui a cliqué avec moi. Où cela est-il expliqué dans la documentation de VueJs?
nebulousGirl


Je pense que c'est la manière la plus indolore d'émettre les modifications apportées au composant parental.
Muhammad

36

Vue vous avertit simplement: vous modifiez l'accessoire dans le composant, mais lorsque le composant parent se re-rend, "list" sera écrasé et vous perdrez toutes vos modifications. Il est donc dangereux de le faire.

Utilisez plutôt la propriété calculée comme ceci:

Vue.component('task', {
    template: '#task-template',
    props: ['list'],
    computed: {
        listJson: function(){
            return JSON.parse(this.list);
        }
    }
});

25
Qu'en est-il d'un accessoire de liaison à 2 voies?
Josh R.

7
Si vous voulez une liaison de données bidirectionnelle, vous devez utiliser des événements personnalisés, pour plus d'informations, lisez: vuejs.org/v2/guide/components.html#sync-Modifier Vous ne pouvez toujours pas modifier directement le prop, vous avez besoin d'un événement et d'une fonction qui gère un changement d'accessoire pour vous.
Marco

22

Si vous utilisez Lodash, vous pouvez cloner l'accessoire avant de le retourner. Ce modèle est utile si vous modifiez cet accessoire sur le parent et l'enfant.

Disons que nous avons une liste d' accessoires sur la grille des composants .

Dans le composant parent

<grid :list.sync="list"></grid>

Dans le composant enfant

props: ['list'],
methods:{
    doSomethingOnClick(entry){
        let modifiedList = _.clone(this.list)
        modifiedList = _.uniq(modifiedList) // Removes duplicates
        this.$emit('update:list', modifiedList)
    }
}

19

Props down, events up. C'est le modèle de Vue. Le fait est que si vous essayez de faire muter les accessoires en passant d'un parent. Cela ne fonctionnera pas et il est simplement écrasé à plusieurs reprises par le composant parent. Le composant enfant peut uniquement émettre un événement pour avertir le composant parent de faire qc. Si vous n'aimez pas ces restrictions, vous pouvez utiliser VUEX (en fait, ce modèle aspirera dans une structure de composants complexes, vous devriez utiliser VUEX!)


2
Il y a aussi une option pour utiliser le bus événementiel
Steven Pribilinskiy

le bus d'événements n'est pas pris en charge de manière native dans la vue 3. github.com/vuejs/rfcs/pull/118
AlexMA

15

Vous ne devez pas modifier la valeur des accessoires dans le composant enfant. Si vous avez vraiment besoin de le changer, vous pouvez utiliser .sync. Juste comme ça

<your-component :list.sync="list"></your-component>

Vue.component('task', {
    template: '#task-template',
    props: ['list'],
    created() {
        this.$emit('update:list', JSON.parse(this.list))
    }
});
new Vue({
    el: '.container'
})

7

Selon VueJs 2.0, vous ne devez pas muter un accessoire à l'intérieur du composant. Ils ne sont mutés que par leurs parents. Par conséquent, vous devez définir des variables dans les données avec des noms différents et les maintenir à jour en regardant les accessoires réels. Si le prop de liste est modifié par un parent, vous pouvez l'analyser et l'affecter à mutableList. Voici une solution complète.

Vue.component('task', {
    template: ´<ul>
                  <li v-for="item in mutableList">
                      {{item.name}}
                  </li>
              </ul>´,
    props: ['list'],
    data: function () {
        return {
            mutableList = JSON.parse(this.list);
        }
    },
    watch:{
        list: function(){
            this.mutableList = JSON.parse(this.list);
        }
    }
});

Il utilise mutableList pour rendre votre modèle, ainsi vous gardez votre prop de liste en sécurité dans le composant.


la montre n'est-elle pas chère à utiliser?
Kick Buttowski

6

ne modifiez pas les accessoires directement dans les composants.Si vous avez besoin de le modifier, définissez une nouvelle propriété comme celle-ci:

data () {
    return () {
        listClone: this.list
    }
}

Et changez la valeur de listClone.


6

La réponse est simple, vous devez briser la mutation prop directe en attribuant la valeur à certaines variables de composant local (il peut s'agir d'une propriété de données, calculée avec des getters, des setters ou des observateurs).

Voici une solution simple utilisant l'observateur.

<template>
  <input
    v-model="input"
    @input="updateInput" 
    @change="updateInput"
  />

</template>

<script>
  export default {
  props: {
    value: {
      type: String,
      default: '',
    },
  },
  data() {
    return {
      input: '',
    };
  },
  watch: {
    value: {
      handler(after) {
        this.input = after;
      },
      immediate: true,
    },
  },
  methods: {
    updateInput() {
      this.$emit('input', this.input);
    },
  },
};
</script>

C'est ce que j'utilise pour créer des composants d'entrée de données et cela fonctionne très bien. Toutes les nouvelles données envoyées (v-model (ed)) du parent seront surveillées par l'observateur de valeur et affectées à la variable d'entrée et une fois que l'entrée est reçue, nous pouvons attraper cette action et émettre une entrée au parent suggérant que les données sont entrées à partir de l'élément de formulaire.


5

J'ai également fait face à ce problème. L'avertissement est parti après avoir utilisé $onet $emit. C'est quelque chose comme l'utilisation $onet $emitrecommandé d'envoyer des données du composant enfant au composant parent.


4

Si vous voulez faire muter les accessoires - utilisez object.

<component :model="global.price"></component>

composant:

props: ['model'],
methods: {
  changeValue: function() {
    this.model.value = "new value";
  }
}

Juste une note, vous devez être prudent lors de la mutation des objets. Les objets Javascript sont passés par référence, mais il y a une mise en garde: la référence est rompue lorsque vous définissez une variable égale à une valeur.
ira

3

Vous devez ajouter une méthode calculée comme celle-ci

component.vue

props: ['list'],
computed: {
    listJson: function(){
        return JSON.parse(this.list);
    }
}

3

Flux de données unidirectionnel, selon https://vuejs.org/v2/guide/components.html , le composant suit un flux de données unidirectionnel, tous les accessoires forment une liaison unidirectionnelle entre la propriété enfant et le parent Premièrement, lorsque la propriété parent est mise à jour, elle descendra vers l'enfant mais pas l'inverse, cela empêche les composants enfants de muter accidentellement ceux du parent, ce qui peut rendre le flux de données de votre application plus difficile à comprendre.

De plus, chaque fois que le composant parent est mis à jour, tous les accessoires des composants enfants seront actualisés avec la dernière valeur. Cela signifie que vous ne devez pas tenter de muter un accessoire dans un composant enfant. Si vous le faites, .vue vous en avertira dans la console.

Il y a généralement deux cas où il est tentant de muter un accessoire: Le prop est utilisé pour passer une valeur initiale; le composant enfant veut ensuite l'utiliser comme propriété de données locale. L'accessoire est transmis en tant que valeur brute qui doit être transformée. La bonne réponse à ces cas d'utilisation est: Définissez une propriété de données locales qui utilise la valeur initiale de l'accessoire comme valeur initiale:

props: ['initialCounter'],
data: function () {
  return { counter: this.initialCounter }
}

Définissez une propriété calculée qui est calculée à partir de la valeur de l'accessoire:

props: ['size'],
computed: {
  normalizedSize: function () {
    return this.size.trim().toLowerCase()
  }
}

2

Les accessoires Vue.js ne doivent pas être mutés car cela est considéré comme un Anti-Pattern dans Vue.

L'approche que vous devrez adopter consiste à créer une propriété de données sur votre composant qui référence la propriété prop d' origine de list

props: ['list'],
data: () {
  return {
    parsedList: JSON.parse(this.list)
  }
}

Maintenant, la structure de votre liste qui est passée au composant est référencée et mutée via la datapropriété de votre composant :-)

Si vous souhaitez faire plus que juste parse votre propriété de liste , puis utiliser la composante Vue computedpropriété. Cela vous permet de faire des mutations plus profondes sur vos accessoires.

props: ['list'],
computed: {
  filteredJSONList: () => {
    let parsedList = JSON.parse(this.list)
    let filteredList = parsedList.filter(listItem => listItem.active)
    console.log(filteredList)
    return filteredList
  }
}

L'exemple ci-dessus analyse votre prop de liste et le filtre aux seuls éléments de liste actifs , le déconnecte pour les schnitts et les rires et le renvoie.

Note : les deux dataet computedpropriétés sont référencées dans le modèle même par exemple

<pre>{{parsedList}}</pre>

<pre>{{filteredJSONList}}</pre>

Il peut être facile de penser qu'une computedpropriété (étant une méthode) doit être appelée ... ce n'est pas le cas


2
Vue.component('task', {
    template: '#task-template',
    props: ['list'],
    computed: {
      middleData() {
        return this.list
      }
    },
    watch: {
      list(newVal, oldVal) {
        console.log(newVal)
        this.newList = newVal
      }
    },
    data() {
      return {
        newList: {}
      }
    }
});
new Vue({
    el: '.container'
})

Peut-être que cela répondra à vos besoins.


2

Ajout de la meilleure réponse,

Vue.component('task', {
    template: '#task-template',
    props: ['list'],
    data: function () {
        return {
            mutableList: JSON.parse(this.list);
        }
    }
});

La définition des props par un tableau est destinée au développement / prototypage, en production, assurez-vous de définir les types de prop ( https://vuejs.org/v2/guide/components-props.html ) et définissez une valeur par défaut au cas où la prop ne l'aurait pas été rempli par le parent, comme tel.

Vue.component('task', {
    template: '#task-template',
    props: {
      list: {
        type: String,
        default() {
          return '{}'
        }
      }
    },
    data: function () {
        return {
            mutableList: JSON.parse(this.list);
        }
    }
});

De cette façon, vous obtenez au moins un objet vide au mutableListlieu d'une erreur JSON.parse s'il n'est pas défini.


1

Vous trouverez ci-dessous un composant de snack-bar, lorsque je donne la variable snackbar directement dans le v-model comme celui-ci si cela fonctionnera mais dans la console, cela donnera une erreur comme

Évitez de muter directement un accessoire car la valeur sera écrasée chaque fois que le composant parent sera à nouveau rendu. Au lieu de cela, utilisez une donnée ou une propriété calculée en fonction de la valeur de l'accessoire.

<template>
        <v-snackbar v-model="snackbar">
        {{ text }}
      </v-snackbar>
</template>

<script>
    export default {
        name: "loader",

        props: {
            snackbar: {type: Boolean, required: true},
            text: {type: String, required: false, default: ""},
        },

    }
</script>

La bonne façon de se débarrasser de cette erreur de mutation est d'utiliser watcher

<template>
        <v-snackbar v-model="snackbarData">
        {{ text }}
      </v-snackbar>
</template>

<script>
/* eslint-disable */ 
    export default {
        name: "loader",
         data: () => ({
          snackbarData:false,
        }),
        props: {
            snackbar: {type: Boolean, required: true},
            text: {type: String, required: false, default: ""},
        },
        watch: { 
        snackbar: function(newVal, oldVal) { 
          this.snackbarData=!this.snackbarDatanewVal;
        }
      }
    }
</script>

Donc, dans le composant principal où vous allez charger ce snack, vous pouvez simplement faire ce code

 <loader :snackbar="snackbarFlag" :text="snackText"></loader>

Cela a fonctionné pour moi


0

Vue.js considère cela comme un anti-pattern. Par exemple, déclarer et définir des accessoires comme

this.propsVal = 'new Props Value'

Donc, pour résoudre ce problème, vous devez prendre une valeur des accessoires aux données ou à la propriété calculée d'une instance de Vue, comme ceci:

props: ['propsVal'],
data: function() {
   return {
       propVal: this.propsVal
   };
},
methods: {
...
}

Cela fonctionnera certainement.


0

En plus de ce qui précède, pour les autres ayant le problème suivant:

"Si la valeur des accessoires n'est pas requise et n'est donc pas toujours renvoyée, les données passées retourneront undefined(au lieu d'être vides)". Ce qui pourrait perturber la <select>valeur par défaut, je l'ai résolu en vérifiant si la valeur est définie beforeMount()(et en la définissant sinon) comme suit:

JS:

export default {
        name: 'user_register',
        data: () => ({
            oldDobMonthMutated: this.oldDobMonth,
        }),
        props: [
            'oldDobMonth',
            'dobMonths', //Used for the select loop
        ],
        beforeMount() {
           if (!this.oldDobMonth) {
              this.oldDobMonthMutated = '';
           } else {
              this.oldDobMonthMutated = this.oldDobMonth
           }
        }
}

Html:

<select v-model="oldDobMonthMutated" id="dob_months" name="dob_month">

 <option selected="selected" disabled="disabled" hidden="hidden" value="">
 Select Month
 </option>

 <option v-for="dobMonth in dobMonths"
  :key="dobMonth.dob_month_slug"
  :value="dobMonth.dob_month_slug">
  {{ dobMonth.dob_month_name }}
 </option>

</select>

0

Je veux donner cette réponse qui évite d'utiliser beaucoup de code, d'observateurs et de propriétés calculées. Dans certains cas, cela peut être une bonne solution:

Les accessoires sont conçus pour fournir une communication unidirectionnelle.

Lorsque vous avez un show/hidebouton modal avec un accessoire, la meilleure solution pour moi est d'émettre un événement:

<button @click="$emit('close')">Close Modal</button>

Ensuite, ajoutez un écouteur à l'élément modal:

<modal :show="show" @close="show = false"></modal>

(Dans ce cas, l'accessoire showest probablement inutile car vous pouvez utiliser un easy v-if="show"directement sur la base-modal)


0

Personnellement, je suggère toujours que si vous avez besoin de muter les accessoires, passez-les d'abord à la propriété calculée et revenez de là, par la suite, on peut muter facilement les accessoires, même si vous pouvez suivre la mutation de l'accessoire, si ceux-ci sont mutés d'un autre composant aussi ou nous pouvons vous regarder aussi.


0

Étant donné que les accessoires Vue sont un flux de données à sens unique, cela empêche les composants enfants de muter accidentellement l'état du parent.

À partir du document officiel Vue, nous trouverons 2 façons de résoudre ces problèmes

  1. si le composant enfant souhaite utiliser des accessoires comme données locales, il est préférable de définir une propriété de données locales.

      props: ['list'],
      data: function() {
        return {
          localList: JSON.parse(this.list);
        }
      }
    
  2. L'accessoire est transmis en tant que valeur brute qui doit être transformée. Dans ce cas, il est préférable de définir une propriété calculée en utilisant la valeur du prop:

      props: ['list'],
      computed: {
        localList: function() {
           return JSON.parse(this.list);
        },
        //eg: if you want to filter this list
        validList: function() {
           return this.list.filter(product => product.isValid === true)
        }
        //...whatever to transform the list
      }
    
    

0

OUI !, la mutation des attributs dans vue2 est anti-pattern. MAIS ... enfreignez simplement les règles en utilisant d'autres règles, et allez de l'avant! Ce dont vous avez besoin est d'ajouter le modificateur .sync à votre attribut de composant dans la portée de paret. <your-awesome-components :custom-attribute-as-prob.sync="value" />

🤡 C'est simple, nous tuons le batman 😁


0

Pour lorsque TypeScript est votre langue préférée. de développement

<template>
<span class="someClassName">
      {{feesInLocale}}
</span>
</template>  



@Prop({default: 0}) fees: any;

// computed are declared with get before a function
get feesInLocale() {
    return this.fees;
}

et pas

<template>
<span class="someClassName">
      {{feesInLocale}}
</span>
</template>  



@Prop() fees: any = 0;
get feesInLocale() {
    return this.fees;
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.