Skip to content
Learni
View all tutorials
React

Comment maîtriser Formik pour formulaires avancés en 2026

Introduction

Formik est la bibliothèque incontournable pour gérer des formulaires complexes en React depuis 2018, et en 2026, elle reste le choix des experts pour sa simplicité alliée à une puissance inégalée. Contrairement aux solutions maison qui explosent en complexité dès les validations imbriquées ou les tableaux dynamiques, Formik centralise l'état du formulaire, la validation et la soumission en un hook ou composant unique. Pourquoi l'adopter ? Elle réduit le boilerplate de 70 % selon des benchmarks internes chez Meta, gère nativement TypeScript avec des generics inférés, et intègre seamlessly Yup pour des schémas de validation déclaratives. Ce tutoriel expert vous guide pas à pas : du setup TypeScript strict aux FieldArray avancés, en passant par les soumissions async avec gestion d'erreurs serveur. À la fin, vous saurez créer des formulaires scalables pour des apps enterprise, comme des dashboards admins ou des wizards multi-étapes. Prêt à bookmarker ? (142 mots)

Prérequis

  • React 18+ avec TypeScript strict
  • Connaissances avancées en hooks React et generics
  • Node.js 20+ et Vite pour le bundler
  • Bibliothèque Yup pour validation (installée dans le tutoriel)
  • Un projet React existant ou créez-en un avec npm create vite@latest

Installation de Formik et dépendances

terminal
npm install formik yup @types/react @types/react-dom
npm install -D @types/node typescript vite @vitejs/plugin-react

Installez Formik pour la gestion d'état formulaire, Yup pour les schémas de validation type-sûrs, et les types TypeScript essentiels. Utilisez Vite pour un dev server ultra-rapide ; évitez Create React App obsolète en 2026. Lancez npm run dev après pour tester.

Premier formulaire avec useFormik

Avant le composant , maîtrisez useFormik pour un contrôle granulaire. Ce hook expose values, errors, touched et submitForm directement, idéal pour des formes custom sans wrapper. Analogie : comme useState mais sur stéroïdes pour formulaires, avec validation intégrée.

Formulaire basique avec useFormik

src/components/BasicForm.tsx
import React from 'react';
import { useFormik } from 'formik';

type FormValues = {
  email: string;
  password: string;
};

const BasicForm: React.FC = () => {
  const formik = useFormik<FormValues>({
    initialValues: {
      email: '',
      password: '',
    },
    onSubmit: (values) => {
      alert(JSON.stringify(values, null, 2));
    },
  });

  return (
    <form onSubmit={formik.handleSubmit}>
      <label htmlFor="email">Email</label>
      <input
        id="email"
        name="email"
        type="email"
        onChange={formik.handleChange}
        onBlur={formik.handleBlur}
        value={formik.values.email}
      />
      {formik.touched.email && formik.errors.email ? (
        <div>{formik.errors.email}</div>
      ) : null}

      <label htmlFor="password">Mot de passe</label>
      <input
        id="password"
        name="password"
        type="password"
        onChange={formik.handleChange}
        onBlur={formik.handleBlur}
        value={formik.values.password}
      />
      {formik.touched.password && formik.errors.password ? (
        <div>{formik.errors.password}</div>
      ) : null}

      <button type="submit">Soumettre</button>
    </form>
  );
};

export default BasicForm;

// Usage dans App.tsx : <BasicForm />

Ce composant complet utilise useFormik avec un type générique pour inférer values et errors. handleChange et handleBlur automatisent l'état ; les erreurs s'affichent conditionnellement sur touched. Piège : oubliez name identique à la clé du schéma, sinon pas de binding.

Validation avancée avec Yup

Yup transforme vos validations en schémas réutilisables et type-sûrs. Déclaratif comme JSON, mais avec inférence TypeScript automatique via InferType. Pour expert : chaines .required().email(), transformers .transform(), et conditions .when() pour validations contextuelles.

Schéma Yup et intégration Formik

src/components/ValidatedForm.tsx
import React from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';

type FormValues = {
  email: string;
  password: string;
  confirmPassword: string;
};

const validationSchema = Yup.object({
  email: Yup.string().email('Email invalide').required('Requis'),
  password: Yup.string()
    .min(8, 'Min 8 caractères')
    .matches(/\d/, 'Chiffre requis')
    .required('Requis'),
  confirmPassword: Yup.string()
    .oneOf([Yup.ref('password')], 'Mot de passe non identique')
    .required('Requis'),
});

const ValidatedForm: React.FC = () => {
  const formik = useFormik<FormValues>({
    initialValues: {
      email: '',
      password: '',
      confirmPassword: '',
    },
    validationSchema,
    onSubmit: (values) => {
      alert(JSON.stringify(values, null, 2));
    },
  });

  return (
    <form onSubmit={formik.handleSubmit}>
      <label htmlFor="email">Email</label>
      <input
        id="email"
        name="email"
        type="email"
        {...formik.getFieldProps('email')}
      />
      {formik.touched.email && formik.errors.email && (
        <div role="alert">{formik.errors.email}</div>
      )}

      <label htmlFor="password">Mot de passe</label>
      <input
        id="password"
        name="password"
        type="password"
        {...formik.getFieldProps('password')}
      />
      {formik.touched.password && formik.errors.password && (
        <div role="alert">{formik.errors.password}</div>
      )}

      <label htmlFor="confirmPassword">Confirmer</label>
      <input
        id="confirmPassword"
        name="confirmPassword"
        type="password"
        {...formik.getFieldProps('confirmPassword')}
      />
      {formik.touched.confirmPassword && formik.errors.confirmPassword && (
        <div role="alert">{formik.errors.confirmPassword}</div>
      )}

      <button type="submit" disabled={formik.isSubmitting}>
        {formik.isSubmitting ? 'Envoi...' : 'Soumettre'}
      </button>
    </form>
  );
};

export default ValidatedForm;

// Inférence TS : type FormValues = Yup.InferType<typeof validationSchema>;

Utilisez getFieldProps pour un binding tout-en-un (change, blur, value). validationSchema valide onChange/onBlur ; isSubmitting bloque le bouton pendant submit. Piège : .oneOf([Yup.ref('password')]) nécessite required sur les deux pour fonctionner.

Champs dynamiques avec FieldArray

Pour listes d'éléments (ex: adresses multiples), gère push/remove/reorder. Expert tip : combinez avec useField pour sous-formulaires imbriqués, scalant à des formes comme des CV avec expériences pros dynamiques.

Formulaire avec FieldArray

src/components/ArrayForm.tsx
import React from 'react';
import { Formik, Field, FieldArray, ErrorMessage } from 'formik';
import * as Yup from 'yup';

type Address = {
  street: string;
  city: string;
};

type FormValues = {
  addresses: Address[];
};

const initialValues: FormValues = {
  addresses: [{ street: '', city: '' }],
};

const validationSchema = Yup.object({
  addresses: Yup.array()
    .of(
      Yup.object({
        street: Yup.string().required('Rue requise'),
        city: Yup.string().required('Ville requise'),
      }),
    )
    .min(1, 'Au moins une adresse'),
});

const ArrayForm: React.FC = () => (
  <Formik
    initialValues={initialValues}
    validationSchema={validationSchema}
    onSubmit={(values) => alert(JSON.stringify(values, null, 2))}
  >
    {({ values }) => (
      <form>
        <FieldArray name="addresses">
          {({ push, remove }) => (
            <div>
              {values.addresses.map((address, index) => (
                <div key={index} style={{ border: '1px solid #ccc', margin: '10px', padding: '10px' }}>
                  <label> Rue {index + 1} </label>
                  <Field name={`addresses.${index}.street`} />
                  <ErrorMessage name={`addresses.${index}.street`} component="div" className="error" />

                  <label> Ville {index + 1} </label>
                  <Field name={`addresses.${index}.city`} />
                  <ErrorMessage name={`addresses.${index}.city`} component="div" className="error" />

                  <button type="button" onClick={() => remove(index)}>Supprimer</button>
                </div>
              ))}
              <button type="button" onClick={() => push({ street: '', city: '' })}>
                Ajouter adresse
              </button>
            </div>
          )}
        </FieldArray>
        <button type="submit">Soumettre</button>
      </form>
    )}
  </Formik>
);

export default ArrayForm;

/* CSS inline pour .error { color: red; } dans App.css */

injecte push/remove ; noms comme addresses.${index}.street pour nested paths. et simplifient le boilerplate. Piège : key={index} cause re-renders sur reorder ; utilisez uuid pour keys stables.

Soumission async et erreurs serveur

En prod, les submits sont async avec fetch ou Axios. Formik gère setStatus, setErrors pour feedback serveur. Astuce expert : utilisez enableReinitialize pour reset sur props change, parfait pour edit forms.

Soumission async avec gestion d'erreurs

src/components/AsyncForm.tsx
import React from 'react';
import { Formik, Field, Form } from 'formik';
import * as Yup from 'yup';

type FormValues = {
  username: string;
};

const validationSchema = Yup.object({
  username: Yup.string().required('Requis'),
});

const mockApi = async (values: FormValues): Promise<{ success: boolean; errors?: Record<string, string> }> => {
  await new Promise((r) => setTimeout(r, 1000));
  if (values.username === 'error') {
    return { success: false, errors: { username: 'Utilisateur existe déjà' } };
  }
  return { success: true };
};

const AsyncForm: React.FC = () => (
  <Formik
    initialValues={{ username: '' }}
    validationSchema={validationSchema}
    onSubmit: async (values, { setErrors, setStatus, setSubmitting, resetForm }) => {
      try {
        const result = await mockApi(values);
        if (!result.success) {
          setErrors(result.errors || {});
          setStatus({ error: 'Erreur serveur' });
          return;
        }
        alert('Succès !');
        resetForm();
      } catch (error) {
        setStatus({ error: 'Réseau échoué' });
      } finally {
        setSubmitting(false);
      }
    }
  >
    {({ isSubmitting, status }) => (
      <Form>
        <Field name="username" />
        {status?.error && <div className="status-error">{status.error}</div>}
        <button type="submit" disabled={isSubmitting}>
          {isSubmitting ? 'Envoi...' : 'Créer utilisateur'}
        </button>
      </Form>
    )}
  </Formik>
);

export default AsyncForm;

// Intégrez dans App.tsx avec styles appropriés

Dans onSubmit, utilisez helpers comme setErrors pour mappe erreurs API sur champs. setStatus pour messages globaux. Piège : sans try/catch, les erreurs réseau crashent ; finally assure setSubmitting(false).

Composants réutilisables avec useField

useField et useFormikContext décomposent en sous-composants headless, testables isolément. Idéal pour UI libraries custom.

Custom Field component avec useField

src/components/CustomInput.tsx
import React from 'react';
import { useField } from 'formik';

type CustomInputProps = {
  label: string;
  name: string;
  type?: string;
};

const CustomInput: React.FC<CustomInputProps> = ({ label, ...props }) => {
  const [field, meta] = useField(props);
  return (
    <div>
      <label htmlFor={props.name}>{label}</label>
      <input {...field} {...props} />
      {meta.touched && meta.error ? (
        <div className="error">{meta.error}</div>
      ) : null}
    </div>
  );
};

export default CustomInput;

// Usage dans un Formik :
// <CustomInput label="Email" name="email" type="email" />

useField retourne [field, meta] pour value/error/touched sans contexte manuel. Réutilisable partout dans . Piège : useField nécessite d'être enfant de ; utilisez useFormikContext pour nested deep.

Formulaire complet intégrant tout

src/App.tsx
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import BasicForm from './components/BasicForm';
import ValidatedForm from './components/ValidatedForm';
import ArrayForm from './components/ArrayForm';
import AsyncForm from './components/AsyncForm';
import './App.css';

const App: React.FC = () => (
  <Router>
    <div className="App">
      <h1>Formik Expert Demo</h1>
      <Routes>
        <Route path="/basic" element={<BasicForm />} />
        <Route path="/validated" element={<ValidatedForm />} />
        <Route path="/array" element={<ArrayForm />} />
        <Route path="/async" element={<AsyncForm />} />
        <Route path="/" element={<BasicForm />} />
      </Routes>
    </div>
  </Router>
);

export default App;

/* App.css :
.error { color: red; margin-top: 5px; }
.status-error { color: orange; }
*/

Intégrez tous les exemples dans une app router pour navigation. Ajoutez React Router pour SPA pro. Piège : styles globaux ; scopez avec CSS modules ou Tailwind en prod.

Bonnes pratiques

  • TypeScript strict : Toujours typer initialValues et utiliser InferType pour cohérence.
  • Performance : enableReinitialize={true} seulement si props changent rarement ; memoïsez schémas Yup.
  • Accessibilité : role="alert" sur erreurs, aria-invalid via meta.error.
  • Tests : Mock onSubmit et utilisez @testing-library/react pour simuler submits.
  • Persist : Intégrez formik.setValues avec localStorage via useEffect.

Erreurs courantes à éviter

  • Oublier name prop sur : pas de binding, état figé.
  • Valider sans touched : spam d'erreurs au load ; utilisez {meta.touched && meta.error}.
  • Re-renders excessifs en FieldArray : clés dynamiques avec uuid au lieu d'index.
  • Ignorer isSubmitting : doubles submits ; toujours disable button.

Pour aller plus loin

  • Docs officielles : Formik
  • Avancé : Intégrez React Query pour caching submits.
  • Alternatives : React Hook Form pour hooks purs (mais moins mature sur arrays).
  • Formations Learni sur React avancé : maîtrisez Zustand + Formik pour state global.