⚛️ React Fundamentals
📋 Vue d'ensemble
React est une bibliothèque JavaScript pour créer des interfaces utilisateur interactives. React 18 introduit de nouvelles fonctionnalités comme le Concurrent Rendering et les Automatic Batching.
🏗️ Concepts fondamentaux
Composants
Les composants sont les blocs de base de React. Ils peuvent être fonctionnels ou de classe.
// Composant fonctionnel (recommandé)
function WelcomeMessage({ name }: { name: string }) {
return (
<div className="text-center py-8">
<h1 className="text-3xl font-bold text-gray-900">
Bienvenue, {name} ! 🎭
</h1>
<p className="text-lg text-gray-600 mt-2">
Prêt pour l'improvisation ?
</p>
</div>
);
}
// Composant de classe (legacy)
class WelcomeMessageClass extends React.Component<{ name: string }> {
render() {
return (
<div className="text-center py-8">
<h1 className="text-3xl font-bold text-gray-900">
Bienvenue, {this.props.name} ! 🎭
</h1>
<p className="text-lg text-gray-600 mt-2">
Prêt pour l'improvisation ?
</p>
</div>
);
}
}Props
Les props sont des données passées d'un composant parent à un composant enfant.
// Interface pour les props
interface EventCardProps {
event: {
id: string;
title: string;
description: string;
date: string;
maxPlayers: number;
};
onJoin?: (eventId: string) => void;
showActions?: boolean;
}
// Composant avec props typées
function EventCard({ event, onJoin, showActions = true }: EventCardProps) {
return (
<div className="bg-white rounded-lg shadow-md p-6">
<h3 className="text-xl font-bold text-gray-900 mb-2">
{event.title}
</h3>
<p className="text-gray-600 mb-4">
{event.description}
</p>
<div className="flex items-center justify-between text-sm text-gray-500 mb-4">
<span>📅 {new Date(event.date).toLocaleDateString('fr-FR')}</span>
<span>👥 {event.maxPlayers} joueurs max</span>
</div>
{showActions && onJoin && (
<button
onClick={() => onJoin(event.id)}
className="w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
Participer
</button>
)}
</div>
);
}
// Utilisation
<EventCard
event={eventData}
onJoin={handleJoinEvent}
showActions={isAuthenticated}
/>État local
L'état local permet aux composants de gérer leurs propres données.
// Hook useState pour l'état simple
function Counter() {
const [count, setCount] = useState(0);
return (
<div className="flex items-center gap-4">
<span className="text-2xl font-bold">{count}</span>
<button
onClick={() => setCount(count + 1)}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Incrémenter
</button>
<button
onClick={() => setCount(0)}
className="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600"
>
Réinitialiser
</button>
</div>
);
}
// État complexe avec useReducer
interface FormState {
title: string;
description: string;
date: string;
maxPlayers: number;
errors: Record<string, string>;
}
type FormAction =
| { type: 'SET_FIELD'; field: string; value: string | number }
| { type: 'SET_ERRORS'; errors: Record<string, string> }
| { type: 'RESET' };
function formReducer(state: FormState, action: FormAction): FormState {
switch (action.type) {
case 'SET_FIELD':
return {
...state,
[action.field]: action.value,
errors: {
...state.errors,
[action.field]: '' // Effacer l'erreur du champ modifié
}
};
case 'SET_ERRORS':
return { ...state, errors: action.errors };
case 'RESET':
return {
title: '',
description: '',
date: '',
maxPlayers: 1,
errors: {}
};
default:
return state;
}
}
function EventForm() {
const [state, dispatch] = useReducer(formReducer, {
title: '',
description: '',
date: '',
maxPlayers: 1,
errors: {}
});
const handleInputChange = (field: string, value: string | number) => {
dispatch({ type: 'SET_FIELD', field, value });
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Validation
const errors: Record<string, string> = {};
if (!state.title.trim()) errors.title = 'Le titre est requis';
if (!state.description.trim()) errors.description = 'La description est requise';
if (!state.date) errors.date = 'La date est requise';
if (state.maxPlayers < 1) errors.maxPlayers = 'Le nombre de joueurs doit être positif';
if (Object.keys(errors).length > 0) {
dispatch({ type: 'SET_ERRORS', errors });
return;
}
// Soumission
console.log('Formulaire soumis:', state);
};
return (
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-2">
Titre de l'événement *
</label>
<input
type="text"
id="title"
value={state.title}
onChange={(e) => handleInputChange('title', e.target.value)}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
state.errors.title ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="Ex: Match d'improvisation"
/>
{state.errors.title && (
<p className="mt-1 text-sm text-red-600">{state.errors.title}</p>
)}
</div>
<div>
<label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-2">
Description *
</label>
<textarea
id="description"
rows={4}
value={state.description}
onChange={(e) => handleInputChange('description', e.target.value)}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
state.errors.description ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="Décrivez votre événement..."
/>
{state.errors.description && (
<p className="mt-1 text-sm text-red-600">{state.errors.description}</p>
)}
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label htmlFor="date" className="block text-sm font-medium text-gray-700 mb-2">
Date et heure *
</label>
<input
type="datetime-local"
id="date"
value={state.date}
onChange={(e) => handleInputChange('date', e.target.value)}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
state.errors.date ? 'border-red-500' : 'border-gray-300'
}`}
/>
{state.errors.date && (
<p className="mt-1 text-sm text-red-600">{state.errors.date}</p>
)}
</div>
<div>
<label htmlFor="maxPlayers" className="block text-sm font-medium text-gray-700 mb-2">
Nombre max de joueurs *
</label>
<input
type="number"
id="maxPlayers"
min="1"
max="50"
value={state.maxPlayers}
onChange={(e) => handleInputChange('maxPlayers', parseInt(e.target.value))}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
state.errors.maxPlayers ? 'border-red-500' : 'border-gray-300'
}`}
/>
{state.errors.maxPlayers && (
<p className="mt-1 text-sm text-red-600">{state.errors.maxPlayers}</p>
)}
</div>
</div>
<div className="flex justify-end gap-4">
<button
type="button"
onClick={() => dispatch({ type: 'RESET' })}
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
>
Réinitialiser
</button>
<button
type="submit"
className="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
Créer l'événement
</button>
</div>
</form>
);
}🔄 Hooks avancés
useEffect
useEffect gère les effets de bord comme les appels API, les abonnements, etc.
// Effet simple
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchUser() {
try {
setLoading(true);
setError(null);
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('Utilisateur non trouvé');
}
const userData = await response.json();
setUser(userData);
} catch (err) {
setError(err instanceof Error ? err.message : 'Erreur inconnue');
} finally {
setLoading(false);
}
}
fetchUser();
}, [userId]); // Dépendance : se relance quand userId change
if (loading) return <div>Chargement...</div>;
if (error) return <div className="text-red-600">Erreur: {error}</div>;
if (!user) return <div>Utilisateur non trouvé</div>;
return (
<div className="p-6 bg-white rounded-lg shadow">
<h2 className="text-xl font-semibold">{user.name}</h2>
<p className="text-gray-600">{user.email}</p>
</div>
);
}
// Effet avec nettoyage
function EventNotifications({ eventId }: { eventId: string }) {
const [notifications, setNotifications] = useState<string[]>([]);
useEffect(() => {
// Simuler un WebSocket ou une connexion en temps réel
const eventSource = new EventSource(`/api/events/${eventId}/notifications`);
eventSource.onmessage = (event) => {
const notification = JSON.parse(event.data);
setNotifications(prev => [...prev, notification.message]);
};
eventSource.onerror = (error) => {
console.error('Erreur EventSource:', error);
};
// Fonction de nettoyage
return () => {
eventSource.close();
};
}, [eventId]);
return (
<div className="space-y-2">
<h3 className="font-semibold">Notifications en temps réel</h3>
{notifications.map((notification, index) => (
<div key={index} className="p-2 bg-blue-50 text-blue-800 rounded">
{notification}
</div>
))}
</div>
);
}useCallback et useMemo
Ces hooks optimisent les performances en mémorisant les fonctions et valeurs.
// useCallback pour mémoriser les fonctions
function EventList({ events, onEventSelect }: {
events: Event[];
onEventSelect: (event: Event) => void;
}) {
// Mémoriser la fonction de tri pour éviter les re-renders inutiles
const sortedEvents = useMemo(() => {
return [...events].sort((a, b) =>
new Date(a.date).getTime() - new Date(b.date).getTime()
);
}, [events]);
// Mémoriser la fonction de gestion du clic
const handleEventClick = useCallback((event: Event) => {
onEventSelect(event);
}, [onEventSelect]);
return (
<div className="space-y-4">
{sortedEvents.map(event => (
<div
key={event.id}
onClick={() => handleEventClick(event)}
className="p-4 border rounded-lg cursor-pointer hover:bg-gray-50"
>
<h3 className="font-semibold">{event.title}</h3>
<p className="text-gray-600">{event.description}</p>
</div>
))}
</div>
);
}
// useMemo pour les calculs coûteux
function EventStats({ events }: { events: Event[] }) {
const stats = useMemo(() => {
const now = new Date();
const upcoming = events.filter(e => new Date(e.date) > now);
const past = events.filter(e => new Date(e.date) <= now);
return {
total: events.length,
upcoming: upcoming.length,
past: past.length,
averagePlayers: events.reduce((sum, e) => sum + e.maxPlayers, 0) / events.length
};
}, [events]);
return (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="p-4 bg-blue-50 rounded-lg text-center">
<div className="text-2xl font-bold text-blue-600">{stats.total}</div>
<div className="text-sm text-blue-800">Total</div>
</div>
<div className="p-4 bg-green-50 rounded-lg text-center">
<div className="text-2xl font-bold text-green-600">{stats.upcoming}</div>
<div className="text-sm text-green-800">À venir</div>
</div>
<div className="p-4 bg-gray-50 rounded-lg text-center">
<div className="text-2xl font-bold text-gray-600">{stats.past}</div>
<div className="text-sm text-gray-800">Passés</div>
</div>
<div className="p-4 bg-purple-50 rounded-lg text-center">
<div className="text-2xl font-bold text-purple-600">
{Math.round(stats.averagePlayers)}
</div>
<div className="text-sm text-purple-800">Joueurs moy.</div>
</div>
</div>
);
}🎯 Gestion d'état avancée
Context API
Le Context API permet de partager des données entre composants sans passer par les props.
// Créer un contexte pour l'authentification
interface AuthContextType {
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
loading: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
// Provider du contexte
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Vérifier l'authentification au chargement
checkAuth();
}, []);
const checkAuth = async () => {
try {
const token = localStorage.getItem('auth-token');
if (token) {
const response = await fetch('/api/auth/me', {
headers: { Authorization: `Bearer ${token}` }
});
if (response.ok) {
const userData = await response.json();
setUser(userData);
}
}
} catch (error) {
console.error('Erreur de vérification d\'authentification:', error);
} finally {
setLoading(false);
}
};
const login = async (email: string, password: string) => {
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
if (!response.ok) {
throw new Error('Identifiants invalides');
}
const { user: userData, token } = await response.json();
localStorage.setItem('auth-token', token);
setUser(userData);
} catch (error) {
throw error;
}
};
const logout = () => {
localStorage.removeItem('auth-token');
setUser(null);
};
const value = {
user,
login,
logout,
loading
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
// Hook personnalisé pour utiliser le contexte
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth doit être utilisé dans un AuthProvider');
}
return context;
}
// Utilisation dans les composants
function LoginForm() {
const { login } = useAuth();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
try {
await login(email, password);
} catch (err) {
setError(err instanceof Error ? err.message : 'Erreur de connexion');
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
Email
</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
Mot de passe
</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
</div>
{error && (
<div className="text-red-600 text-sm">{error}</div>
)}
<button
type="submit"
className="w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
Se connecter
</button>
</form>
);
}
function UserMenu() {
const { user, logout } = useAuth();
if (!user) return null;
return (
<div className="relative">
<button className="flex items-center space-x-2 p-2 rounded-md hover:bg-gray-100">
<span className="text-sm font-medium">{user.name}</span>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
<div className="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-10">
<button
onClick={logout}
className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
>
Se déconnecter
</button>
</div>
</div>
);
}🧪 Tests des composants
Tests avec React Testing Library
// Tests du composant EventCard
import { render, screen, fireEvent } from '@testing-library/react';
import { EventCard } from '@/components/EventCard';
describe('EventCard', () => {
const mockEvent = {
id: '1',
title: 'Match d\'improvisation',
description: 'Un match passionnant',
date: '2024-12-25T20:00:00Z',
maxPlayers: 12
};
it('should render event information correctly', () => {
render(<EventCard event={mockEvent} />);
expect(screen.getByText('Match d\'improvisation')).toBeInTheDocument();
expect(screen.getByText('Un match passionnant')).toBeInTheDocument();
expect(screen.getByText('25/12/2024')).toBeInTheDocument();
expect(screen.getByText('12 joueurs max')).toBeInTheDocument();
});
it('should call onJoin when join button is clicked', () => {
const mockOnJoin = jest.fn();
render(<EventCard event={mockEvent} onJoin={mockOnJoin} />);
const joinButton = screen.getByText('Participer');
fireEvent.click(joinButton);
expect(mockOnJoin).toHaveBeenCalledWith('1');
});
it('should not show actions when showActions is false', () => {
render(<EventCard event={mockEvent} showActions={false} />);
expect(screen.queryByText('Participer')).not.toBeInTheDocument();
});
});🚀 Bonnes pratiques
Do's
- ✅ Utiliser des composants fonctionnels - Plus modernes et performants
- ✅ Typer les props avec TypeScript - Meilleure maintenabilité
- ✅ Séparer les responsabilités - Un composant, une responsabilité
- ✅ Utiliser les hooks appropriés - useState, useEffect, useCallback, useMemo
- ✅ Tester les composants - Tests unitaires et d'intégration
Don'ts
- ❌ Créer des composants trop gros - Diviser en composants plus petits
- ❌ Oublier la gestion d'erreurs - Toujours gérer les cas d'erreur
- ❌ Abuser des re-renders - Optimiser avec useCallback et useMemo
- ❌ Ignorer l'accessibilité - Utiliser les bonnes pratiques a11y
🚀 Prochaines étapes
- Next.js - App Router et composants
- TailwindCSS - Système de design
- Frontend Overview - Vue d'ensemble du frontend
- Backend - PayloadCMS et architecture
React est la base de notre frontend. Maîtrisez ces concepts pour contribuer efficacement ! 🚀
