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 :
| Aspect | Bénéfice |
|---|---|
| Séparation claire | Les champs du formulaire sont isolés des autres données |
| Reset facile | Réinitialiser le formulaire revient à remplacer fields |
| Validation centralisée | Les 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ère | Vuex | Pinia |
|---|---|---|
| Syntaxe | Plus verbeux (mutations obligatoires) | Plus concise (actions directes) |
| TypeScript | Support via définitions de types | Support natif excellent |
| DevTools | Vue DevTools (extension) | Vue DevTools (intégré) |
| Taille du bundle | ~10kb | ~1kb |
| Modules | Namespaced, configuration manuelle | Stores indépendants, auto-découverte |
| Composition API | Support ajouté | Conçu pour |
| Mutations | Obligatoires (séparation claire) | Optionnelles (plus flexible) |
| Learning curve | Plus raide | Plus douce |
| Écosystème | Mature, beaucoup de ressources | En croissance rapide |
| Recommandation Vue 3 | Legacy support | Recommandé 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 !
In-Article Ad
Dev Mode
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
Migrer vers la Composition API Vue 3 : Guide Etape par Etape
Migrez vos composants Vue vers la Composition API. useStore, ref, onMounted et lifecycle hooks expliques avec des exemples pratiques.
Securiser votre application Vue.js avec Vue Router et les jetons JWT
Implementez une authentification securisee avec Vue Router et Vuex. Gerez les jetons JWT et protegez vos routes sensibles efficacement.
Créer une application météo avec Vue.js et Vuex : Une approc
Voici une proposition de meta description qui correspond aux exigences : "Découvrez comment créer une application Vue.js responsive et sécurisée avec un systèm