Ce que vous saurez faire à la fin
- Créer un projet React avec Vite et TypeScript en 2 minutes
- Écrire des composants fonctionnels avec hooks
- Gérer formulaires, listes, navigations
- Utiliser React Query pour le data fetching
- 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
- Ouvre
http://localhost:5173. - Structure :
src/App.tsxest le composant racine,src/main.tsxle 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é