Skip to content

⚛️ 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.

tsx
// 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.

tsx
// 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.

tsx
// 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.

tsx
// 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.

tsx
// 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.

tsx
// 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

tsx
// 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


React est la base de notre frontend. Maîtrisez ces concepts pour contribuer efficacement ! 🚀

Released under the MIT License.