ITSkillsCenter
Développement Web

React : construire des interfaces modernes

7 دقائق للقراءة
React : construire des interfaces modernes

Ce que vous saurez faire à la fin

  1. Créer un projet React avec Vite et TypeScript en 2 minutes
  2. Écrire des composants fonctionnels avec hooks
  3. Gérer formulaires, listes, navigations
  4. Utiliser React Query pour le data fetching
  5. Déployer une SPA React en production

Durée : 3 heures. Pré-requis : Node.js 20+, VS Code, bases JavaScript.

Étape 1 — Créer le projet

npm create vite@latest mon-app -- --template react-ts
cd mon-app
npm install
npm run dev
  1. Ouvre http://localhost:5173.
  2. Structure : src/App.tsx est le composant racine, src/main.tsx le point d’entrée.

Étape 2 — Ajouter Tailwind CSS

npm install -D tailwindcss @tailwindcss/vite
# vite.config.ts
# import tailwindcss from "@tailwindcss/vite";
# plugins: [react(), tailwindcss()],

# src/index.css
@import "tailwindcss";

Étape 3 — Premier composant fonctionnel

// src/components/Carte.tsx
type CarteProps = {
  titre: string;
  description: string;
  prix: number;
};

export function Carte({ titre, description, prix }: CarteProps) {
  return (
    <article className="border rounded-lg p-4 shadow-sm">
      <h3 className="text-lg font-bold">{titre}</h3>
      <p className="text-gray-600 mt-1">{description}</p>
      <p className="text-blue-600 font-semibold mt-2">
        {prix.toLocaleString("fr-FR")} FCFA
      </p>
    </article>
  );
}

Étape 4 — useState et événements

import { useState } from "react";

export function Compteur() {
  const [n, setN] = useState(0);
  
  return (
    <div className="flex gap-2 items-center">
      <button onClick={() => setN(n - 1)} className="px-3 py-1 border">−</button>
      <span className="text-2xl font-bold w-16 text-center">{n}</span>
      <button onClick={() => setN(n + 1)} className="px-3 py-1 border">+</button>
    </div>
  );
}

Étape 5 — useEffect pour effets de bord

import { useState, useEffect } from "react";

type Client = { id: number; nom: string };

export function ListeClients() {
  const [clients, setClients] = useState<Client[]>([]);
  const [chargement, setChargement] = useState(true);
  const [erreur, setErreur] = useState<string | null>(null);
  
  useEffect(() => {
    const ctrl = new AbortController();
    fetch("/api/clients", { signal: ctrl.signal })
      .then(r => r.ok ? r.json() : Promise.reject(r.status))
      .then(setClients)
      .catch(e => setErreur(String(e)))
      .finally(() => setChargement(false));
    return () => ctrl.abort();
  }, []);
  
  if (chargement) return <p>Chargement...</p>;
  if (erreur) return <p className="text-red-600">Erreur: {erreur}</p>;
  
  return (
    <ul className="space-y-1">
      {clients.map(c => (
        <li key={c.id} className="border-b py-2">{c.nom}</li>
      ))}
    </ul>
  );
}

Étape 6 — Formulaire contrôlé

import { useState } from "react";

type FormProps = {
  onSave: (data: { nom: string; ville: string }) => void;
};

export function FormClient({ onSave }: FormProps) {
  const [nom, setNom] = useState("");
  const [ville, setVille] = useState("Dakar");
  
  function submit(e: React.FormEvent) {
    e.preventDefault();
    if (!nom.trim()) return;
    onSave({ nom: nom.trim(), ville });
    setNom("");
  }
  
  return (
    <form onSubmit={submit} className="flex gap-2">
      <input
        value={nom}
        onChange={e => setNom(e.target.value)}
        placeholder="Nom"
        className="border px-3 py-2 rounded flex-1"
      />
      <select
        value={ville}
        onChange={e => setVille(e.target.value)}
        className="border px-3 py-2 rounded"
      >
        <option>Dakar</option>
        <option>Thiès</option>
        <option>Saint-Louis</option>
      </select>
      <button className="bg-blue-600 text-white px-4 py-2 rounded">Ajouter</button>
    </form>
  );
}

Étape 7 — useMemo et useCallback

import { useMemo, useCallback } from "react";

// useMemo: calcul dérivé coûteux, caché tant que les deps ne changent pas
const totalFiltre = useMemo(() =>
  clients.filter(c => c.ville === villeFiltre)
         .reduce((s, c) => s + c.ca, 0),
  [clients, villeFiltre]
);

// useCallback: fonction stable pour éviter re-renders des enfants mémoïsés
const sauvegarder = useCallback((data: Partial<Client>) => {
  return fetch(`/api/clients/${id}`, {
    method: "PATCH",
    body: JSON.stringify(data),
  });
}, [id]);

Étape 8 — Hook personnalisé

// src/hooks/useFetch.ts
import { useEffect, useState } from "react";

export function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [error, setError] = useState<Error | null>(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    const ctrl = new AbortController();
    setLoading(true);
    fetch(url, { signal: ctrl.signal })
      .then(r => r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`)))
      .then(setData)
      .catch(e => { if (e.name !== "AbortError") setError(e); })
      .finally(() => setLoading(false));
    return () => ctrl.abort();
  }, [url]);
  
  return { data, error, loading };
}

// Usage
const { data, loading } = useFetch<Client[]>("/api/clients");

Étape 9 — Contexte pour état global

import { createContext, useContext, useState } from "react";

type Auth = {
  user: { id: number; nom: string } | null;
  login: (u: any) => void;
  logout: () => void;
};

const AuthCtx = createContext<Auth | null>(null);

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<any>(null);
  return (
    <AuthCtx.Provider value={{ user, login: setUser, logout: () => setUser(null) }}>
      {children}
    </AuthCtx.Provider>
  );
}

export function useAuth() {
  const ctx = useContext(AuthCtx);
  if (!ctx) throw new Error("useAuth hors AuthProvider");
  return ctx;
}

// Dans App.tsx, envelopper:
// <AuthProvider><Router /></AuthProvider>

Étape 10 — TanStack Query (React Query)

npm install @tanstack/react-query
// src/main.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient({
  defaultOptions: {
    queries: { staleTime: 60_000, retry: 1 },
  },
});

root.render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>
);
// src/hooks/useClients.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";

export function useClients() {
  return useQuery({
    queryKey: ["clients"],
    queryFn: () => fetch("/api/clients").then(r => r.json()),
  });
}

export function useCreerClient() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: (data: any) =>
      fetch("/api/clients", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data),
      }).then(r => r.json()),
    onSuccess: () => qc.invalidateQueries({ queryKey: ["clients"] }),
  });
}

Étape 11 — React Router

npm install react-router-dom
import { createBrowserRouter, RouterProvider, Outlet, Link } from "react-router-dom";
import { Home } from "./pages/Home";
import { ListeClients } from "./pages/ListeClients";
import { FicheClient } from "./pages/FicheClient";

function Layout() {
  return (
    <>
      <nav className="bg-blue-600 text-white p-4">
        <Link to="/" className="mr-4">Accueil</Link>
        <Link to="/clients">Clients</Link>
      </nav>
      <main className="p-6"><Outlet /></main>
    </>
  );
}

const router = createBrowserRouter([
  {
    path: "/",
    element: <Layout />,
    children: [
      { index: true, element: <Home /> },
      { path: "clients", element: <ListeClients /> },
      { path: "clients/:id", element: <FicheClient /> },
    ],
  },
]);

root.render(<RouterProvider router={router} />);

Étape 12 — Tests avec Vitest

npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom
// vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  test: { environment: "jsdom", globals: true },
});
// src/components/Carte.test.tsx
import { render, screen } from "@testing-library/react";
import { Carte } from "./Carte";

test("affiche le titre et le prix", () => {
  render(<Carte titre="Formation" description="desc" prix={150000} />);
  expect(screen.getByText("Formation")).toBeInTheDocument();
  expect(screen.getByText(/150\s000\s+FCFA/)).toBeInTheDocument();
});

Étape 13 — Accessibilité (a11y)

// Boutons labellisés
<button aria-label="Fermer">
  <XIcon />
</button>

// Contraste minimum 4.5:1
// Focus visible (pas outline: none sans remplacer)
// Hiérarchie h1 > h2 > h3 respectée

// Tester automatiquement
npm install -D @axe-core/react
// axe.run dans les tests

Étape 14 — Build production

npm run build
# Produit /dist optimisé

# Analyser la taille
npm install -D rollup-plugin-visualizer
# vite.config.ts + plugin visualizer()
# Ouvre stats.html après build

# Serveur local pour tester le build
npm run preview

Étape 15 — Déployer

# Vercel
npx vercel --prod

# Netlify
npx netlify deploy --prod --dir=dist

# Cloudflare Pages
npx wrangler pages deploy dist

# Nginx statique
# Copier dist/ vers /var/www/app
# server { try_files $uri $uri/ /index.html; }  # SPA fallback

Checklist

✓ TypeScript strict activé
✓ Composants fonctionnels + hooks (pas de classes)
✓ useEffect avec cleanup (AbortController)
✓ Clés uniques sur listes (.map(key={id}))
✓ useMemo/useCallback sur calculs coûteux uniquement
✓ Context ou TanStack Query pour état partagé
✓ React Router pour navigation
✓ Tests avec Testing Library
✓ a11y: aria-label, contraste, focus visible
✓ Build < 300 KB gzippé
Besoin d'un site web ?

Confiez-nous la Création de Votre Site Web

Site vitrine, e-commerce ou application web — nous transformons votre vision en réalité digitale. Accompagnement personnalisé de A à Z.

À partir de 250.000 FCFA
Parlons de Votre Projet
Publicité