Chapitre 7
Chapitre 07 — React
Chapitre 07 — React
Chapitre 07 — React : Cours Complet
1. Histoire et Fondamentaux
1.1 Le problème de la manipulation DOM
Avant React, les frameworks comme jQuery manipulaient directement le DOM. Problème : à mesure que les applications grandissaient, la gestion des mises à jour devenait un cauchemar.
Démo du problème :
// jQuery — manipulation impérative
$("#userList").append(`<li>${user.name}</li>`);
$("#userList li").click(function() {
$(this).toggleClass("selected");
});
// React — approche déclarative
function UserList({ users }) {
return (
<ul>
{users.map(user => (
<li key={user.id} onClick={handleClick}>{user.name}</li>
))}
</ul>
);
}
React introduit le Virtual DOM : une représentation légère du DOM réel en mémoire. Quand l'état change, React :
- Crée un nouveau Virtual DOM
- Le compare avec le précédent (Diffing)
- Calcule le minimum de mutations nécessaires (Reconciliation)
1.2 React Fiber (React 16+)
En 2017, React réécrit son moteur de réconciliation : Fiber. Objectifs :
- Incremental rendering : découper le travail en unités et le répartir sur plusieurs frames
- Priorité : les mises à jour urgentes (input utilisateur) passent avant les mises à jour différées (animations)
- Suspense : permettre à React d'attendre du contenu asynchrone
// Concept : chaque fibre est une unité de travail
interface Fiber {
tag: WorkTag; // type de fibre (composant, host, etc.)
key: string | null;
type: any; // fonction composant, classe, string pour DOM
stateNode: any; // instance du composant
child: Fiber | null;
sibling: Fiber | null;
return: Fiber | null; // parent
pendingProps: any;
memoizedProps: any;
memoizedState: any;
effectTag: SideEffectTag; // PLACEMENT, UPDATE, DELETION
alternate: Fiber | null; // lien vers l'ancienne fibre (double buffering)
}
1.3 JSX : Pourquoi et comment ça marche ?
JSX est un sucre syntaxique pour React.createElement.
// JSX
const element = <h1 className="title">Hello, World!</h1>;
// Ce que JSX compile
const element = React.createElement(
"h1",
{ className: "title" },
"Hello, World!"
);
Le compilateur (Babel, TypeScript via jsx: "react-jsx") transforme JSX en appels jsx() (React 17+), ce qui permet d'importer React automatiquement.
2. Composants : Function vs Class
2.1 Function Components (moderne)
interface GreetingProps {
name: string;
age?: number;
}
const Greeting: React.FC<GreetingProps> = ({ name, age }) => {
return (
<div>
<h1>Bonjour, {name}!</h1>
{age && <p>Tu as {age} ans.</p>}
</div>
);
};
2.2 Class Components (legacy, encore supporté)
interface CounterState {
count: number;
}
class Counter extends React.Component<{}, CounterState> {
state: CounterState = { count: 0 };
componentDidMount() {
console.log("Mounted");
}
componentDidUpdate(prevProps: {}, prevState: CounterState) {
if (prevState.count !== this.state.count) {
console.log(`Count changed: ${this.state.count}`);
}
}
componentWillUnmount() {
console.log("Will unmount");
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
+
</button>
</div>
);
}
}
Depuis React 19, les Class Components sont dépréciés — utilisez des fonctions et des hooks.
3. Les Hooks
3.1 useState
const [state, setState] = useState<number>(0);
// Fonction de mise à jour
setState(prev => prev + 1);
// Lazy initializer
const [data] = useState(() => expensiveComputation());
3.2 useEffect
useEffect(() => {
// Effet : s'exécute après le rendu
const subscription = api.subscribe(id, handleUpdate);
// Cleanup : s'exécute avant le prochain effet ou au démontage
return () => {
subscription.unsubscribe();
};
}, [id]); // Se ré-exécute si `id` change
// useEffect sans dépendances (s'exécute à chaque rendu) — rare
useEffect(() => {
console.log("Rendu");
});
// useEffect avec [] (s'exécute une fois au montage)
useEffect(() => {
fetchData();
}, []);
3.3 useContext
interface ThemeContextType {
theme: "light" | "dark";
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within ThemeProvider");
}
return context;
}
// Provider
function App() {
const [theme, setTheme] = useState<"light" | "dark">("light");
const toggleTheme = () => setTheme(t => t === "light" ? "dark" : "light");
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<MainContent />
</ThemeContext.Provider>
);
}
// Consumer
function MainContent() {
const { theme, toggleTheme } = useTheme();
return <div className={theme}>...<button onClick={toggleTheme}>Toggle</button></div>;
}
3.4 useRef
// Référence à un élément DOM
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
// Valeur mutable persistante entre les rendus
const intervalRef = useRef<number | null>(null);
intervalRef.current = setInterval(() => {}, 1000);
// Stocker la valeur précédente
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
3.5 useMemo et useCallback
// useMemo : mémoïse une valeur calculée
const sortedItems = useMemo(() => {
return items.sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
// useCallback : mémoïse une fonction
const handleClick = useCallback((e: React.MouseEvent) => {
console.log("Clicked at", e.clientX, e.clientY);
}, []);
// Quand les utiliser ?
// - useMemo : calculs coûteux, références stables pour les enfants memoïsés
// - useCallback : passer des callbacks à des composants memoïsés
// Ne pas les utiliser systématiquement — ils ont un coût
3.6 useReducer
type TaskAction =
| { type: "ADD"; payload: string }
| { type: "TOGGLE"; payload: number }
| { type: "DELETE"; payload: number }
| { type: "SET_FILTER"; payload: Filter };
interface TaskState {
tasks: Task[];
filter: Filter;
}
function taskReducer(state: TaskState, action: TaskAction): TaskState {
switch (action.type) {
case "ADD":
return {
...state,
tasks: [...state.tasks, { id: Date.now(), text: action.payload, done: false }],
};
case "TOGGLE":
return {
...state,
tasks: state.tasks.map(t =>
t.id === action.payload ? { ...t, done: !t.done } : t
),
};
case "DELETE":
return {
...state,
tasks: state.tasks.filter(t => t.id !== action.payload),
};
case "SET_FILTER":
return { ...state, filter: action.payload };
default:
return state;
}
}
function TaskApp() {
const [state, dispatch] = useReducer(taskReducer, { tasks: [], filter: "all" });
}
3.7 useLayoutEffect
// Similaire à useEffect mais synchrone (avant le paint)
useLayoutEffect(() => {
// Mesures DOM, animations, scroll position
const rect = elementRef.current?.getBoundingClientRect();
if (rect) {
setPosition({ x: rect.x, y: rect.y });
}
}, []);
// N'utilisez useLayoutEffect que si vous voyez un flash / flicker
// useEffect est généralement suffisant
3.8 useId (React 18+)
function Checkbox() {
const id = useId();
return (
<div>
<label htmlFor={id}>Accept terms</label>
<input id={id} type="checkbox" />
</div>
);
}
// Génère des IDs uniques pour l'accessibilité et le SSR hydration
3.9 useTransition (React 18+)
function SearchPage() {
const [query, setQuery] = useState("");
const [isPending, startTransition] = useTransition();
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
// Urgent : mettre à jour l'input immédiatement
setQuery(e.target.value);
// Non urgent : lancer la recherche (peut être interrompue)
startTransition(() => {
setSearchQuery(e.target.value);
});
}
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<SearchResults query={searchQuery} />
</div>
);
}
3.10 useDeferredValue (React 18+)
function SearchPage({ query }: { query: string }) {
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
return (
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<SearchResults query={deferredQuery} />
</div>
);
}
// Similaire à useTransition mais pour les valeurs, pas les fonctions
3.11 useOptimistic (React 19)
function MessageThread({ messages, sendMessage }: MessageThreadProps) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage: string) => [...state, { text: newMessage, sending: true }]
);
async function handleSubmit(formData: FormData) {
const message = formData.get("message") as string;
addOptimisticMessage(message);
await sendMessage(message); // La mise à jour réelle remplace l'optimistic
}
return (
<form action={handleSubmit}>
{optimisticMessages.map((msg, i) => (
<div key={i}>
{msg.text} {msg.sending && <Spinner />}
</div>
))}
<input name="message" />
<button type="submit">Send</button>
</form>
);
}
3.12 useSyncExternalStore (React 18+)
function useOnlineStatus() {
const isOnline = useSyncExternalStore(
(callback) => {
window.addEventListener("online", callback);
window.addEventListener("offline", callback);
return () => {
window.removeEventListener("online", callback);
window.removeEventListener("offline", callback);
};
},
() => navigator.onLine, // Snapshot côté client
() => true // Snapshot côté serveur (SSR)
);
return isOnline;
}
// Pour s'abonner à des stores externes non-React (Redux, Zustand, etc.)
4. Rules of Hooks
- Appelez les Hooks uniquement au niveau racine — pas dans des conditions, boucles ou fonctions imbriquées
- Appelez les Hooks uniquement depuis des composants React ou des Custom Hooks — pas depuis des fonctions JavaScript classiques
// ❌ FAUX
function BadComponent({ condition }: { condition: boolean }) {
if (condition) {
const [value, setValue] = useState(0); // Violation de la règle 1
}
}
// ✅ CORRECT
function GoodComponent({ condition }: { condition: boolean }) {
const [value, setValue] = useState(0); // Toujours au même endroit
// Utiliser condition dans le JSX ou useEffect
}
Pourquoi ? React utilise l'ordre d'appel des hooks pour associer chaque hook à son état. Si l'ordre change entre deux rendus, les états sont mélangés.
5. Gestion d'État
5.1 Stratégies de gestion d'état
| Type d'état | Solution | Quand l'utiliser |
|---|---|---|
| Local | useState | État propre à un composant |
| Composant voisin | useState + Lifting | Composants parent/enfant proches |
| Arbre profond | useContext | Éviter le prop drilling |
| Formulaires complexes | useReducer | Logique d'état complexe |
| Global (données serveur) | TanStack Query / SWR | Cache, revalidation, pagination |
| Global (état client) | Zustand / Redux Toolkit | État partagé entre pages ou modules |
| URL | useSearchParams | État partagé via URL (filtres, pagination) |
5.2 Context + useReducer (état local global)
interface AuthState {
user: User | null;
isLoading: boolean;
}
type AuthAction =
| { type: "LOGIN_START" }
| { type: "LOGIN_SUCCESS"; payload: User }
| { type: "LOGIN_ERROR"; payload: string }
| { type: "LOGOUT" };
function authReducer(state: AuthState, action: AuthAction): AuthState {
switch (action.type) {
case "LOGIN_START":
return { ...state, isLoading: true };
case "LOGIN_SUCCESS":
return { user: action.payload, isLoading: false };
case "LOGIN_ERROR":
return { user: null, isLoading: false };
case "LOGOUT":
return { user: null, isLoading: false };
default:
return state;
}
}
5.3 Zustand (moderne, minimaliste)
import { create } from "zustand";
import { persist } from "zustand/middleware";
interface CartStore {
items: CartItem[];
total: number;
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
clearCart: () => void;
}
const useCartStore = create<CartStore>()(
persist(
(set, get) => ({
items: [],
total: 0,
addItem: (item) =>
set((state) => ({
items: [...state.items, item],
total: state.total + item.price * item.quantity,
})),
removeItem: (id) =>
set((state) => ({
items: state.items.filter((i) => i.id !== id),
total: state.items
.filter((i) => i.id !== id)
.reduce((sum, i) => sum + i.price * i.quantity, 0),
})),
clearCart: () => set({ items: [], total: 0 }),
}),
{ name: "cart-storage" }
)
);
5.4 Redux Toolkit (industriel)
import { createSlice, configureStore, PayloadAction } from "@reduxjs/toolkit";
interface CounterState {
value: number;
}
const counterSlice = createSlice({
name: "counter",
initialState: { value: 0 } as CounterState,
reducers: {
increment: (state) => { state.value += 1; },
decrement: (state) => { state.value -= 1; },
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export const store = configureStore({
reducer: { counter: counterSlice.reducer },
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
6. Patterns de Composition
6.1 Compound Components
interface TabsContextType {
activeTab: string;
setActiveTab: (tab: string) => void;
}
const TabsContext = createContext<TabsContextType | undefined>(undefined);
function Tabs({ children, defaultTab }: { children: React.ReactNode; defaultTab: string }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
{children}
</TabsContext.Provider>
);
}
Tabs.Tab = function Tab({ id, children }: { id: string; children: React.ReactNode }) {
const { activeTab, setActiveTab } = useContext(TabsContext)!;
return (
<button
className={activeTab === id ? "active" : ""}
onClick={() => setActiveTab(id)}
>
{children}
</button>
);
};
Tabs.Panel = function Panel({ id, children }: { id: string; children: React.ReactNode }) {
const { activeTab } = useContext(TabsContext)!;
return activeTab === id ? <div>{children}</div> : null;
};
// Utilisation
<Tabs defaultTab="profile">
<Tabs.Tab id="profile">Profil</Tabs.Tab>
<Tabs.Tab id="settings">Paramètres</Tabs.Tab>
<Tabs.Panel id="profile">Contenu du profil</Tabs.Panel>
<Tabs.Panel id="settings">Contenu des paramètres</Tabs.Panel>
</Tabs>
6.2 Render Props
interface MouseTrackerProps {
render: (position: { x: number; y: number }) => React.ReactNode;
}
function MouseTracker({ render }: MouseTrackerProps) {
const [position, setPosition] = useState({ x: 0, y: 0 });
return (
<div onMouseMove={(e) => setPosition({ x: e.clientX, y: e.clientY })}>
{render(position)}
</div>
);
}
// Utilisation
<MouseTracker
render={({ x, y }) => (
<p>La souris est à ({x}, {y})</p>
)}
/>
6.3 Higher-Order Component (HOC)
function withAuth<P extends object>(Component: React.ComponentType<P>) {
return function AuthenticatedComponent(props: P) {
const { user } = useAuth();
if (!user) {
return <Redirect to="/login" />;
}
return <Component {...props} user={user} />;
};
}
const DashboardPage = withAuth(Dashboard);
6.4 Custom Hooks
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
};
return [storedValue, setValue] as const;
}
7. Performance
7.1 React.memo
const ExpensiveList = React.memo(function ExpensiveList({ items }: { items: Item[] }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
});
// Ne se re-rend que si les props changent (shallow comparison)
7.2 Code Splitting + Suspense
const AdminPanel = lazy(() => import("./AdminPanel"));
const Dashboard = lazy(() => import("./Dashboard"));
function App() {
return (
<Suspense fallback={<BigSpinner />}>
<Routes>
<Route path="/admin" element={<AdminPanel />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
);
}
7.3 useMemo et useCallback — guide pratique
// À utiliser
const sorted = useMemo(() => sortLargeArray(data), [data]); // Calcul coûteux
const handleSubmit = useCallback((e) => {
formRef.current?.submit(e);
}, []); // Référence stable pour composants memoïsés
// Éviter
const a = useMemo(() => 1, []); // Trop simple
const b = useCallback(() => fn(), []); // useCallback(fn, []) === fn
8. React Server Components (RSC)
Les Server Components (React 18+, Next.js 13+ App Router) s'exécuent sur le serveur et ne sont jamais envoyés au client.
// ServerComponent.server.tsx — s'exécute sur le serveur
async function NoteList({ category }: { category: string }) {
const notes = await db.query(
"SELECT * FROM notes WHERE category = ?",
[category]
);
return (
<ul>
{notes.map(note => (
<li key={note.id}>{note.title}</li>
))}
</ul>
);
}
// ClientComponent.client.tsx — s'exécute sur le client
("use client");
function InteractiveButton({ onClick }: { onClick: () => void }) {
return <button onClick={onClick}>Click me</button>;
}
9. React 19 Nouveautés
// 1. Actions (formulaires natifs)
function AddTodo() {
async function addTodo(formData: FormData) {
"use server";
const title = formData.get("title");
await db.todos.create({ title });
}
return (
<form action={addTodo}>
<input name="title" />
<button type="submit">Add</button>
</form>
);
}
// 2. use() — lecture de promesses et contextes
function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
const comments = use(commentsPromise);
return comments.map(c => <Comment key={c.id} {...c} />);
}
// 3. ref comme prop directe
function Input({ ref, ...props }: { ref: Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}
// Plus besoin de forwardRef!
10. Error Boundaries
import { Component, ErrorInfo, ReactNode } from "react";
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
}
interface ErrorBoundaryState {
hasError: boolean;
error?: Error;
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
state: ErrorBoundaryState = { hasError: false };
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, info: ErrorInfo) {
console.error("Uncaught error:", error, info.componentStack);
}
render() {
if (this.state.hasError) {
return this.props.fallback || <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
11. Portals
function Modal({ isOpen, onClose, children }: ModalProps) {
if (!isOpen) return null;
return createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>,
document.getElementById("portal-root")!
);
}
12. Anti-patterns et Erreurs des Juniors
// ❌ 1. Modifier l'état directement
const [items, setItems] = useState([1, 2, 3]);
items.push(4); // Mutation directe — ne déclenche pas de re-render
setItems(items); // Ne marche pas (même référence)
// ✅ CORRECT
setItems([...items, 4]);
// ❌ 2. useEffect sans dépendances quand nécessaire
useEffect(() => {
setCount(count + 1); // Stale closure — count est capturé
}, []); // count devrait être dans les dépendances
// ❌ 3. Trop de useState
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [age, setAge] = useState(0);
// ✅ useReducer ou objet
// ❌ 4. Clés non uniques dans les listes
items.map((item) => <li key={item.index}>{item.name}</li>);
// ✅ Clé stable et unique : key={item.id}
// ❌ 5. setState dans useEffect sans condition
useEffect(() => {
fetch("/api/data").then(setData);
}, []); // Pas de cleanup — risque de memory leak si le composant est démonté avant la fin
// ✅ CORRECT
useEffect(() => {
let cancelled = false;
fetch("/api/data").then(data => {
if (!cancelled) setData(data);
});
return () => { cancelled = true; };
}, []);