React v18
Composants, Hooks, gestion d'état, React Router et patterns essentiels pour construire des interfaces React.
Vite est l'outil recommandé pour créer un projet React — démarrage rapide, HMR instantané et build optimisé.
# Vite (recommandé)
npm create vite@latest mon-app -- --template react
cd mon-app && npm install && npm run dev
# Create React App (classique)
npx create-react-app mon-app
cd mon-app && npm start
# Scripts npm
npm run dev # serveur de développement (Vite)
npm run build # build de production
npm run preview # prévisualiser le build
Un composant React est une fonction qui retourne du JSX. Il peut être déclaré avec function ou comme une arrow function.
// Composant simple
function Bonjour() {
return <h1>Bonjour monde !</h1>;
}
// Arrow function
const Bonjour = () => <h1>Bonjour monde !</h1>;
// Export
export default Bonjour;
export { Bonjour }; // export nommé
// Utilisation
<Bonjour />
// Composant avec plusieurs éléments (Fragment)
function App() {
return (
<>
<Header />
<Main />
<Footer />
</>
);
}
JSX est une extension de syntaxe JavaScript. Les expressions JS s'écrivent entre accolades {}. Les attributs suivent la convention camelCase.
const nom = "Alice";
const estConnecte = true;
const styles = { color: "red", fontSize: "16px" };
// Expressions JS entre accolades {}
<h1>Bonjour {nom} !</h1>
<p>{2 + 2}</p>
<p>{estConnecte ? "Connecté" : "Déconnecté"}</p>
// Attributs JSX (camelCase)
<div className="container"> {/* class → className */}
<label htmlFor="email">Email</label> {/* for → htmlFor */}
<input
type="text"
value={valeur}
onChange={e => setValeur(e.target.value)}
disabled={!estActif} {/* true/false */}
style={{ color: "red", marginTop: "10px" }}
/>
// Auto-fermeture obligatoire pour les balises vides
<br /> <img src="..." alt="..." /> <input />
// Commentaires en JSX
{/* Ceci est un commentaire JSX */}
// Fragments
<React.Fragment key={id}>...</React.Fragment>
<>...</>
// dangerouslySetInnerHTML (attention XSS !)
<div dangerouslySetInnerHTML={{ __html: htmlSanitize }} />
Les props permettent de passer des données d'un composant parent à un composant enfant. Elles sont en lecture seule.
// Passer des props
<UserCard nom="Alice" age={30} actif={true} onClick={handleClick} />
// Recevoir des props
function UserCard({ nom, age, actif = false, onClick }) {
return (
<div className={`card ${actif ? "active" : ""}`} onClick={onClick}>
<h2>{nom}</h2>
<p>{age} ans</p>
</div>
);
}
// Prop children
function Modal({ titre, children, onClose }) {
return (
<div className="modal">
<div className="modal-header">
<h2>{titre}</h2>
<button onClick={onClose}>✕</button>
</div>
<div className="modal-body">{children}</div>
</div>
);
}
// Utilisation
<Modal titre="Connexion" onClose={() => setOpen(false)}>
<LoginForm />
</Modal>
// Spread des props
const props = { nom: "Alice", age: 30 };
<UserCard {...props} />
// PropTypes (validation runtime)
import PropTypes from "prop-types";
UserCard.propTypes = {
nom: PropTypes.string.isRequired,
age: PropTypes.number,
onClick: PropTypes.func,
};
UserCard.defaultProps = { age: 0 };
useState retourne une valeur d'état et une fonction pour la mettre à jour. Chaque appel à la fonction de mise à jour provoque un re-rendu.
import { useState } from "react";
// Valeur simple
const [count, setCount] = useState(0);
const [nom, setNom] = useState("");
const [actif, setActif] = useState(false);
// Objet
const [user, setUser] = useState({ nom: "", email: "" });
// Mise à jour
setCount(count + 1);
setCount(prev => prev + 1); // fonction de mise à jour (sûre)
// Objet : spread pour ne pas perdre les autres propriétés
setUser(prev => ({ ...prev, nom: "Alice" }));
// Tableau
const [items, setItems] = useState([]);
setItems(prev => [...prev, nouvelItem]); // ajouter
setItems(prev => prev.filter(i => i.id !== id)); // supprimer
setItems(prev => prev.map(i => i.id === id ? { ...i, done: true } : i)); // modifier
// Initialisation lazy (pour calculs coûteux)
const [data, setData] = useState(() => JSON.parse(localStorage.getItem("data")) ?? []);
// Exemple formulaire complet
function LoginForm() {
const [form, setForm] = useState({ email: "", password: "" });
const [errors, setErrors] = useState({});
const handleChange = (e) => {
setForm(prev => ({ ...prev, [e.target.name]: e.target.value }));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log(form);
};
return (
<form onSubmit={handleSubmit}>
<input name="email" value={form.email} onChange={handleChange} />
<input name="password" value={form.password} onChange={handleChange} type="password" />
<button type="submit">Connexion</button>
</form>
);
}
useEffect permet d'effectuer des opérations après le rendu : fetch de données, abonnements, manipulation du DOM. Le tableau de dépendances contrôle quand l'effet s'exécute.
import { useEffect } from "react";
// Exécuté après chaque rendu
useEffect(() => {
console.log("Rendu !");
});
// Exécuté une seule fois (montage)
useEffect(() => {
fetchData();
}, []);
// Exécuté quand userId change
useEffect(() => {
fetchUser(userId);
}, [userId]);
// Avec nettoyage (cleanup)
useEffect(() => {
const timer = setInterval(() => setCount(c => c + 1), 1000);
return () => clearInterval(timer); // cleanup au démontage ou avant la prochaine exécution
}, []);
// Fetch de données
useEffect(() => {
const controller = new AbortController();
async function load() {
try {
const res = await fetch(`/api/users/${id}`, { signal: controller.signal });
const data = await res.json();
setUser(data);
} catch (err) {
if (err.name !== "AbortError") setError(err.message);
} finally {
setLoading(false);
}
}
setLoading(true);
load();
return () => controller.abort(); // annule si l'id change avant la fin
}, [id]);
useRef retourne un objet mutable dont la propriété .current persiste entre les rendus sans provoquer de re-rendu lors de sa modification.
import { useRef } from "react";
// Référence DOM
function InputFocus() {
const inputRef = useRef(null);
const focus = () => inputRef.current.focus();
return (
<>
<input ref={inputRef} type="text" />
<button onClick={focus}>Focus</button>
</>
);
}
// Valeur persistante SANS re-rendu (ex: timer, valeur précédente)
function Chrono() {
const [time, setTime] = useState(0);
const timerRef = useRef(null);
const start = () => {
timerRef.current = setInterval(() => setTime(t => t + 1), 1000);
};
const stop = () => clearInterval(timerRef.current);
return (
<>
<p>{time}s</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</>
);
}
// Garder la valeur précédente
function usePrevious(value) {
const ref = useRef();
useEffect(() => { ref.current = value; });
return ref.current;
}
useContext permet de consommer un contexte React sans avoir à passer des props à chaque niveau de l'arbre de composants.
import { createContext, useContext, useState } from "react";
// 1. Créer le contexte
const ThemeContext = createContext("light");
const AuthContext = createContext(null);
// 2. Fournir le contexte
function App() {
const [theme, setTheme] = useState("dark");
const [user, setUser] = useState(null);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<AuthContext.Provider value={{ user, setUser }}>
<Router />
</AuthContext.Provider>
</ThemeContext.Provider>
);
}
// 3. Consommer le contexte
function Navbar() {
const { user } = useContext(AuthContext);
const { theme, setTheme } = useContext(ThemeContext);
return (
<nav className={theme}>
<span>{user?.nom ?? "Invité"}</span>
<button onClick={() => setTheme(t => t === "dark" ? "light" : "dark")}>
Toggle Thème
</button>
</nav>
);
}
// Custom hook pour le contexte
export const useAuth = () => useContext(AuthContext);
export const useTheme = () => useContext(ThemeContext);
useMemo mémoïse le résultat d'un calcul coûteux. useCallback mémoïse une fonction pour éviter de recréer sa référence à chaque rendu.
import { useMemo, useCallback } from "react";
// useMemo — mémoïse le RÉSULTAT d'un calcul coûteux
const total = useMemo(() => {
return items.reduce((sum, item) => sum + item.prix, 0);
}, [items]); // recalcule seulement si items change
// useCallback — mémoïse une FONCTION
const handleDelete = useCallback((id) => {
setItems(prev => prev.filter(item => item.id !== id));
}, []); // la fonction ne change jamais de référence
// useCallback avec dépendance
const handleSearch = useCallback((query) => {
return items.filter(item => item.nom.includes(query));
}, [items]);
// Quand utiliser ?
// useMemo : calculs lourds, filtres/tris sur grandes listes
// useCallback : fonctions passées à des composants enfants avec React.memo
Un custom hook est une fonction dont le nom commence par use et qui peut appeler d'autres hooks. Ils permettent d'extraire et réutiliser de la logique avec état.
// Hook de fetch générique
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch(url)
.then(res => { if (!res.ok) throw new Error(res.statusText); return res.json(); })
.then(data => { setData(data); setLoading(false); })
.catch(err => { setError(err.message); setLoading(false); });
}, [url]);
return { data, loading, error };
}
// Utilisation
const { data: users, loading, error } = useFetch("/api/users");
// Hook localStorage
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() =>
JSON.parse(localStorage.getItem(key)) ?? initialValue
);
const set = (val) => {
setValue(val);
localStorage.setItem(key, JSON.stringify(val));
};
return [value, set];
}
// Hook media query
function useMediaQuery(query) {
const [matches, setMatches] = useState(() => window.matchMedia(query).matches);
useEffect(() => {
const media = window.matchMedia(query);
const handler = () => setMatches(media.matches);
media.addEventListener("change", handler);
return () => media.removeEventListener("change", handler);
}, [query]);
return matches;
}
const isMobile = useMediaQuery("(max-width: 768px)");
Dans un formulaire contrôlé, React est la source de vérité pour la valeur des champs via value et onChange.
// Formulaire contrôlé
function RegisterForm() {
const [form, setForm] = useState({ nom: "", email: "", password: "" });
const [errors, setErrors] = useState({});
const [loading, setLoading] = useState(false);
const handleChange = (e) =>
setForm(prev => ({ ...prev, [e.target.name]: e.target.value }));
const validate = () => {
const err = {};
if (!form.nom) err.nom = "Nom requis";
if (!form.email.includes("@")) err.email = "Email invalide";
if (form.password.length < 8) err.password = "8 caractères minimum";
return err;
};
const handleSubmit = async (e) => {
e.preventDefault();
const err = validate();
if (Object.keys(err).length > 0) return setErrors(err);
setLoading(true);
try {
await api("/api/register", { method: "POST", body: form });
} finally { setLoading(false); }
};
return (
<form onSubmit={handleSubmit}>
<input name="nom" value={form.nom} onChange={handleChange} />
{errors.nom && <span className="error">{errors.nom}</span>}
<input name="email" type="email" value={form.email} onChange={handleChange} />
{errors.email && <span className="error">{errors.email}</span>}
<input name="password" type="password" value={form.password} onChange={handleChange} />
{errors.password && <span className="error">{errors.password}</span>}
<button type="submit" disabled={loading}>
{loading ? "Chargement..." : "S'inscrire"}
</button>
</form>
);
}
L'attribut key est obligatoire lors du rendu de listes. Il doit être unique et stable — préférer un identifiant métier plutôt que l'index du tableau.
// Rendu de liste
const users = [
{ id: 1, nom: "Alice", role: "admin" },
{ id: 2, nom: "Bob", role: "user" },
];
function UserList({ users }) {
if (!users.length) return <p>Aucun utilisateur.</p>;
return (
<ul>
{users.map(user => (
<UserItem key={user.id} user={user} /> // key OBLIGATOIRE, unique, stable
))}
</ul>
);
}
// Clés : utiliser un id unique, PAS l'index (sauf liste statique)
// ❌ {items.map((item, index) => <li key={index}>)}
// ✅ {items.map(item => <li key={item.id}>)}
// Filtrer et trier
{users
.filter(u => u.role === "admin")
.sort((a, b) => a.nom.localeCompare(b.nom))
.map(u => <UserItem key={u.id} user={u} />)
}
React offre plusieurs patterns pour afficher conditionnellement des éléments. Attention au court-circuit && avec les valeurs falsy numériques.
// if/else avec JSX
function Status({ estConnecte }) {
if (estConnecte) return <p>Bienvenue !</p>;
return <p>Veuillez vous connecter.</p>;
}
// Opérateur ternaire
<div>{estConnecte ? <Dashboard /> : <LoginPage />}</div>
// Court-circuit && (attention : ne jamais mettre 0 !)
{isLoading && <Spinner />}
{error && <ErrorMessage message={error} />}
{!!users.length && <UserList users={users} />} // !! pour éviter le 0
// Rendu null (ne rien afficher)
if (!data) return null;
// État de chargement + erreur + données
function UserPage({ id }) {
const { data, loading, error } = useFetch(`/api/users/${id}`);
if (loading) return <div className="spinner">Chargement...</div>;
if (error) return <div className="error">Erreur: {error}</div>;
if (!data) return null;
return <UserCard user={data} />;
}
React Router v6 utilise <Routes> et <Route> pour définir les routes. Les routes imbriquées s'affichent via <Outlet />.
import { BrowserRouter, Routes, Route, Link, NavLink, useNavigate, useParams, useSearchParams, Outlet } from "react-router-dom";
// npm install react-router-dom
// Configuration des routes
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/users" element={<UsersPage />} />
<Route path="/users/:id" element={<UserDetail />} />
<Route path="/about" element={<AboutPage />} />
<Route path="*" element={<NotFound />} /> {/* 404 */}
{/* Routes imbriquées */}
<Route path="/dashboard" element={<Dashboard />}>
<Route index element={<DashboardHome />} />
<Route path="profil" element={<Profil />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
</BrowserRouter>
);
}
// Outlet (rendu des routes enfants)
function Dashboard() {
return (
<div>
<Sidebar />
<main><Outlet /></main> {/* les routes enfants s'affichent ici */}
</div>
);
}
// Navigation
<Link to="/users">Utilisateurs</Link>
<NavLink to="/users" className={({ isActive }) => isActive ? "active" : ""}>
Utilisateurs
</NavLink>
// Hooks de navigation
function UserDetail() {
const { id } = useParams(); // paramètres URL (/users/:id)
const [params] = useSearchParams(); // query string (?page=2)
const navigate = useNavigate(); // navigation programmatique
const page = params.get("page") ?? 1;
return (
<div>
<p>User ID: {id}</p>
<button onClick={() => navigate("/users")}>Retour</button>
<button onClick={() => navigate(-1)}>Page précédente</button>
</div>
);
}
// Route protégée
function ProtectedRoute({ children }) {
const { user } = useAuth();
if (!user) return <Navigate to="/login" replace />;
return children;
}
// Usage: <Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
Zustand est une solution de gestion d'état légère et sans boilerplate. Le store est accessible depuis n'importe quel composant sans Provider.
// npm install zustand
import { create } from "zustand";
// Créer un store
const useCartStore = create((set, get) => ({
items: [],
total: 0,
addItem: (product) =>
set(state => ({
items: [...state.items, product],
total: state.total + product.prix,
})),
removeItem: (id) =>
set(state => {
const item = state.items.find(i => i.id === id);
return {
items: state.items.filter(i => i.id !== id),
total: state.total - (item?.prix ?? 0),
};
}),
clearCart: () => set({ items: [], total: 0 }),
itemCount: () => get().items.length,
}));
// Utilisation dans n'importe quel composant
function CartButton() {
const { items, addItem } = useCartStore();
return <button>{items.length} articles</button>;
}
function ProductCard({ product }) {
const addItem = useCartStore(state => state.addItem);
return <button onClick={() => addItem(product)}>Ajouter au panier</button>;
}
Aucun résultat pour votre recherche.