Tutoriel React React Hook Form validation formulaire intégration API

Intégrer Syvel avec React Hook Form en 10 minutes

Guide étape par étape pour valider les emails en temps réel dans vos formulaires React sans friction UX.

Par Équipe Syvel · · 7 min de lecture

Ce que vous allez construire

À la fin de ce guide, votre formulaire React :

  • Valide les emails en temps réel via l’API Syvel
  • Affiche un indicateur visuel (loading → ✓ valide / ✗ problème)
  • Ne bloque la soumission que pour les adresses clairement problématiques (is_risky === true)
  • Affiche un avertissement optionnel pour les adresses suspectes (score 50–64)
  • Propose une correction de typo si did_you_mean est retourné (gmial.comgmail.com)
  • Respecte les bonnes pratiques UX : pas de validation intrusive, debounce, gestion d’erreur gracieuse

Le tout en moins de 60 lignes de code.


Prérequis

  • Projet React avec React Hook Form (npm install react-hook-form)
  • Une clé API Syvel (gratuit, créez un compte →) — consultez nos tarifs si vous avez besoin de volumes importants
  • Node 18+

Réponse API : les champs utilisés

L’endpoint GET /v1/check/{email} retourne :

interface SyvelResult {
risk_score: number; // 0-100
is_risky: boolean; // true si risk_score >= seuil projet (défaut: 65)
reason: 'safe' | 'disposable' | 'undeliverable' | 'role_account';
did_you_mean: string | null; // ex: "gmail.com" pour "gmial.com"
deliverability_score: number; // 0-100
is_free_provider: boolean;
is_corporate_email: boolean;
is_alias_email: boolean;
mx_provider_label: string | null;
}

Étape 1 : Créer le hook de validation

Créez un fichier src/hooks/useEmailValidation.ts :

import { useState, useCallback } from 'react';
interface SyvelResult {
risk_score: number;
is_risky: boolean;
reason: 'safe' | 'disposable' | 'undeliverable' | 'role_account';
did_you_mean: string | null;
deliverability_score: number;
}
interface ValidationState {
status: 'idle' | 'loading' | 'valid' | 'warning' | 'error';
message?: string;
didYouMean?: string;
score?: number;
}
export function useEmailValidation(apiKey: string) {
const [state, setState] = useState<ValidationState>({ status: 'idle' });
const validate = useCallback(async (email: string): Promise<string | true> => {
if (!email || email.length < 5) return true; // Laisser passer, validation format s'en charge
setState({ status: 'loading' });
try {
const res = await fetch(
`https://api.syvel.io/v1/check/${encodeURIComponent(email)}`,
{ headers: { 'Authorization': `Bearer ${apiKey}` } }
);
if (!res.ok) {
// En cas d'erreur API, ne pas bloquer l'utilisateur (fail open)
setState({ status: 'idle' });
return true;
}
const data: SyvelResult = await res.json();
// Typo détectée — avertir sans bloquer
if (data.did_you_mean) {
setState({
status: 'warning',
message: `Vouliez-vous dire ${data.did_you_mean} ?`,
didYouMean: data.did_you_mean,
score: data.risk_score,
});
// On ne bloque pas, mais on avertit
return true;
}
if (data.is_risky) {
let message = 'Cette adresse email n\'est pas acceptée.';
if (data.reason === 'undeliverable') {
message = 'Ce domaine ne peut pas recevoir d\'emails.';
} else if (data.reason === 'role_account') {
message = 'Veuillez utiliser une adresse email personnelle.';
}
setState({ status: 'error', message, score: data.risk_score });
return message;
}
// Score modéré (non bloquant) : avertissement doux
if (data.risk_score >= 50) {
setState({
status: 'warning',
message: 'Cette adresse semble temporaire. Utilisez votre email professionnel.',
score: data.risk_score,
});
return true;
}
setState({ status: 'valid', score: data.risk_score });
return true;
} catch {
// Erreur réseau : fail open (ne jamais bloquer un utilisateur légitime)
setState({ status: 'idle' });
return true;
}
}, [apiKey]);
return { state, validate };
}

Points importants :

  • Fail open : si l’API Syvel est inaccessible, on laisse passer l’email. Ne jamais bloquer un utilisateur légitime à cause d’une erreur serveur externe.
  • is_risky : champ de blocage recommandé. Le seuil est configurable dans votre dashboard.
  • did_you_mean : proposez la correction en temps réel plutôt que de bloquer.
  • Score 50–64 : avertissement visible mais soumission autorisée. Utile pour les formulaires B2C.

Étape 2 : Créer le composant d’indicateur

Créez src/components/EmailInput.tsx :

import React from 'react';
import { UseFormRegister, FieldErrors } from 'react-hook-form';
interface ValidationState {
status: 'idle' | 'loading' | 'valid' | 'warning' | 'error';
message?: string;
didYouMean?: string;
}
interface Props {
register: UseFormRegister<any>;
errors: FieldErrors;
validationState: ValidationState;
onDidYouMeanClick?: (suggestion: string) => void;
name?: string;
}
function StatusIcon({ status }: { status: ValidationState['status'] }) {
switch (status) {
case 'loading':
return <span className="email-icon loading" aria-hidden></span>;
case 'valid':
return <span className="email-icon valid" aria-hidden></span>;
case 'warning':
return <span className="email-icon warning" aria-hidden></span>;
case 'error':
return <span className="email-icon error" aria-hidden></span>;
default:
return null;
}
}
export function EmailInput({ register, errors, validationState, onDidYouMeanClick, name = 'email' }: Props) {
return (
<div className="field-wrapper">
<label htmlFor={name}>Adresse email</label>
<div className="input-wrapper">
<input
id={name}
type="email"
autoComplete="email"
aria-invalid={!!errors[name]}
aria-describedby={`${name}-hint`}
className={`email-input status-${validationState.status}`}
{...register(name)}
/>
<StatusIcon status={validationState.status} />
</div>
{/* Message d'erreur RHF (format invalide) */}
{errors[name] && (
<p id={`${name}-hint`} role="alert" className="field-error">
{errors[name]?.message as string}
</p>
)}
{/* Avertissement Syvel (non-bloquant) */}
{validationState.status === 'warning' && !errors[name] && (
<p id={`${name}-hint`} role="status" className="field-warning">
{validationState.message}
{validationState.didYouMean && onDidYouMeanClick && (
<button
type="button"
className="did-you-mean-btn"
onClick={() => onDidYouMeanClick(validationState.didYouMean!)}
>
Utiliser {validationState.didYouMean}
</button>
)}
</p>
)}
</div>
);
}

Étape 3 : Assembler le formulaire

import React from 'react';
import { useForm } from 'react-hook-form';
import { useEmailValidation } from '../hooks/useEmailValidation';
import { EmailInput } from '../components/EmailInput';
const API_KEY = import.meta.env.VITE_SYVEL_API_KEY;
interface FormData {
email: string;
name: string;
}
export function SignupForm() {
const { state: emailState, validate } = useEmailValidation(API_KEY);
const {
register,
handleSubmit,
setValue,
formState: { errors, isSubmitting },
} = useForm<FormData>({
mode: 'onBlur', // Valider à la perte de focus, pas à chaque frappe
});
const handleDidYouMean = (suggestion: string) => {
// Remplace le domaine dans le champ email par la suggestion
const currentEmail = (document.getElementById('email') as HTMLInputElement)?.value;
if (currentEmail && currentEmail.includes('@')) {
const local = currentEmail.split('@')[0];
setValue('email', `${local}@${suggestion}`, { shouldValidate: true });
}
};
const onSubmit = async (data: FormData) => {
console.log('Formulaire soumis :', data);
// Votre logique d'inscription ici
};
return (
<form onSubmit={handleSubmit(onSubmit)} noValidate>
<div className="field-wrapper">
<label htmlFor="name">Nom</label>
<input
id="name"
type="text"
autoComplete="name"
{...register('name', { required: 'Le nom est requis.' })}
/>
{errors.name && <p className="field-error">{errors.name.message}</p>}
</div>
<EmailInput
register={register}
errors={errors}
validationState={emailState}
onDidYouMeanClick={handleDidYouMean}
name="email"
/>
<button
type="submit"
disabled={isSubmitting || emailState.status === 'loading'}
>
{isSubmitting ? 'Création en cours...' : 'Créer mon compte'}
</button>
</form>
);
}

Pour brancher la validation Syvel au cycle React Hook Form :

// Dans le composant EmailInput, modifiez le register('email') ainsi :
{...register(name, {
required: 'L\'email est requis.',
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: 'Format d\'email invalide.',
},
validate: async (value) => await validate(value),
})}

Étape 4 : Ajouter le debounce

Si vous validez en mode onChange plutôt que onBlur, ajoutez un debounce pour éviter un appel API à chaque frappe :

import { useMemo } from 'react';
import { debounce } from 'lodash-es'; // ou votre propre implémentation
// Dans useEmailValidation, wrapper validate avec debounce :
const debouncedValidate = useMemo(
() => debounce(validate, 500),
[validate]
);

En mode onBlur (recommandé), le debounce n’est pas nécessaire — la validation se déclenche uniquement quand l’utilisateur quitte le champ.


Variables d’environnement

Dans votre .env.local :

Terminal window
VITE_SYVEL_API_KEY=sv_votre_cle_ici

Ne jamais committer votre clé API. Ajoutez .env.local à votre .gitignore. En production, injectez la variable via votre pipeline CI/CD.

Pour les applications où la clé serait exposée côté client, préférez passer l’appel API via votre propre backend (BFF) qui détient la clé côté serveur.


Résultat final

Avec cette intégration, votre formulaire :

CasComportement
Email valide (is_risky: false, score < 50)Indicateur vert ✓, soumission autorisée
Email suspect (score 50–64)Avertissement ⚠, soumission autorisée
Typo détectée (did_you_mean)Suggestion interactive, soumission autorisée
Email risqué (is_risky: true)Erreur ✗, soumission bloquée
API indisponibleFail open, soumission autorisée
Email mal formatéErreur regex locale, pas d’appel API

Pour aller plus loin

Protégez vos formulaires avec Syvel

Bloquez les emails jetables, catch-all et malformés en temps réel. API REST simple, conforme RGPD, hébergée en France.