Gestion de Formulaires avec Vuex : Mutations, Getters et Actions

Maîtrisez la gestion des formulaires avec Vuex. Créez des mutations pour chaque champ, des getters pour l'état et des actions pour les API.

Mahmoud DEVO
Mahmoud DEVO
December 27, 2025 11 min read
Gestion de Formulaires avec Vuex : Mutations, Getters et Actions
Table of Contents

Gérer les Formulaires avec Vuex : Une Approche Structurée

La gestion des formulaires représente l’un des défis les plus courants dans le développement d’applications Vue.js. Lorsque les formulaires deviennent complexes, avec de nombreux champs interdépendants et des validations sophistiquées, une approche structurée devient indispensable. Vuex offre une solution élégante pour centraliser et orchestrer l’état des formulaires.


Introduction à la Gestion d’État des Formulaires

Pourquoi Centraliser l’État des Formulaires ?

Dans une application Vue.js classique, chaque composant gère son propre état local avec data(). Cette approche fonctionne bien pour des formulaires simples, mais présente des limitations significatives :

  • Partage de données complexe : Passer des données entre composants frères nécessite de remonter l’état au parent commun
  • Synchronisation difficile : Maintenir la cohérence entre plusieurs composants qui utilisent les mêmes données
  • Debugging laborieux : Tracer les modifications d’état à travers de multiples composants
  • Tests unitaires complexes : Isoler la logique métier du rendu visuel

Vuex résout ces problèmes en fournissant un store centralisé qui agit comme source unique de vérité pour l’état de l’application.

Architecture d’un Formulaire Vuex

Voici l’architecture typique d’un formulaire géré avec Vuex :

┌─────────────────────────────────────────────────────────┐
│                     COMPOSANT VUE                        │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐  │
│  │   v-model   │    │   computed  │    │   methods   │  │
│  │  (get/set)  │    │  (getters)  │    │  (actions)  │  │
│  └──────┬──────┘    └──────┬──────┘    └──────┬──────┘  │
└─────────┼──────────────────┼──────────────────┼─────────┘
          │                  │                  │
          ▼                  ▼                  ▼
┌─────────────────────────────────────────────────────────┐
│                      VUEX STORE                          │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐  │
│  │  mutations  │◄───│    state    │───►│   getters   │  │
│  └──────┬──────┘    └─────────────┘    └─────────────┘  │
│         │                                               │
│         ▼                                               │
│  ┌─────────────┐                                        │
│  │   actions   │──────► API / Services externes         │
│  └─────────────┘                                        │
└─────────────────────────────────────────────────────────┘

Structure du State pour les Formulaires

Définir un State Bien Organisé

La première étape consiste à définir la structure du state. Une bonne pratique consiste à regrouper les champs du formulaire dans un objet dédié :

// store/index.js
import { createStore } from 'vuex'

const store = createStore({
  state() {
    return {
      // Champs du formulaire regroupés
      fields: {
        newItem: '',
        email: '',
        urgency: '',
        termsAndConditions: false,
        // Champs additionnels
        firstName: '',
        lastName: '',
        phone: '',
        address: {
          street: '',
          city: '',
          postalCode: '',
          country: ''
        }
      },
      // Données persistées
      items: [],
      // État de l'interface
      ui: {
        isSubmitting: false,
        submitError: null,
        submitSuccess: false
      },
      // Erreurs de validation
      errors: {
        newItem: [],
        email: [],
        urgency: [],
        termsAndConditions: []
      }
    }
  }
})

export default store

Avantages de cette Structure

Cette organisation offre plusieurs avantages :

AspectBénéfice
Séparation claireLes champs du formulaire sont isolés des autres données
Reset facileRéinitialiser le formulaire revient à remplacer fields
Validation centraliséeLes erreurs sont stockées au même endroit
État UI dédiéGestion du loading et des erreurs sans polluer les données

Les Mutations : Modifier l’État de Manière Prévisible

Principe des Mutations

Les mutations sont les seules fonctions autorisées à modifier le state dans Vuex. Elles doivent être :

  • Synchrones : Pas d’opérations asynchrones
  • Pures : Même entrée = même sortie
  • Traçables : Chaque mutation est enregistrée dans les DevTools

Créer une Mutation par Champ

Pour un contrôle granulaire, créez une mutation dédiée à chaque champ :

// store/mutations.js
export const mutations = {
  // Mutations pour les champs principaux
  UPDATE_NEW_ITEM(state, payload) {
    state.fields.newItem = payload
  },

  UPDATE_EMAIL(state, payload) {
    state.fields.email = payload
  },

  UPDATE_URGENCY(state, payload) {
    state.fields.urgency = payload
  },

  UPDATE_TERMS_AND_CONDITIONS(state, payload) {
    state.fields.termsAndConditions = payload
  },

  // Mutations pour les champs d'adresse
  UPDATE_FIRST_NAME(state, payload) {
    state.fields.firstName = payload
  },

  UPDATE_LAST_NAME(state, payload) {
    state.fields.lastName = payload
  },

  UPDATE_PHONE(state, payload) {
    state.fields.phone = payload
  },

  UPDATE_ADDRESS_STREET(state, payload) {
    state.fields.address.street = payload
  },

  UPDATE_ADDRESS_CITY(state, payload) {
    state.fields.address.city = payload
  },

  UPDATE_ADDRESS_POSTAL_CODE(state, payload) {
    state.fields.address.postalCode = payload
  },

  UPDATE_ADDRESS_COUNTRY(state, payload) {
    state.fields.address.country = payload
  },

  // Mutation pour les items
  UPDATE_ITEMS(state, payload) {
    state.items = payload
  },

  // Mutation pour ajouter un item
  ADD_ITEM(state, item) {
    state.items.push(item)
  },

  // Mutation pour supprimer un item
  REMOVE_ITEM(state, index) {
    state.items.splice(index, 1)
  },

  // Réinitialisation complète du formulaire
  CLEAR_FIELDS(state) {
    state.fields.newItem = ''
    state.fields.email = ''
    state.fields.urgency = ''
    state.fields.termsAndConditions = false
    state.fields.firstName = ''
    state.fields.lastName = ''
    state.fields.phone = ''
    state.fields.address = {
      street: '',
      city: '',
      postalCode: '',
      country: ''
    }
  },

  // Mutations pour l'état UI
  SET_SUBMITTING(state, isSubmitting) {
    state.ui.isSubmitting = isSubmitting
  },

  SET_SUBMIT_ERROR(state, error) {
    state.ui.submitError = error
  },

  SET_SUBMIT_SUCCESS(state, success) {
    state.ui.submitSuccess = success
  },

  // Mutations pour les erreurs de validation
  SET_FIELD_ERROR(state, { field, errors }) {
    state.errors[field] = errors
  },

  CLEAR_ALL_ERRORS(state) {
    Object.keys(state.errors).forEach(key => {
      state.errors[key] = []
    })
  }
}

Mutation Générique pour les Formulaires

Pour éviter la répétition, vous pouvez créer une mutation générique :

// Mutation générique
UPDATE_FIELD(state, { field, value }) {
  // Gestion des champs imbriqués avec notation pointée
  const keys = field.split('.')
  let obj = state.fields

  for (let i = 0; i < keys.length - 1; i++) {
    obj = obj[keys[i]]
  }

  obj[keys[keys.length - 1]] = value
}

// Utilisation
this.$store.commit('UPDATE_FIELD', { field: 'email', value: 'test@example.com' })
this.$store.commit('UPDATE_FIELD', { field: 'address.city', value: 'Paris' })

Les Getters : Accéder et Transformer l’État

Rôle des Getters

Les getters permettent de :

  • Accéder aux données du state de manière encapsulée
  • Calculer des valeurs dérivées
  • Valider les données en temps réel
  • Filtrer et transformer les données

Getters pour un Formulaire Complet

// store/getters.js
export const getters = {
  // Accès direct aux champs
  newItem: state => state.fields.newItem,
  email: state => state.fields.email,
  urgency: state => state.fields.urgency,
  termsAndConditions: state => state.fields.termsAndConditions,
  firstName: state => state.fields.firstName,
  lastName: state => state.fields.lastName,
  phone: state => state.fields.phone,
  address: state => state.fields.address,

  // Getters calculés
  newItemLength: state => state.fields.newItem.length,

  fullName: state => {
    const { firstName, lastName } = state.fields
    return `${firstName} ${lastName}`.trim()
  },

  fullAddress: state => {
    const { street, city, postalCode, country } = state.fields.address
    return [street, city, postalCode, country]
      .filter(Boolean)
      .join(', ')
  },

  // Getters de validation
  isNewItemInputLimitExceeded: state => state.fields.newItem.length >= 20,

  isEmailValid: state => {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    return emailRegex.test(state.fields.email)
  },

  isPhoneValid: state => {
    const phoneRegex = /^(\+33|0)[1-9](\d{2}){4}$/
    return state.fields.phone === '' || phoneRegex.test(state.fields.phone)
  },

  isNotUrgent: state => state.fields.urgency === 'Nonessential',

  isFormValid: (state, getters) => {
    return (
      state.fields.newItem.length > 0 &&
      state.fields.newItem.length < 20 &&
      getters.isEmailValid &&
      state.fields.urgency !== '' &&
      state.fields.termsAndConditions === true
    )
  },

  // Getters pour les erreurs
  hasErrors: state => {
    return Object.values(state.errors).some(errors => errors.length > 0)
  },

  fieldErrors: state => field => state.errors[field] || [],

  // Getters pour l'UI
  isSubmitting: state => state.ui.isSubmitting,
  submitError: state => state.ui.submitError,
  submitSuccess: state => state.ui.submitSuccess,

  // Getters pour les items
  items: state => state.items,
  itemCount: state => state.items.length,

  urgentItems: state => {
    return state.items.filter(item => item.urgency !== 'Nonessential')
  },

  itemsByUrgency: state => {
    return state.items.reduce((acc, item) => {
      const key = item.urgency || 'Non défini'
      if (!acc[key]) acc[key] = []
      acc[key].push(item)
      return acc
    }, {})
  }
}

Les Actions : Logique Asynchrone et Appels API

Quand Utiliser les Actions

Les actions sont conçues pour :

  • Opérations asynchrones : Appels API, timeouts, etc.
  • Logique métier complexe : Validation côté serveur, calculs
  • Enchaînement de mutations : Orchestrer plusieurs modifications

Actions pour la Gestion de Formulaires

// store/actions.js
import apiClient from '@/services/apiClient'

export const actions = {
  // Charger les items depuis l'API
  async loadItems({ commit }) {
    commit('SET_SUBMITTING', true)
    commit('SET_SUBMIT_ERROR', null)

    try {
      const items = await apiClient.loadItems()
      commit('UPDATE_ITEMS', items)
    } catch (error) {
      commit('SET_SUBMIT_ERROR', 'Erreur lors du chargement des données')
      console.error('loadItems error:', error)
    } finally {
      commit('SET_SUBMITTING', false)
    }
  },

  // Sauvegarder les items
  async saveItems({ commit, state }) {
    commit('SET_SUBMITTING', true)
    commit('SET_SUBMIT_ERROR', null)
    commit('SET_SUBMIT_SUCCESS', false)

    try {
      const newItem = {
        ...state.fields,
        id: Date.now(),
        createdAt: new Date().toISOString()
      }

      await apiClient.saveItem(newItem)

      commit('ADD_ITEM', newItem)
      commit('CLEAR_FIELDS')
      commit('SET_SUBMIT_SUCCESS', true)

      // Reset du message de succès après 3 secondes
      setTimeout(() => {
        commit('SET_SUBMIT_SUCCESS', false)
      }, 3000)

    } catch (error) {
      commit('SET_SUBMIT_ERROR', error.message || 'Erreur lors de la sauvegarde')
      console.error('saveItems error:', error)
    } finally {
      commit('SET_SUBMITTING', false)
    }
  },

  // Supprimer un item
  async deleteItem({ commit }, { id, index }) {
    commit('SET_SUBMITTING', true)

    try {
      await apiClient.deleteItem(id)
      commit('REMOVE_ITEM', index)
    } catch (error) {
      commit('SET_SUBMIT_ERROR', 'Erreur lors de la suppression')
    } finally {
      commit('SET_SUBMITTING', false)
    }
  },

  // Validation côté serveur
  async validateField({ commit }, { field, value }) {
    try {
      const errors = await apiClient.validateField(field, value)
      commit('SET_FIELD_ERROR', { field, errors })
      return errors.length === 0
    } catch (error) {
      console.error('Validation error:', error)
      return false
    }
  },

  // Soumettre le formulaire avec validation complète
  async submitForm({ dispatch, getters, commit }) {
    commit('CLEAR_ALL_ERRORS')

    // Validation locale
    if (!getters.isFormValid) {
      commit('SET_SUBMIT_ERROR', 'Veuillez remplir tous les champs requis')
      return false
    }

    // Validation serveur (optionnel)
    const emailValid = await dispatch('validateField', {
      field: 'email',
      value: getters.email
    })

    if (!emailValid) {
      return false
    }

    // Sauvegarde
    await dispatch('saveItems')
    return true
  },

  // Réinitialiser le formulaire
  resetForm({ commit }) {
    commit('CLEAR_FIELDS')
    commit('CLEAR_ALL_ERRORS')
    commit('SET_SUBMIT_ERROR', null)
    commit('SET_SUBMIT_SUCCESS', false)
  }
}

Utiliser mapMutations et mapGetters dans les Composants

Les Helpers Vuex

Vuex fournit des helpers pour simplifier l’intégration dans les composants :

import { mapGetters, mapMutations, mapActions } from 'vuex'

export default {
  computed: {
    // Mapper les getters comme propriétés computed
    ...mapGetters([
      'newItem',
      'newItemLength',
      'isNewItemInputLimitExceeded',
      'email',
      'urgency',
      'isNotUrgent',
      'termsAndConditions',
      'items',
      'isFormValid',
      'isSubmitting',
      'submitError',
      'submitSuccess'
    ]),

    // Renommer si nécessaire
    ...mapGetters({
      formIsValid: 'isFormValid',
      currentItems: 'items'
    })
  },

  methods: {
    // Mapper les mutations comme méthodes
    ...mapMutations([
      'UPDATE_NEW_ITEM',
      'UPDATE_EMAIL',
      'UPDATE_URGENCY',
      'UPDATE_TERMS_AND_CONDITIONS',
      'CLEAR_FIELDS'
    ]),

    // Mapper les actions
    ...mapActions([
      'loadItems',
      'saveItems',
      'submitForm',
      'resetForm'
    ]),

    // Gestionnaire d'événements générique
    onInputChange(evt) {
      const element = evt.target
      const value = element.type === 'checkbox'
        ? element.checked
        : element.value

      this.$store.commit(`UPDATE_${element.name}`, value)
    }
  }
}

v-model avec Vuex : Pattern Computed Get/Set

Le Problème du v-model Direct

Utiliser v-model directement avec le state Vuex génère une erreur car cela modifie le state en dehors d’une mutation :

<!-- NE FONCTIONNE PAS -->
<input v-model="$store.state.fields.email" />

Solution : Propriétés Computed avec Get/Set

La solution recommandée consiste à créer des propriétés computed avec getter et setter :

<template>
  <form @submit.prevent="handleSubmit">
    <!-- Input text avec v-model -->
    <div class="form-group">
      <label for="newItem">Nouvel élément</label>
      <input
        id="newItem"
        v-model="newItemModel"
        type="text"
        :class="{ 'is-invalid': isNewItemInputLimitExceeded }"
      />
      <small>{{ newItemLength }}/20 caractères</small>
    </div>

    <!-- Input email -->
    <div class="form-group">
      <label for="email">Email</label>
      <input
        id="email"
        v-model="emailModel"
        type="email"
        :class="{ 'is-invalid': !isEmailValid && email }"
      />
    </div>

    <!-- Select -->
    <div class="form-group">
      <label for="urgency">Urgence</label>
      <select id="urgency" v-model="urgencyModel">
        <option value="">Sélectionnez...</option>
        <option value="Urgent">Urgent</option>
        <option value="Important">Important</option>
        <option value="Nonessential">Non essentiel</option>
      </select>
    </div>

    <!-- Checkbox -->
    <div class="form-group">
      <label>
        <input
          v-model="termsModel"
          type="checkbox"
        />
        J'accepte les conditions d'utilisation
      </label>
    </div>

    <!-- Boutons -->
    <div class="form-actions">
      <button type="submit" :disabled="!isFormValid || isSubmitting">
        {{ isSubmitting ? 'Envoi en cours...' : 'Soumettre' }}
      </button>
      <button type="button" @click="resetForm">
        Réinitialiser
      </button>
    </div>

    <!-- Messages -->
    <div v-if="submitError" class="error-message">
      {{ submitError }}
    </div>
    <div v-if="submitSuccess" class="success-message">
      Formulaire envoyé avec succès !
    </div>
  </form>
</template>

<script>
import { mapGetters, mapActions } from 'vuex'

export default {
  name: 'FormComponent',

  computed: {
    ...mapGetters([
      'newItem',
      'newItemLength',
      'isNewItemInputLimitExceeded',
      'email',
      'isEmailValid',
      'urgency',
      'termsAndConditions',
      'isFormValid',
      'isSubmitting',
      'submitError',
      'submitSuccess'
    ]),

    // v-model pour newItem
    newItemModel: {
      get() {
        return this.newItem
      },
      set(value) {
        this.$store.commit('UPDATE_NEW_ITEM', value)
      }
    },

    // v-model pour email
    emailModel: {
      get() {
        return this.email
      },
      set(value) {
        this.$store.commit('UPDATE_EMAIL', value)
      }
    },

    // v-model pour urgency
    urgencyModel: {
      get() {
        return this.urgency
      },
      set(value) {
        this.$store.commit('UPDATE_URGENCY', value)
      }
    },

    // v-model pour termsAndConditions
    termsModel: {
      get() {
        return this.termsAndConditions
      },
      set(value) {
        this.$store.commit('UPDATE_TERMS_AND_CONDITIONS', value)
      }
    }
  },

  methods: {
    ...mapActions(['submitForm', 'resetForm']),

    async handleSubmit() {
      const success = await this.submitForm()
      if (success) {
        this.$emit('submitted')
      }
    }
  },

  mounted() {
    this.$store.dispatch('loadItems')
  }
}
</script>

Helper pour Générer les Computed v-model

Pour éviter la répétition, créez un helper :

// helpers/vuexForm.js
export function createVuexModels(fields) {
  const computed = {}

  fields.forEach(({ name, getter, mutation }) => {
    computed[`${name}Model`] = {
      get() {
        return this.$store.getters[getter || name]
      },
      set(value) {
        this.$store.commit(mutation || `UPDATE_${name.toUpperCase()}`, value)
      }
    }
  })

  return computed
}

// Utilisation dans un composant
import { createVuexModels } from '@/helpers/vuexForm'

export default {
  computed: {
    ...createVuexModels([
      { name: 'newItem' },
      { name: 'email' },
      { name: 'urgency' },
      { name: 'termsAndConditions', mutation: 'UPDATE_TERMS_AND_CONDITIONS' }
    ])
  }
}

Exemple Complet : Formulaire de Contact Complexe

Voici un exemple complet d’un formulaire de contact avec validation et soumission :

<template>
  <div class="contact-form-container">
    <h2>Formulaire de Contact</h2>

    <form @submit.prevent="handleSubmit" class="contact-form">
      <!-- Informations personnelles -->
      <fieldset>
        <legend>Informations personnelles</legend>

        <div class="form-row">
          <div class="form-group">
            <label for="firstName">Prénom *</label>
            <input
              id="firstName"
              v-model="firstNameModel"
              type="text"
              required
            />
          </div>

          <div class="form-group">
            <label for="lastName">Nom *</label>
            <input
              id="lastName"
              v-model="lastNameModel"
              type="text"
              required
            />
          </div>
        </div>

        <div class="form-group">
          <label for="email">Email *</label>
          <input
            id="email"
            v-model="emailModel"
            type="email"
            required
            :class="{ 'is-invalid': emailErrors.length > 0 }"
          />
          <ul v-if="emailErrors.length" class="error-list">
            <li v-for="error in emailErrors" :key="error">{{ error }}</li>
          </ul>
        </div>

        <div class="form-group">
          <label for="phone">Téléphone</label>
          <input
            id="phone"
            v-model="phoneModel"
            type="tel"
            placeholder="+33 6 12 34 56 78"
          />
        </div>
      </fieldset>

      <!-- Adresse -->
      <fieldset>
        <legend>Adresse</legend>

        <div class="form-group">
          <label for="street">Rue</label>
          <input
            id="street"
            v-model="streetModel"
            type="text"
          />
        </div>

        <div class="form-row">
          <div class="form-group">
            <label for="city">Ville</label>
            <input
              id="city"
              v-model="cityModel"
              type="text"
            />
          </div>

          <div class="form-group">
            <label for="postalCode">Code postal</label>
            <input
              id="postalCode"
              v-model="postalCodeModel"
              type="text"
            />
          </div>
        </div>

        <div class="form-group">
          <label for="country">Pays</label>
          <select id="country" v-model="countryModel">
            <option value="">Sélectionnez un pays</option>
            <option value="France">France</option>
            <option value="Belgique">Belgique</option>
            <option value="Suisse">Suisse</option>
            <option value="Canada">Canada</option>
          </select>
        </div>
      </fieldset>

      <!-- Message -->
      <fieldset>
        <legend>Votre message</legend>

        <div class="form-group">
          <label for="subject">Sujet *</label>
          <input
            id="subject"
            v-model="newItemModel"
            type="text"
            required
            :class="{ 'is-invalid': isNewItemInputLimitExceeded }"
          />
          <small :class="{ 'text-danger': isNewItemInputLimitExceeded }">
            {{ newItemLength }}/20 caractères
          </small>
        </div>

        <div class="form-group">
          <label for="urgency">Priorité *</label>
          <select id="urgency" v-model="urgencyModel" required>
            <option value="">Sélectionnez une priorité</option>
            <option value="Urgent">Urgent</option>
            <option value="Important">Important</option>
            <option value="Nonessential">Non essentiel</option>
          </select>
          <small v-if="isNotUrgent" class="text-info">
            Les demandes non essentielles sont traitées sous 5 jours ouvrés.
          </small>
        </div>
      </fieldset>

      <!-- Conditions -->
      <div class="form-group checkbox-group">
        <label>
          <input
            v-model="termsModel"
            type="checkbox"
            required
          />
          J'accepte les <a href="/terms">conditions d'utilisation</a>
          et la <a href="/privacy">politique de confidentialité</a> *
        </label>
      </div>

      <!-- Actions -->
      <div class="form-actions">
        <button
          type="submit"
          class="btn btn-primary"
          :disabled="!isFormValid || isSubmitting"
        >
          <span v-if="isSubmitting">
            <span class="spinner"></span> Envoi en cours...
          </span>
          <span v-else>Envoyer le message</span>
        </button>

        <button
          type="button"
          class="btn btn-secondary"
          @click="resetForm"
          :disabled="isSubmitting"
        >
          Réinitialiser
        </button>
      </div>

      <!-- Messages de statut -->
      <div v-if="submitError" class="alert alert-error">
        {{ submitError }}
      </div>

      <div v-if="submitSuccess" class="alert alert-success">
        Votre message a été envoyé avec succès ! Nous vous répondrons dans les plus brefs délais.
      </div>
    </form>
  </div>
</template>

<script>
import { mapGetters, mapActions } from 'vuex'

export default {
  name: 'ContactForm',

  computed: {
    ...mapGetters([
      'newItem',
      'newItemLength',
      'isNewItemInputLimitExceeded',
      'email',
      'urgency',
      'isNotUrgent',
      'termsAndConditions',
      'firstName',
      'lastName',
      'phone',
      'address',
      'isFormValid',
      'isSubmitting',
      'submitError',
      'submitSuccess',
      'fieldErrors'
    ]),

    emailErrors() {
      return this.fieldErrors('email')
    },

    // Modèles v-model
    firstNameModel: {
      get() { return this.firstName },
      set(v) { this.$store.commit('UPDATE_FIRST_NAME', v) }
    },
    lastNameModel: {
      get() { return this.lastName },
      set(v) { this.$store.commit('UPDATE_LAST_NAME', v) }
    },
    emailModel: {
      get() { return this.email },
      set(v) { this.$store.commit('UPDATE_EMAIL', v) }
    },
    phoneModel: {
      get() { return this.phone },
      set(v) { this.$store.commit('UPDATE_PHONE', v) }
    },
    streetModel: {
      get() { return this.address.street },
      set(v) { this.$store.commit('UPDATE_ADDRESS_STREET', v) }
    },
    cityModel: {
      get() { return this.address.city },
      set(v) { this.$store.commit('UPDATE_ADDRESS_CITY', v) }
    },
    postalCodeModel: {
      get() { return this.address.postalCode },
      set(v) { this.$store.commit('UPDATE_ADDRESS_POSTAL_CODE', v) }
    },
    countryModel: {
      get() { return this.address.country },
      set(v) { this.$store.commit('UPDATE_ADDRESS_COUNTRY', v) }
    },
    newItemModel: {
      get() { return this.newItem },
      set(v) { this.$store.commit('UPDATE_NEW_ITEM', v) }
    },
    urgencyModel: {
      get() { return this.urgency },
      set(v) { this.$store.commit('UPDATE_URGENCY', v) }
    },
    termsModel: {
      get() { return this.termsAndConditions },
      set(v) { this.$store.commit('UPDATE_TERMS_AND_CONDITIONS', v) }
    }
  },

  methods: {
    ...mapActions(['submitForm', 'resetForm']),

    async handleSubmit() {
      const success = await this.submitForm()
      if (success) {
        this.$emit('form-submitted')
      }
    }
  }
}
</script>

Tableau Comparatif : Vuex vs Pinia pour les Formulaires

Si vous hésitez entre Vuex et Pinia pour gérer vos formulaires, voici un comparatif :

CritèreVuexPinia
SyntaxePlus verbeux (mutations obligatoires)Plus concise (actions directes)
TypeScriptSupport via définitions de typesSupport natif excellent
DevToolsVue DevTools (extension)Vue DevTools (intégré)
Taille du bundle~10kb~1kb
ModulesNamespaced, configuration manuelleStores indépendants, auto-découverte
Composition APISupport ajoutéConçu pour
MutationsObligatoires (séparation claire)Optionnelles (plus flexible)
Learning curvePlus raidePlus douce
ÉcosystèmeMature, beaucoup de ressourcesEn croissance rapide
Recommandation Vue 3Legacy supportRecommandé officiellement

Exemple équivalent avec Pinia

// stores/formStore.js (Pinia)
import { defineStore } from 'pinia'

export const useFormStore = defineStore('form', {
  state: () => ({
    fields: {
      newItem: '',
      email: '',
      urgency: '',
      termsAndConditions: false
    },
    items: [],
    isSubmitting: false,
    error: null
  }),

  getters: {
    isFormValid: (state) => {
      return state.fields.newItem.length > 0 &&
             state.fields.email.includes('@') &&
             state.fields.termsAndConditions
    }
  },

  actions: {
    updateField(field, value) {
      this.fields[field] = value
    },

    async submitForm() {
      this.isSubmitting = true
      try {
        await api.submit(this.fields)
        this.items.push({ ...this.fields })
        this.$reset()
      } catch (e) {
        this.error = e.message
      } finally {
        this.isSubmitting = false
      }
    }
  }
})

Bonnes Pratiques

Voici les recommandations essentielles pour une gestion efficace des formulaires avec Vuex :

1. Nommez vos mutations de manière explicite

Utilisez une convention de nommage cohérente comme UPDATE_FIELD_NAME ou SET_FIELD_NAME. Cela facilite le debugging dans les DevTools.

2. Regroupez les champs du formulaire dans un objet dédié

// Bon
state: {
  fields: { name: '', email: '' },
  items: []
}

// À éviter
state: {
  name: '',
  email: '',
  items: []
}

3. Utilisez des getters pour la validation

Centralisez la logique de validation dans les getters pour la réutiliser facilement et maintenir la cohérence.

4. Gérez les états UI séparément

Créez un objet ui ou meta pour les états comme isSubmitting, error, success. Ne mélangez pas avec les données du formulaire.

5. Préférez les actions pour les opérations asynchrones

Ne faites jamais d’appels API dans les mutations. Utilisez toujours les actions pour orchestrer les opérations asynchrones.

6. Documentez vos mutations et actions

Ajoutez des commentaires JSDoc pour clarifier le rôle de chaque mutation et action, surtout dans les projets en équipe.


Pièges Courants à Éviter

1. Modifier le state directement sans mutation

// ERREUR : modification directe
this.$store.state.fields.email = 'test@test.com'

// CORRECT : via mutation
this.$store.commit('UPDATE_EMAIL', 'test@test.com')

2. Oublier de réinitialiser le formulaire après soumission

Créez toujours une mutation CLEAR_FIELDS et appelez-la après une soumission réussie.

3. Ne pas gérer les erreurs dans les actions

// ERREUR : pas de gestion d'erreur
async submitForm({ commit }) {
  const result = await api.submit()
  commit('SET_SUCCESS', true)
}

// CORRECT : try/catch avec gestion
async submitForm({ commit }) {
  try {
    commit('SET_SUBMITTING', true)
    await api.submit()
    commit('SET_SUCCESS', true)
  } catch (error) {
    commit('SET_ERROR', error.message)
  } finally {
    commit('SET_SUBMITTING', false)
  }
}

4. Créer des mutations trop génériques

Une mutation UPDATE_STATE(state, payload) qui accepte n’importe quoi rend le debugging impossible. Préférez des mutations spécifiques.

5. Utiliser v-model directement sur le state

Comme vu précédemment, utilisez toujours le pattern computed get/set pour le v-model avec Vuex.


Conclusion

La gestion des formulaires avec Vuex offre une architecture solide et prévisible pour les applications Vue.js complexes. En suivant les patterns présentés dans cet article, vous bénéficiez de :

  • Prévisibilité : Chaque modification passe par une mutation traçable
  • Maintenabilité : La logique est centralisée et réutilisable
  • Testabilité : Les mutations et actions sont facilement testables en isolation
  • Debugging facilité : Les DevTools Vue permettent de voyager dans le temps

Pour les nouveaux projets Vue 3, considérez Pinia comme alternative plus moderne et légère. Cependant, Vuex reste un excellent choix pour les projets existants et les équipes familières avec son écosystème.


Pour Aller Plus Loin

  • Explorez les modules Vuex pour organiser les formulaires complexes en namespaces
  • Implémentez la persistance avec vuex-persistedstate pour sauvegarder les brouillons
  • Découvrez vee-validate pour une validation de formulaires plus avancée intégrée à Vuex
  • Testez vos stores avec @vue/test-utils et Jest

N’hésitez pas à partager vos questions et retours d’expérience dans les commentaires !

Advertisement

In-Article Ad

Dev Mode

Share this article

Mahmoud DEVO

Mahmoud DEVO

Senior Full-Stack Developer

I'm a passionate full-stack developer with 10+ years of experience building scalable web applications. I write about Vue.js, Node.js, PostgreSQL, and modern DevOps practices.

Enjoyed this article?

Subscribe to get more tech content delivered to your inbox.

Related Articles