Modern Frontend Engineering
Chapitre 20
Chapitre 20 — Corrections
> Corrigés détaillés des 40 exercices du chapitre 18, avec solutions complètes, explications, et notes d'optimisation.
Corrigés des Exercices
Introduction
Ce chapitre fournit les corrigés détaillés de tous les exercices du chapitre 18. Chaque corrigé comprend :
- La solution complète avec code
- L'explication de la démarche
- Les erreurs fréquentes à éviter
- Les optimisations possibles
- Le niveau de difficulté
Niveau Débutant
Exercice 1 : Page HTML sémantique
Objectif : Créer une page HTML structurée avec les balises sémantiques.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mon article de blog</title>
</head>
<body>
<header>
<nav>
<ul>
<li><a href="/">Accueil</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>Titre de l'article</h1>
<p>Publié le <time datetime="2026-07-28">28 juillet 2026</time></p>
<section>
<h2>Introduction</h2>
<p>Contenu de l'introduction...</p>
</section>
<section>
<h2>Développement</h2>
<p>Contenu principal...</p>
</section>
</article>
<aside>
<h2>Articles similaires</h2>
<ul>
<li><a href="/article-2">Article 2</a></li>
<li><a href="/article-3">Article 3</a></li>
</ul>
</aside>
</main>
<footer>
<p>© 2026 Mon Blog</p>
</footer>
</body>
</html>
Explication : L'utilisation de balises sémantiques (header, nav, main, article, section, aside, footer) améliore le SEO, l'accessibilité et la maintenabilité.
Erreurs fréquentes :
- Utiliser des
divpour tout (div-itis) - Oublier le
langdans<html> - Absence de
main - Mauvais nesting des headings
Exercice 2 : Layout CSS Flexbox
Objectif : Créer une barre de navigation responsive avec Flexbox.
.nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
background: #1a1a2e;
color: white;
}
.nav__logo {
font-size: 1.5rem;
font-weight: bold;
}
.nav__links {
display: flex;
gap: 1.5rem;
list-style: none;
}
.nav__links a {
color: white;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 4px;
transition: background 0.2s;
}
.nav__links a:hover {
background: rgba(255, 255, 255, 0.1);
}
.nav__toggle {
display: none;
background: none;
border: none;
color: white;
font-size: 1.5rem;
}
@media (max-width: 768px) {
.nav__links {
display: none;
flex-direction: column;
position: absolute;
top: 100%;
left: 0;
right: 0;
background: #1a1a2e;
padding: 1rem;
}
.nav__links.is-active {
display: flex;
}
.nav__toggle {
display: block;
}
}
Exercice 3 : Fonctions JavaScript
Objectif : Implémenter une fonction de debounce.
function debounce(fn, delay) {
let timeoutId = null;
return function (...args) {
const context = this;
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
fn.apply(context, args);
timeoutId = null;
}, delay);
};
}
// Utilisation
const handleSearch = debounce((query) => {
console.log('Recherche:', query);
}, 300);
handleSearch('a');
handleSearch('ab');
handleSearch('abc');
// Seul 'abc' sera loggé après 300ms
Explication : Le debounce attend que l'utilisateur arrête de taper avant d'exécuter la fonction, évitant les appels API superflus.
Exercice 4 : Manipulation du DOM
class Accordion {
constructor(container) {
this.container = container;
this.items = container.querySelectorAll('.accordion-item');
this.init();
}
init() {
this.items.forEach(item => {
const header = item.querySelector('.accordion-header');
header.addEventListener('click', () => this.toggle(item));
});
}
toggle(item) {
const isOpen = item.classList.contains('is-open');
// Fermer tous les autres
this.items.forEach(i => {
i.classList.remove('is-open');
i.querySelector('.accordion-content').style.maxHeight = '0';
});
// Ouvrir si fermé
if (!isOpen) {
item.classList.add('is-open');
const content = item.querySelector('.accordion-content');
content.style.maxHeight = content.scrollHeight + 'px';
}
}
}
Exercice 5 : Git Workflow
Commandes :
git checkout -b feature/navbar
git add .
git commit -m "feat(navbar): add responsive navigation component"
git push -u origin feature/navbar
# Create PR on GitHub
git checkout main
git pull
git merge feature/navbar
git push
Niveau Intermédiaire
Exercice 6 : Custom Hook React
import { useState, useEffect, useCallback } from 'react';
interface UseApiResult<T> {
data: T | null;
loading: boolean;
error: Error | null;
refetch: () => void;
}
function useApi<T>(url: string): UseApiResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const [refetchCount, setRefetchCount] = useState(0);
const refetch = useCallback(() => {
setRefetchCount(c => c + 1);
}, []);
useEffect(() => {
let cancelled = false;
async function fetchData() {
setLoading(true);
setError(null);
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const json = await response.json();
if (!cancelled) {
setData(json);
}
} catch (err) {
if (!cancelled) {
setError(err instanceof Error ? err : new Error('Unknown error'));
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchData();
return () => { cancelled = true; };
}, [url, refetchCount]);
return { data, loading, error, refetch };
}
Exercice 7 : TypeScript Generics
// Builder pattern générique
class QueryBuilder<T extends Record<string, unknown>> {
private conditions: string[] = [];
private orderField?: keyof T;
private orderDirection: 'ASC' | 'DESC' = 'ASC';
private limitCount?: number;
where<K extends keyof T>(field: K, operator: string, value: T[K]): this {
const escaped = typeof value === 'string' ? `'${value}'` : value;
this.conditions.push(`${String(field)} ${operator} ${escaped}`);
return this;
}
orderBy(field: keyof T, direction: 'ASC' | 'DESC' = 'ASC'): this {
this.orderField = field;
this.orderDirection = direction;
return this;
}
limit(count: number): this {
this.limitCount = count;
return this;
}
build(): string {
let query = `SELECT * FROM ${this.getTableName()}`;
if (this.conditions.length > 0) {
query += ` WHERE ${this.conditions.join(' AND ')}`;
}
if (this.orderField) {
query += ` ORDER BY ${String(this.orderField)} ${this.orderDirection}`;
}
if (this.limitCount) {
query += ` LIMIT ${this.limitCount}`;
}
return query;
}
private getTableName(): string {
return 'items';
}
}
// Utilisation
interface User {
id: number;
name: string;
email: string;
age: number;
}
const query = new QueryBuilder<User>()
.where('age', '>=', 18)
.where('name', 'LIKE', '%John%')
.orderBy('name', 'ASC')
.limit(10)
.build();
Exercice 8 : Tests avec Vitest
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { SearchComponent } from './SearchComponent';
describe('SearchComponent', () => {
const mockOnSearch = vi.fn();
beforeEach(() => {
mockOnSearch.mockClear();
});
it('renders search input', () => {
render(<SearchComponent onSearch={mockOnSearch} />);
expect(screen.getByRole('searchbox')).toBeDefined();
});
it('calls onSearch when typing after debounce', async () => {
const user = userEvent.setup();
render(<SearchComponent onSearch={mockOnSearch} debounceMs={300} />);
await user.type(screen.getByRole('searchbox'), 'hello');
// Debounce attend 300ms
await new Promise(r => setTimeout(r, 350));
expect(mockOnSearch).toHaveBeenCalledWith('hello');
});
it('shows loading state', () => {
render(<SearchComponent onSearch={mockOnSearch} loading={true} />);
expect(screen.getByRole('status')).toBeDefined();
});
});
Exercice 9 : Appels API avec gestion d'erreur
class ApiClient {
private baseUrl: string;
private interceptors: Array<(config: RequestInit) => RequestInit> = [];
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
addInterceptor(fn: (config: RequestInit) => RequestInit) {
this.interceptors.push(fn);
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
let config: RequestInit = {
headers: {
'Content-Type': 'application/json',
},
...options,
};
for (const interceptor of this.interceptors) {
config = interceptor(config);
}
const response = await fetch(`${this.baseUrl}${endpoint}`, config);
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new ApiError(
error.message || response.statusText,
response.status,
error
);
}
return response.json();
}
async get<T>(endpoint: string): Promise<T> {
return this.request<T>(endpoint, { method: 'GET' });
}
async post<T>(endpoint: string, body: unknown): Promise<T> {
return this.request<T>(endpoint, {
method: 'POST',
body: JSON.stringify(body),
});
}
}
class ApiError extends Error {
constructor(
message: string,
public status: number,
public data: unknown
) {
super(message);
this.name = 'ApiError';
}
}
Exercice 10 : State Management avec Context
import React, { createContext, useContext, useReducer, ReactNode } from 'react';
// State
interface AuthState {
user: User | null;
isAuthenticated: boolean;
}
// Actions
type AuthAction =
| { type: 'LOGIN'; payload: User }
| { type: 'LOGOUT' }
| { type: 'UPDATE_PROFILE'; payload: Partial<User> };
// Reducer
function authReducer(state: AuthState, action: AuthAction): AuthState {
switch (action.type) {
case 'LOGIN':
return { user: action.payload, isAuthenticated: true };
case 'LOGOUT':
return { user: null, isAuthenticated: false };
case 'UPDATE_PROFILE':
if (!state.user) return state;
return { ...state, user: { ...state.user, ...action.payload } };
default:
return state;
}
}
// Context
interface AuthContextType extends AuthState {
login: (user: User) => void;
logout: () => void;
updateProfile: (data: Partial<User>) => void;
}
const AuthContext = createContext<AuthContextType | null>(null);
// Provider
function AuthProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(authReducer, {
user: null,
isAuthenticated: false,
});
const login = (user: User) => dispatch({ type: 'LOGIN', payload: user });
const logout = () => dispatch({ type: 'LOGOUT' });
const updateProfile = (data: Partial<User>) =>
dispatch({ type: 'UPDATE_PROFILE', payload: data });
return (
<AuthContext.Provider value={{ ...state, login, logout, updateProfile }}>
{children}
</AuthContext.Provider>
);
}
// Hook
function useAuth(): AuthContextType {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
Niveau Avancé
Exercice 11 : Architecture Clean Architecture
// Domain Layer - Entities
interface TodoEntity {
id: string;
title: string;
completed: boolean;
createdAt: Date;
}
// Domain Layer - Use Cases
class CreateTodoUseCase {
constructor(private todoRepository: TodoRepository) {}
async execute(title: string): Promise<TodoEntity> {
if (!title || title.trim().length === 0) {
throw new Error('Title is required');
}
if (title.length > 100) {
throw new Error('Title must be less than 100 characters');
}
return this.todoRepository.save({
id: crypto.randomUUID(),
title: title.trim(),
completed: false,
createdAt: new Date(),
});
}
}
// Application Layer - Ports
interface TodoRepository {
save(todo: TodoEntity): Promise<TodoEntity>;
findAll(): Promise<TodoEntity[]>;
update(id: string, data: Partial<TodoEntity>): Promise<TodoEntity>;
delete(id: string): Promise<void>;
}
// Infrastructure Layer - Adapters
class ApiTodoRepository implements TodoRepository {
constructor(private apiClient: ApiClient) {}
async save(todo: TodoEntity): Promise<TodoEntity> {
return this.apiClient.post('/todos', todo);
}
async findAll(): Promise<TodoEntity[]> {
return this.apiClient.get('/todos');
}
async update(id: string, data: Partial<TodoEntity>): Promise<TodoEntity> {
return this.apiClient.put(`/todos/${id}`, data);
}
async delete(id: string): Promise<void> {
return this.apiClient.delete(`/todos/${id}`);
}
}
// Presentation Layer
function TodoList() {
const createTodo = new CreateTodoUseCase(new ApiTodoRepository(apiClient));
const [todos, setTodos] = useState<TodoEntity[]>([]);
const handleCreate = async (title: string) => {
try {
const todo = await createTodo.execute(title);
setTodos(prev => [...prev, todo]);
} catch (error) {
// Gestion d'erreur UI
}
};
}
Exercice 12 : Performance - Virtualisation
import { useRef, useState, useCallback, useEffect, ReactNode } from 'react';
interface VirtualListProps<T> {
items: T[];
itemHeight: number;
renderItem: (item: T, index: number) => ReactNode;
overscan?: number;
}
function VirtualList<T>({
items,
itemHeight,
renderItem,
overscan = 3,
}: VirtualListProps<T>) {
const containerRef = useRef<HTMLDivElement>(null);
const [scrollTop, setScrollTop] = useState(0);
const [containerHeight, setContainerHeight] = useState(0);
useEffect(() => {
if (containerRef.current) {
setContainerHeight(containerRef.current.clientHeight);
}
}, []);
const handleScroll = useCallback(() => {
if (containerRef.current) {
setScrollTop(containerRef.current.scrollTop);
}
}, []);
const totalHeight = items.length * itemHeight;
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
const endIndex = Math.min(
items.length,
Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan
);
const visibleItems = items.slice(startIndex, endIndex);
return (
<div
ref={containerRef}
onScroll={handleScroll}
style={{ height: '100%', overflow: 'auto' }}
>
<div style={{ height: totalHeight, position: 'relative' }}>
{visibleItems.map((item, index) => (
<div
key={startIndex + index}
style={{
position: 'absolute',
top: (startIndex + index) * itemHeight,
height: itemHeight,
left: 0,
right: 0,
}}
>
{renderItem(item, startIndex + index)}
</div>
))}
</div>
</div>
);
}
Exercice 13 : SSR avec Next.js App Router
// app/page.tsx
async function getPosts(): Promise<Post[]> {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 },
});
if (!res.ok) throw new Error('Failed to fetch posts');
return res.json();
}
export default async function HomePage() {
const posts = await getPosts();
return (
<main>
<h1>Blog</h1>
<div className="grid">
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
</main>
);
}
// app/error.tsx
'use client';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Erreur de chargement</h2>
<button onClick={reset}>Réessayer</button>
</div>
);
}
// app/loading.tsx
export default function Loading() {
return <div className="skeleton">Chargement...</div>;
}
Niveau Expert
Exercice 14 : Micro-Frontends avec Module Federation
// Host - webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
products: 'products@http://localhost:3001/remoteEntry.js',
cart: 'cart@http://localhost:3002/remoteEntry.js',
auth: 'auth@http://localhost:3003/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^19.0.0' },
'react-dom': { singleton: true, requiredVersion: '^19.0.0' },
},
}),
],
};
// Remote - products/src/App.tsx
const ProductsApp = () => {
return (
<div>
<h2>Catalogue Produits</h2>
<ProductList />
</div>
);
};
export default ProductsApp;
Exercice 15 : WebAssembly avec Rust
// src/lib.rs
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct ImageProcessor {
width: u32,
height: u32,
pixels: Vec<u8>,
}
#[wasm_bindgen]
impl ImageProcessor {
pub fn new(width: u32, height: u32, pixels: Vec<u8>) -> ImageProcessor {
ImageProcessor { width, height, pixels }
}
pub fn apply_grayscale(&mut self) {
for pixel in self.pixels.chunks_mut(4) {
let r = pixel[0] as f32;
let g = pixel[1] as f32;
let b = pixel[2] as f32;
let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8;
pixel[0] = gray;
pixel[1] = gray;
pixel[2] = gray;
}
}
pub fn apply_sepia(&mut self) {
for pixel in self.pixels.chunks_mut(4) {
let r = pixel[0] as f32;
let g = pixel[1] as f32;
let b = pixel[2] as f32;
pixel[0] = (r * 0.393 + g * 0.769 + b * 0.189).min(255.0) as u8;
pixel[1] = (r * 0.349 + g * 0.686 + b * 0.168).min(255.0) as u8;
pixel[2] = (r * 0.272 + g * 0.534 + b * 0.131).min(255.0) as u8;
}
}
}
Grille d'évaluation
| Niveau | Critères | Points |
|---|---|---|
| Débutant | Syntaxe correcte, structure, sémantique | 10 |
| Intermédiaire | Patterns, tests, types | 20 |
| Avancé | Architecture, performance, maintenabilité | 30 |
| Expert | Innovation, optimisation, production-ready | 40 |
Critères transverses
- Qualité du code : nommage, structure, commentaires utiles
- Tests : couverture, edge cases, mocking
- Performance : complexité algorithmique, rendering
- Accessibilité : ARIA, navigation clavier, contrast
- Documentation : README, JSDoc, ADR