Pourquoi React ?
React est la bibliothèque JavaScript la plus populaire pour construire des interfaces utilisateur. En 2025, elle domine avec plus de 15 millions de téléchargements hebdomadaires sur NPM.
Avantages clés
- Composants réutilisables : Créez une fois, utilisez partout
- Virtual DOM : Mises à jour ultra-rapides
- Écosystème riche : Milliers de bibliothèques compatibles
- TypeScript natif : Support de premier ordre
- React 18+ : Concurrent rendering et Suspense
1. Premiers pas : Créer un projet React
Avec Vite (Recommandé en 2025)
# Créer un nouveau projet
npm create vite@latest mon-app-react -- --template react-ts
# Naviguer et installer
cd mon-app-react
npm install
# Lancer le serveur de développement
npm run dev
Structure du projet
mon-app-react/
├── node_modules/
├── public/
├── src/
│ ├── App.tsx
│ ├── App.css
│ ├── main.tsx
│ └── vite-env.d.ts
├── index.html
├── package.json
├── tsconfig.json
└── vite.config.ts
2. Composants : Les briques de votre application
Votre premier composant
// src/components/Welcome.tsx
const Welcome = () => {
return (
<div>
# Bienvenue dans React !
<p>Votre premier composant fonctionne.</p>
</div>
);
};
export default Welcome;
JSX : JavaScript + XML
// JSX permet d'écrire du HTML dans JavaScript
const element = # Hello, world!;
// Équivalent JavaScript pur (ne pas utiliser!)
const element = React.createElement('h1', null, 'Hello, world!');
// Expressions JavaScript dans JSX
const name = 'Marie';
const element = # Bonjour, {name}!;
// Attributs en camelCase
const button = (
<button
className="btn-primary" // class → className
onClick={handleClick} // onclick → onClick
disabled={isLoading}
>
Cliquez-moi
</button>
);
Composant avec structure complète
// src/components/UserProfile.tsx
import { CSSProperties } from 'react';
interface UserProfileProps {
name: string;
age: number;
avatar?: string;
}
const UserProfile = ({ name, age, avatar }: UserProfileProps) => {
const cardStyle: CSSProperties = {
border: '1px solid #ddd',
borderRadius: '8px',
padding: '20px',
maxWidth: '300px',
textAlign: 'center'
};
return (
<div style={cardStyle}>
{avatar && (
<img
src={avatar}
alt={name}
style={{ borderRadius: '50%', width: '100px', height: '100px' }}
/>
)}
## {name}
<p>Age: {age} ans</p>
</div>
);
};
export default UserProfile;
Utiliser votre composant
// src/App.tsx
import UserProfile from './components/UserProfile';
function App() {
return (
<div className="app">
# Notre équipe
<UserProfile
name="Sophie Dupont"
age={28}
avatar="https://i.pravatar.cc/100?img=1"
/>
<UserProfile
name="Pierre Martin"
age={32}
avatar="https://i.pravatar.cc/100?img=2"
/>
<UserProfile
name="Julie Bernard"
age={25}
/>
</div>
);
}
export default App;
3. Props : Passer des données aux composants
Props de base
// Button.tsx
interface ButtonProps {
text: string;
variant?: 'primary' | 'secondary' | 'danger';
disabled?: boolean;
onClick?: () => void;
}
const Button = ({
text,
variant = 'primary',
disabled = false,
onClick
}: ButtonProps) => {
return (
<button
className={btn btn--${variant}}
disabled={disabled}
onClick={onClick}
>
{text}
</button>
);
};
// Utilisation
<Button text="Enregistrer" variant="primary" />
<Button text="Annuler" variant="secondary" />
<Button text="Supprimer" variant="danger" onClick={handleDelete} />
Props.children : Contenu imbriqué
// Card.tsx
interface CardProps {
title: string;
children: React.ReactNode;
footer?: React.ReactNode;
}
const Card = ({ title, children, footer }: CardProps) => {
return (
<div className="card">
<div className="card-header">
### {title}
</div>
<div className="card-body">
{children}
</div>
{footer && (
<div className="card-footer">
{footer}
</div>
)}
</div>
);
};
// Utilisation
<Card
title="Mon profil"
footer={<Button text="Sauvegarder" />}
>
<p>Nom: Sophie Dupont</p>
<p>Email: sophie@example.com</p>
</Card>
Props avec spread operator
// Input.tsx
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
error?: string;
}
const Input = ({ label, error, ...inputProps }: InputProps) => {
return (
<div className="form-group">
<label>{label}</label>
<input
className={error ? 'input-error' : 'input'}
{...inputProps} // Spread toutes les props HTML natives
/>
{error && <span className="error-message">{error}</span>}
</div>
);
};
// Utilisation
<Input
label="Email"
type="email"
placeholder="votre@email.com"
required
maxLength={100}
error="Email invalide"
/>
4. State : Données dynamiques et interactivité
useState : Le hook fondamental
import { useState } from 'react';
const Counter = () => {
// [valeur, fonction pour modifier la valeur]
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const reset = () => setCount(0);
return (
<div className="counter">
## Compteur: {count}
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<button onClick={reset}>Reset</button>
</div>
);
};
State avec types complexes
interface Todo {
id: number;
text: string;
completed: boolean;
}
const TodoList = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const [inputValue, setInputValue] = useState('');
const addTodo = () => {
if (inputValue.trim()) {
const newTodo: Todo = {
id: Date.now(),
text: inputValue,
completed: false
};
setTodos([...todos, newTodo]);
setInputValue('');
}
};
const toggleTodo = (id: number) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const deleteTodo = (id: number) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div className="todo-list">
## Ma Todo List
<div className="todo-input">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
placeholder="Nouvelle tâche..."
/>
<button onClick={addTodo}>Ajouter</button>
</div>
<ul>
{todos.map(todo => (
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => deleteTodo(todo.id)}>×</button>
</li>
))}
</ul>
<p>{todos.filter(t => !t.completed).length} tâches restantes</p>
</div>
);
};
State avec objets
interface FormData {
username: string;
email: string;
age: number;
newsletter: boolean;
}
const RegistrationForm = () => {
const [formData, setFormData] = useState<FormData>({
username: '',
email: '',
age: 18,
newsletter: false
});
// ✅ Bonne pratique : fonction de mise à jour
const updateField = (field: keyof FormData, value: any) => {
setFormData(prev => ({
...prev,
[field]: value
}));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log('Données du formulaire:', formData);
// Envoyer à l'API...
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={formData.username}
onChange={(e) => updateField('username', e.target.value)}
placeholder="Nom d'utilisateur"
/>
<input
type="email"
value={formData.email}
onChange={(e) => updateField('email', e.target.value)}
placeholder="Email"
/>
<input
type="number"
value={formData.age}
onChange={(e) => updateField('age', parseInt(e.target.value))}
/>
<label>
<input
type="checkbox"
checked={formData.newsletter}
onChange={(e) => updateField('newsletter', e.target.checked)}
/>
Recevoir la newsletter
</label>
<button type="submit">S'inscrire</button>
</form>
);
};
5. Événements et handlers
Événements courants
const EventExamples = () => {
// Click
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log('Cliqué!', e.currentTarget);
};
// Input change
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log('Valeur:', e.target.value);
};
// Form submit
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log('Formulaire soumis');
};
// Key press
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
console.log('Enter pressé');
}
};
// Focus
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
e.target.select(); // Sélectionne tout le texte
};
return (
<div>
<button onClick={handleClick}>Cliquez</button>
<input onChange={handleChange} onKeyPress={handleKeyPress} />
<form onSubmit={handleSubmit}>
<input onFocus={handleFocus} />
</form>
</div>
);
};
Passer des paramètres aux handlers
const ProductList = () => {
const products = [
{ id: 1, name: 'Laptop', price: 999 },
{ id: 2, name: 'Mouse', price: 29 },
{ id: 3, name: 'Keyboard', price: 79 }
];
// Méthode 1: Arrow function inline (simple mais crée une nouvelle fonction à chaque render)
const handleBuy1 = (productId: number) => {
console.log('Achat produit:', productId);
};
// Méthode 2: Currying (plus performant)
const handleBuy2 = (productId: number) => () => {
console.log('Achat produit:', productId);
};
return (
<div>
{products.map(product => (
<div key={product.id}>
### {product.name}
<p>{product.price}€</p>
{/* Méthode 1 */}
<button onClick={() => handleBuy1(product.id)}>
Acheter
</button>
{/* Méthode 2 (préférée) */}
<button onClick={handleBuy2(product.id)}>
Acheter
</button>
</div>
))}
</div>
);
};
6. Listes et clés
Rendu de listes
interface User {
id: number;
name: string;
email: string;
active: boolean;
}
const UserList = () => {
const users: User[] = [
{ id: 1, name: 'Alice', email: 'alice@example.com', active: true },
{ id: 2, name: 'Bob', email: 'bob@example.com', active: false },
{ id: 3, name: 'Charlie', email: 'charlie@example.com', active: true }
];
// ✅ Bonne pratique : key unique et stable
return (
{users.map(user => (
<li key={user.id}>
<span className={user.active ? 'active' : 'inactive'}>
{user.name}
</span>
<span>{user.email}</span>
))}
);
};
// ❌ Mauvaise pratique : index comme key
users.map((user, index) => (
<li key={index}>{user.name} // Problèmes avec réorganisation
));
// ❌ Mauvaise pratique : pas de key
users.map(user => (
- {user.name} // Warning React
));
Listes filtrées et triées
const AdvancedUserList = () => {
const [users, setUsers] = useState<User[]>([...]);
const [filter, setFilter] = useState<'all' | 'active' | 'inactive'>('all');
const [sortBy, setSortBy] = useState<'name' | 'email'>('name');
// Filtrage
const filteredUsers = users.filter(user => {
if (filter === 'active') return user.active;
if (filter === 'inactive') return !user.active;
return true;
});
// Tri
const sortedUsers = [...filteredUsers].sort((a, b) => {
return a[sortBy].localeCompare(b[sortBy]);
});
return (
<div>
<div className="controls">
<select value={filter} onChange={(e) => setFilter(e.target.value as any)}>
<option value="all">Tous</option>
<option value="active">Actifs</option>
<option value="inactive">Inactifs</option>
</select>
<select value={sortBy} onChange={(e) => setSortBy(e.target.value as any)}>
<option value="name">Nom</option>
<option value="email">Email</option>
</select>
</div>
<ul>
{sortedUsers.map(user => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
<p>Total: {sortedUsers.length} utilisateurs</p>
</div>
);
};
7. Rendu conditionnel
Méthodes de rendu conditionnel
const ConditionalRendering = () => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [user, setUser] = useState<User | null>(null);
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
// Méthode 1: if/else
if (!isLoggedIn) {
return <LoginPage />;
}
// Méthode 2: Ternaire
return (
<div>
{isLoggedIn ? (
<Dashboard user={user} />
) : (
<LoginPage />
)}
</div>
);
// Méthode 3: && (pour un seul cas)
return (
<div>
{user && <Welcome name={user.name} />}
{error && <ErrorMessage message={error} />}
</div>
);
// Méthode 4: Switch avec objet
const statusComponents = {
loading: <LoadingSpinner />,
success: <SuccessMessage />,
error: <ErrorMessage />
};
return (
<div>
{statusComponents[status]}
</div>
);
};
8. Exemple complet : Application de recherche
// SearchApp.tsx
import { useState, useEffect } from 'react';
interface Article {
id: number;
title: string;
excerpt: string;
category: string;
tags: string[];
}
const SearchApp = () => {
const [articles, setArticles] = useState<Article[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const [selectedCategory, setSelectedCategory] = useState<string>('all');
const [loading, setLoading] = useState(true);
// Simuler un fetch API
useEffect(() => {
const fetchArticles = async () => {
setLoading(true);
// Dans la vraie vie: await fetch('/api/articles')
const mockArticles: Article[] = [
{
id: 1,
title: 'Introduction à React',
excerpt: 'Apprenez les bases de React',
category: 'Tutorial',
tags: ['react', 'javascript']
},
{
id: 2,
title: 'Next.js pour les débutants',
excerpt: 'Créez des apps performantes',
category: 'Tutorial',
tags: ['nextjs', 'ssr']
},
{
id: 3,
title: 'TypeScript Best Practices',
excerpt: 'Écrivez du TypeScript propre',
category: 'Guide',
tags: ['typescript', 'best-practices']
}
];
setTimeout(() => {
setArticles(mockArticles);
setLoading(false);
}, 1000);
};
fetchArticles();
}, []);
// Filtrage
const filteredArticles = articles.filter(article => {
const matchesSearch = article.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
article.excerpt.toLowerCase().includes(searchTerm.toLowerCase());
const matchesCategory = selectedCategory === 'all' || article.category === selectedCategory;
return matchesSearch && matchesCategory;
});
// Extraire les catégories uniques
const categories = ['all', ...new Set(articles.map(a => a.category))];
if (loading) {
return <div className="loading">Chargement...</div>;
}
return (
<div className="search-app">
<header>
# Recherche d'articles
</header>
<div className="filters">
<input
type="text"
placeholder="Rechercher un article..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="search-input"
/>
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="category-select"
>
{categories.map(cat => (
<option key={cat} value={cat}>
{cat === 'all' ? 'Toutes catégories' : cat}
</option>
))}
</select>
</div>
<div className="results">
<p>{filteredArticles.length} résultat(s)</p>
{filteredArticles.length === 0 ? (
<div className="no-results">
Aucun article trouvé pour "{searchTerm}"
</div>
) : (
<div className="articles-grid">
{filteredArticles.map(article => (
<article key={article.id} className="article-card">
## {article.title}
<p>{article.excerpt}</p>
<div className="meta">
<span className="category">{article.category}</span>
<div className="tags">
{article.tags.map(tag => (
<span key={tag} className="tag">#{tag}</span>
))}
</div>
</div>
</article>
))}
</div>
)}
</div>
</div>
);
};
export default SearchApp;
9. Tests avec React Testing Library
// SearchApp.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import SearchApp from './SearchApp';
describe('SearchApp', () => {
test('affiche le loading initialement', () => {
render(<SearchApp />);
expect(screen.getByText('Chargement...')).toBeInTheDocument();
});
test('affiche les articles après chargement', async () => {
render(<SearchApp />);
await waitFor(() => {
expect(screen.getByText('Introduction à React')).toBeInTheDocument();
});
});
test('filtre les articles par recherche', async () => {
const user = userEvent.setup();
render(<SearchApp />);
await waitFor(() => {
expect(screen.getByText('Introduction à React')).toBeInTheDocument();
});
const searchInput = screen.getByPlaceholderText('Rechercher un article...');
await user.type(searchInput, 'Next.js');
expect(screen.getByText('Next.js pour les débutants')).toBeInTheDocument();
expect(screen.queryByText('Introduction à React')).not.toBeInTheDocument();
});
test('filtre par catégorie', async () => {
render(<SearchApp />);
await waitFor(() => {
expect(screen.getByText('Introduction à React')).toBeInTheDocument();
});
const categorySelect = screen.getByRole('combobox');
fireEvent.change(categorySelect, { target: { value: 'Guide' } });
expect(screen.getByText('TypeScript Best Practices')).toBeInTheDocument();
expect(screen.queryByText('Introduction à React')).not.toBeInTheDocument();
});
});
10. Styles CSS
/* SearchApp.css */
.search-app {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.filters {
display: flex;
gap: 16px;
margin: 24px 0;
}
.search-input,
.category-select {
padding: 12px 16px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.2s;
}
.search-input {
flex: 1;
}
.search-input:focus,
.category-select:focus {
outline: none;
border-color: #3b82f6;
}
.articles-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 24px;
margin-top: 24px;
}
.article-card {
padding: 24px;
border: 1px solid #e2e8f0;
border-radius: 12px;
background: white;
transition: transform 0.2s, box-shadow 0.2s;
}
.article-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
}
.article-card h2 {
margin: 0 0 12px 0;
color: #1a202c;
}
.meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16px;
}
.category {
padding: 4px 12px;
background: #e0e7ff;
color: #4c51bf;
border-radius: 12px;
font-size: 14px;
}
.tags {
display: flex;
gap: 8px;
}
.tag {
color: #718096;
font-size: 14px;
}
.loading,
.no-results {
text-align: center;
padding: 48px;
color: #718096;
font-size: 18px;
}
Meilleures pratiques 2025
handleClick, isLoading, hasErrorConclusion
Vous maîtrisez maintenant les trois piliers de React :