WebAssembly + Go + React : Valider tes formulaires comme un chef !

Ben ✨Ben ✨
10 min read

Salut toi ! Alors comme ça, tu en as marre que la validation de tes formulaires JavaScript qui rame ? Tu aimerais bien avoir les performances du natif sans te prendre la tête avec du C++ ? J'ai la solution parfaite pour toi : WebAssembly avec Go !

Aujourd'hui, on va découvrir GoWM, un petit bijou qui va révolutionner ta façon d'intégrer WebAssembly dans tes apps React, Vue.js ou dans du Node.js. Prépare ton café, on va s'amuser !

GoWM, qu'est-ce que c'est que ce truc ?

GoWM (Go Wasm Manager), c'est un peu comme ton couteau suisse pour WebAssembly. Cette bibliothèque te simplifie la vie au maximum pour charger et utiliser des modules WASM Go dans tes apps JavaScript.

Fini les galères d'intégration ! GoWM te donne :

  • Une interface unifiée pour charger tes modules WASM Go (enfin !)

  • Un support complet pour Node.js et navigateur avec détection automatique (malin, non ?)

  • Des hooks React intégrés (useWasm, useWasmFromNPM) parce qu'on aime React ici

  • Des composables Vue.js pour les fans de Vue 3

  • Un chargement depuis NPM avec résolution automatique (plus besoin de se creuser la tête)

  • Une gestion robuste des erreurs et détection de fonctions

  • Une gestion automatique de la mémoire (tu peux dormir tranquille)

Pourquoi WebAssembly pour valider tes formulaires ?

Alors là, excellente question ! Tu sais, quand tu as des formulaires avec des règles métier complexes, ça peut vite devenir l'enfer côté performance. Genre, tu as des regex pourries, des validations cross-field, et ton navigateur qui commence à suffoquer.

WebAssembly, c'est comme avoir une Ferrari dans ton navigateur. Du code Go compilé qui tourne à des vitesses de malade, directement dans le browser. C'est beau, non ?

Allez, on installe tout ça !

Bon, assez parlé, on passe aux choses sérieuses :

npm install gowm
# ou si tu es team Yarn
yarn add gowm

Facile, hein ? 😄

Création de notre module WASM de validation

Structure du projet

D'abord, organisons-nous correctement :

validator-wasm/
├── main.go           # Notre code Go de la mort
├── go.mod            # Configuration Go
├── build.sh          # Script magique de compilation
├── package.json      # Config NPM
└── README.md         # Documentation (sois pas fainéant !)

Le code Go du Wasm

Allez, on s'attaque au vif du sujet. Voici notre validateur Go qui sera builder en wasm :

//go:build js && wasm
package main

import (
    "encoding/json"
    "fmt"
    "regexp"
    "strings"
    "syscall/js"
)

type ValidationRule struct {
    Field    string `json:"field"`
    Required bool   `json:"required"`
    MinLen   int    `json:"minLen"`
    MaxLen   int    `json:"maxLen"`
    Pattern  string `json:"pattern"`
    Type     string `json:"type"`
}

type ValidationResult struct {
    Valid  bool              `json:"valid"`
    Errors map[string]string `json:"errors"`
}

// validateEmail - parce qu'on en a tous marre des emails pourris
func validateEmail(email string) bool {
    pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
    matched, _ := regexp.MatchString(pattern, email)
    return matched
}

// validatePhone - pour les numéros français
func validatePhone(phone string) bool {
    pattern := `^(?:\+33|0)[1-9](?:[0-9]{8})$`
    matched, _ := regexp.MatchString(pattern, phone)
    return matched
}

// validateForm - la fonction qui fait tout le boulot
func validateForm(this js.Value, args []js.Value) interface{} {
    if len(args) != 2 {
        return js.ValueOf(map[string]interface{}{
            "valid":  false,
            "errors": map[string]string{"system": "Arguments invalides, mon reuf !"},
        })
    }

    // Parse des données (attention, ça peut piquer)
    formDataJSON := args[0].String()
    rulesJSON := args[1].String()

    var formData map[string]interface{}
    var rules []ValidationRule

    if err := json.Unmarshal([]byte(formDataJSON), &formData); err != nil {
        return js.ValueOf(map[string]interface{}{
            "valid":  false,
            "errors": map[string]string{"system": "Données formulaire pourries"},
        })
    }

    if err := json.Unmarshal([]byte(rulesJSON), &rules); err != nil {
        return js.ValueOf(map[string]interface{}{
            "valid":  false,
            "errors": map[string]string{"system": "Règles de validation foireuses"},
        })
    }

    result := ValidationResult{
        Valid:  true,
        Errors: make(map[string]string),
    }

    // La magie opère ici
    for _, rule := range rules {
        value, exists := formData[rule.Field]
        valueStr := ""

        if exists && value != nil {
            valueStr = strings.TrimSpace(value.(string))
        }

        // Champ requis ? On vérifie !
        if rule.Required && valueStr == "" {
            result.Valid = false
            result.Errors[rule.Field] = "Ce champ est requis, fais un effort !"
            continue
        }

        if valueStr == "" {
            continue // On skip si pas requis et vide
        }

        // Longueur minimale
        if rule.MinLen > 0 && len(valueStr) < rule.MinLen {
            result.Valid = false
            result.Errors[rule.Field] = fmt.Sprintf("Minimum %d caractères requis", rule.MinLen)
            continue
        }

        // Longueur maximale
        if rule.MaxLen > 0 && len(valueStr) > rule.MaxLen {
            result.Valid = false
            result.Errors[rule.Field] = fmt.Sprintf("Maximum %d caractères autorisés", rule.MaxLen)
            continue
        }

        // Validation par type
        switch rule.Type {
        case "email":
            if !validateEmail(valueStr) {
                result.Valid = false
                result.Errors[rule.Field] = "Merci d'entrer une adresse email valide"
            }
        case "phone":
            if !validatePhone(valueStr) {
                result.Valid = false
                result.Errors[rule.Field] = "Ce numéro de téléphone, c'est n'importe quoi"
            }
        }

        // Pattern personnalisé pour les perfectionnistes
        if rule.Pattern != "" {
            matched, err := regexp.MatchString(rule.Pattern, valueStr)
            if err != nil || !matched {
                result.Valid = false
                result.Errors[rule.Field] = "Format invalide, recommence !"
            }
        }
    }

    // On retourne le tout en JSON
    resultJSON, _ := json.Marshal(result)
    return js.ValueOf(string(resultJSON))
}

func main() {
    // On expose notre fonction au monde JavaScript
    js.Global().Set("validateForm", js.FuncOf(validateForm))

    // Signal de prêt pour GoWM
    js.Global().Set("__gowm_ready", js.ValueOf(true))

    // On maintient le programme en vie (important !)
    select {}
}

Script de compilation en Wasm

Créons notre script build.sh qui va compiler tout ça proprement :

#!/bin/bash
set -e

echo "🔨 Compilation du module de validation WASM..."

# Compilation optimisée (on veut du perf !)
GOOS=js GOARCH=wasm go build \
    -ldflags="-s -w" \
    -o validator.wasm \
    main.go

echo "📦 Copie du runtime Go..."
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" .

echo "✅ Build terminé !"
echo "📊 Taille du fichier WASM: $(du -h validator.wasm | cut -f1)"

Intégration React avec GoWM

Maintenant, la partie vraiment fun : intégrer tout ça dans React ! Voici un composant :

import React, { useState, useEffect } from 'react';
import { load } from 'gowm';

const ContactForm = () => {
    const [wasm, setWasm] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
        const loadWasm = async () => {
            try {
                setLoading(true);
                setError(null);
                const wasmModule = await load('/validator.wasm');
                setWasm(wasmModule);
                setLoading(false);
            } catch (err) {
                setError(err);
                setLoading(false);
            }
        };

        loadWasm();
    }, []);

    const [formData, setFormData] = useState({
        name: '',
        email: '',
        phone: '',
        message: ''
    });

    const [validationErrors, setValidationErrors] = useState({});
    const [isSubmitting, setIsSubmitting] = useState(false);

    // Nos règles de validation (modulables à souhait)
    const validationRules = [
        { field: 'name', required: true, minLen: 2, maxLen: 50 },
        { field: 'email', required: true, type: 'email' },
        { field: 'phone', required: false, type: 'phone' },
        { field: 'message', required: true, minLen: 10, maxLen: 500 }
    ];

    const handleInputChange = (e) => {
        const { name, value } = e.target;
        setFormData(prev => ({
            ...prev,
            [name]: value
        }));

        // Validation en temps réel (parce qu'on est des pros)
        if (wasm) {
            validateField(name, value);
        }
    };

    const validateField = async (fieldName, value) => {
        if (!wasm) {
            // Validation de fallback en JavaScript si WASM pas encore chargé
            const errors = {};
            const rules = validationRules.filter(rule => rule.field === fieldName);
            for (const rule of rules) {
                if (rule.required && !value.trim()) {
                    errors[fieldName] = "Ce champ est requis, fais un effort !";
                    break;
                }
                if (value.trim() && rule.minLen && value.length < rule.minLen) {
                    errors[fieldName] = `Minimum ${rule.minLen} caractères requis`;
                    break;
                }
                if (value.trim() && rule.type === 'email') {
                    const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
                    if (!emailRegex.test(value)) {
                        errors[fieldName] = "Cet email sent le poisson pourri";
                        break;
                    }
                }
            }
            setValidationErrors(prev => ({
                ...prev,
                [fieldName]: errors[fieldName] || null
            }));
            return;
        }

        try {
            const singleFieldData = { [fieldName]: value };
            const fieldRules = validationRules.filter(rule => rule.field === fieldName);

            // Avec gowm, les fonctions sont disponibles globalement après le chargement
            const result = window.validateForm(
                JSON.stringify(singleFieldData), 
                JSON.stringify(fieldRules)
            );

            const validation = JSON.parse(result);

            setValidationErrors(prev => ({
                ...prev,
                [fieldName]: validation.errors[fieldName] || null
            }));
        } catch (err) {
            console.error('Oups, erreur de validation:', err);
        }
    };

    const handleSubmit = async (e) => {
        e.preventDefault();

        if (!wasm) {
            alert('Patience, le validateur charge encore !');
            return;
        }

        setIsSubmitting(true);

        try {
            // Validation complète (le moment de vérité)
            const result = window.validateForm(
                JSON.stringify(formData), 
                JSON.stringify(validationRules)
            );

            const validation = JSON.parse(result);

            if (validation.valid) {
                alert('Formulaire nickel ! Envoi en cours...');
                // Ici tu peux envoyer tes données
                console.log('Données prêtes à partir:', formData);
            } else {
                setValidationErrors(validation.errors);
            }
        } catch (err) {
            console.error('Erreur lors de la validation:', err);
            alert('Oups, quelque chose a foiré !');
        } finally {
            setIsSubmitting(false);
        }
    };

    if (loading) {
        return (
            <div className="flex items-center justify-center p-8">
                <div className="text-center">
                    <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
                    <p className="mt-2 text-gray-600">Chargement du validateur...</p>
                </div>
            </div>
        );
    }

    if (error) {
        return (
            <div className="bg-red-50 border border-red-200 rounded-lg p-4">
                <p className="text-red-600">Erreur de chargement: {error.message}</p>
            </div>
        );
    }

    return (
        <div className="max-w-md mx-auto bg-white rounded-lg shadow-md p-6">
            <h2 className="text-2xl font-bold mb-6 text-gray-800">Contact</h2>

            <form onSubmit={handleSubmit} className="space-y-4">
                <div>
                    <label className="block text-sm font-medium text-gray-700 mb-1">
                        Nom *
                    </label>
                    <input
                        type="text"
                        name="name"
                        value={formData.name}
                        onChange={handleInputChange}
                        className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
                            validationErrors.name ? 'border-red-500' : 'border-gray-300'
                        }`}
                    />
                    {validationErrors.name && (
                        <p className="text-red-500 text-sm mt-1">{validationErrors.name}</p>
                    )}
                </div>

                <div>
                    <label className="block text-sm font-medium text-gray-700 mb-1">
                        Email *
                    </label>
                    <input
                        type="email"
                        name="email"
                        value={formData.email}
                        onChange={handleInputChange}
                        className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
                            validationErrors.email ? 'border-red-500' : 'border-gray-300'
                        }`}
                    />
                    {validationErrors.email && (
                        <p className="text-red-500 text-sm mt-1">{validationErrors.email}</p>
                    )}
                </div>

                <div>
                    <label className="block text-sm font-medium text-gray-700 mb-1">
                        Téléphone
                    </label>
                    <input
                        type="tel"
                        name="phone"
                        value={formData.phone}
                        onChange={handleInputChange}
                        className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
                            validationErrors.phone ? 'border-red-500' : 'border-gray-300'
                        }`}
                    />
                    {validationErrors.phone && (
                        <p className="text-red-500 text-sm mt-1">{validationErrors.phone}</p>
                    )}
                </div>

                <div>
                    <label className="block text-sm font-medium text-gray-700 mb-1">
                        Message *
                    </label>
                    <textarea
                        name="message"
                        rows={4}
                        value={formData.message}
                        onChange={handleInputChange}
                        className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
                            validationErrors.message ? 'border-red-500' : 'border-gray-300'
                        }`}
                    />
                    {validationErrors.message && (
                        <p className="text-red-500 text-sm mt-1">{validationErrors.message}</p>
                    )}
                </div>

                <button
                    type="submit"
                    disabled={isSubmitting}
                    className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
                >
                    {isSubmitting ? 'Validation en cours...' : 'Envoyer'}
                </button>
            </form>
        </div>
    );
};

export default ContactForm;

Pourquoi cette approche ?

Performances.

Avec WebAssembly, ton code de validation tourne à la vitesse de l'éclair. Les regex complexes et la logique métier sont traitées de manière ultra-efficace. Fini les freeze d'interface !

Validation en temps réel sans lag

Grâce au hook useWasm de GoWM, tu peux valider tes champs en temps réel sans que ton interface rame.

Réutilisabilité au max

Ton module WASM, tu peux :

  • Le publier sur NPM pour le réutiliser partout

  • L'utiliser côté serveur avec Node.js (pratique !)

  • L'intégrer dans des apps Vue.js avec les composables GoWM

Sécurité renforcée

La validation côté client en WASM ajoute une couche de sécurité. Bien sûr, tu gardes ta validation côté serveur (on n'est pas fous !).

Configuration avancée avec GoWM

GoWM, c'est pas que basique. Tu peux faire d’autres trucs sympa :

// Chargement simple
import { load } from 'gowm';

async function loadValidator() {
    try {
        const validator = await load('/validator.wasm');
        // Utilisation directe des fonctions exposées globalement
        const result = window.validateForm(dataJSON, rulesJSON);
        return JSON.parse(result);
    } catch (error) {
        console.error('Oops, chargement foiré:', error);
    }
}

// Gestion de plusieurs modules (pour les gourmands)
import { load, get, listModules } from 'gowm';

const loadMultipleModules = async () => {
    await load('/validator.wasm', { name: 'validator' });
    await load('/formatter.wasm', { name: 'formatter' });

    // Lister les modules chargés
    console.log('Modules chargés:', listModules());

    // Récupérer un module spécifique
    const validator = get('validator');
};

Support TypeScript (pour les perfectionnistes)

GoWM inclut des types TypeScript complets :

import { load, LoadOptions } from 'gowm';

interface ValidationResult {
    valid: boolean;
    errors: Record<string, string>;
}

const loadValidator = async (): Promise<void> => {
    await load('./validator.wasm');

    // Les fonctions WASM sont disponibles globalement
    const result: string = (window as any).validateForm(formDataJSON, rulesJSON);
    const validation: ValidationResult = JSON.parse(result);
};

Alors, convaincu ? GoWM rend WebAssembly accessible à tous les développeurs JavaScript, même les plus fainéants ! Cette approche te permet de :

  • Booster les performances de tes validations complexes

  • Simplifier l'intégration grâce aux hooks React tout prêts

  • Maintenir un code propre et testable

  • Réutiliser ta logique entre client et serveur

Que ce soit pour la validation de formulaires, le traitement d'images, ou des calculs de la mort, WebAssembly avec GoWM ouvre de nouvelles possibilités pour tes applications web

Le code source complet de cet exemple sera bientôt disponible sur GitHub.

A plus ! 🚀

0
Subscribe to my newsletter

Read articles from Ben ✨ directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ben ✨
Ben ✨

Développeur web français, passionné d'innovation digitale. Je crée des applis innovantes et partage mes astuces sur les solutions opensource.