Cours JavaScript et React · Module 8: Patterns React avancés

Design patterns et réutilisabilité de la logique

15 min de lecture
Dans cette leçon5

Au cours de notre voyage cosmique à travers le monde de React, l'utilisation efficace des ressources est essentielle au succès de la mission. Tout comme les ingénieurs spatiaux utilisent des conceptions éprouvées pour différents modules de vaisseau, nous pouvons nous aussi exploiter les design patterns en React pour maximiser la réutilisabilité du code et créer des applications plus scalables.

Patterns de rendu et de composition

1. Higher-Order Components (HOC)

Les Higher-Order Components (HOC) sont des fonctions qui prennent un composant et retournent un nouveau composant amélioré. C'est un pattern basé sur le principe de la composition, et non de l'héritage.

1// HOC simple ajoutant une fonctionnalité de suivi du temps
2function withTimer(WrappedComponent) {
3  return function WithTimer(props) {
4    const [startTime] = useState(Date.now());
5    const [currentTime, setCurrentTime] = useState(Date.now());
6
7    useEffect(() => {
8      const timer = setInterval(() => {
9        setCurrentTime(Date.now());
10      }, 1000);
11
12      return () => clearInterval(timer);
13    }, []);
14
15    const elapsedTime = (currentTime - startTime) / 1000;
16
17    return (
18      <WrappedComponent
19        {...props}
20        elapsedTime={elapsedTime}
21      />
22    );
23  };
24}
25
26// Utilisation du HOC
27function MissionControl({ missionName, elapsedTime }) {
28  return (
29    <div className="mission-control">
30      <h2>Mission : {missionName}</h2>
31      <p>Durée : {elapsedTime.toFixed(0)} secondes</p>
32      {/* reste du composant */}
33    </div>
34  );
35}
36
37const MissionControlWithTimer = withTimer(MissionControl);
38
39// Rendu du composant amélioré
40function App() {
41  return <MissionControlWithTimer missionName="Apollo" />;
42}

2. Render Props

Render Props est un pattern où un composant reçoit une fonction qui lui indique quoi rendre. Cela permet de partager de la logique entre composants.

1function MouseTracker({ render }) {
2  const [position, setPosition] = useState({ x: 0, y: 0 });
3
4  useEffect(() => {
5    function handleMouseMove(event) {
6      setPosition({
7        x: event.clientX,
8        y: event.clientY
9      });
10    }
11
12    window.addEventListener('mousemove', handleMouseMove);
13
14    return () => {
15      window.removeEventListener('mousemove', handleMouseMove);
16    };
17  }, []);
18
19  return render(position);
20}
21
22// Utilisation des render props
23function App() {
24  return (
25    <MouseTracker
26      render={({ x, y }) => (
27        <div className="space-map">
28          <h2>Carte stellaire</h2>
29          <p>Coordonnées du curseur : ({x}, {y})</p>
30          <div
31            className="space-ship"
32            style={{
33              position: 'absolute',
34              left: x,
35              top: y
36            }}
37          >
38            ▲
39          </div>
40        </div>
41      )}
42    />
43  );
44}

3. Pattern Compound Components

Ce pattern te permet de créer des composants qui collaborent tout en conservant flexibilité et lisibilité du code.

1function Spaceship({ children }) {
2  const [power, setPower] = useState(0);
3  const [shields, setShields] = useState(false);
4
5  const contextValue = {
6    power,
7    setPower,
8    shields,
9    setShields
10  };
11
12  return (
13    <SpaceshipContext.Provider value={contextValue}>
14      <div className="spaceship">
15        {children}
16      </div>
17    </SpaceshipContext.Provider>
18  );
19}
20
21// Composants enfants
22Spaceship.Engine = function Engine() {
23  const { power, setPower } = useContext(SpaceshipContext);
24
25  return (
26    <div className="engine">
27      <h3>Moteur</h3>
28      <p>Puissance : {power}%</p>
29      <input
30        type="range"
31        min="0"
32        max="100"
33        value={power}
34        onChange={e => setPower(Number(e.target.value))}
35      />
36    </div>
37  );
38};
39
40Spaceship.Shields = function Shields() {
41  const { shields, setShields } = useContext(SpaceshipContext);
42
43  return (
44    <div className="shields">
45      <h3>Boucliers</h3>
46      <button onClick={() => setShields(!shields)}>
47        {shields ? 'Désactiver' : 'Activer'} les boucliers
48      </button>
49      <p>Statut : {shields ? 'Actifs' : 'Inactifs'}</p>
50    </div>
51  );
52};
53
54// Utilisation
55function App() {
56  return (
57    <Spaceship>
58      <h2>Panneau de contrôle du vaisseau</h2>
59      <Spaceship.Engine />
60      <Spaceship.Shields />
61    </Spaceship>
62  );
63}

Patterns de gestion d'état

1. Flux / Redux

Redux implémente le pattern Flux, qui repose sur un flux de données unidirectionnel. L'état de l'application est stocké dans un seul objet (store), et les modifications d'état ne peuvent se produire qu'en dispatchant des actions traitées par des reducers.

1// Actions
2const ADD_CREWMEMBER = 'ADD_CREWMEMBER';
3const REMOVE_CREWMEMBER = 'REMOVE_CREWMEMBER';
4
5// Action creators
6function addCrewmember(member) {
7  return {
8    type: ADD_CREWMEMBER,
9    payload: member
10  };
11}
12
13function removeCrewmember(id) {
14  return {
15    type: REMOVE_CREWMEMBER,
16    payload: id
17  };
18}
19
20// Reducer
21function crewReducer(state = [], action) {
22  switch (action.type) {
23    case ADD_CREWMEMBER:
24      return [...state, action.payload];
25    case REMOVE_CREWMEMBER:
26      return state.filter(member => member.id !== action.payload);
27    default:
28      return state;
29  }
30}
31
32// Dans un composant avec react-redux
33function CrewManagement() {
34  const crew = useSelector(state => state.crew);
35  const dispatch = useDispatch();
36
37  function handleAddCrewmember() {
38    const newMember = {
39      id: Date.now(),
40      name: 'Nouveau membre d\'équipage',
41      role: 'Cadet'
42    };
43    dispatch(addCrewmember(newMember));
44  }
45
46  return (
47    <div>
48      <h2>Gestion de l'équipage</h2>
49      <button onClick={handleAddCrewmember}>Ajouter un membre d'équipage</button>
50      <ul>
51        {crew.map(member => (
52          <li key={member.id}>
53            {member.name} - {member.role}
54            <button onClick={() => dispatch(removeCrewmember(member.id))}>
55              Retirer
56            </button>
57          </li>
58        ))}
59      </ul>
60    </div>
61  );
62}

2. Context API

Le Context API permet de transmettre des données à travers l'arbre des composants sans avoir à passer des props à chaque niveau.

1// Création du contexte
2const MissionContext = createContext();
3
4// Provider
5function MissionProvider({ children }) {
6  const [mission, setMission] = useState({
7    name: 'Apollo 11',
8    status: 'En cours',
9    crewMembers: [
10      { id: 1, name: 'Neil Armstrong', role: 'Commandant' },
11      { id: 2, name: 'Buzz Aldrin', role: 'Pilote du module lunaire' },
12      { id: 3, name: 'Michael Collins', role: 'Pilote du module de commande' }
13    ]
14  });
15
16  function updateMissionStatus(newStatus) {
17    setMission(prev => ({ ...prev, status: newStatus }));
18  }
19
20  function addCrewMember(member) {
21    setMission(prev => ({
22      ...prev,
23      crewMembers: [...prev.crewMembers, member]
24    }));
25  }
26
27  const value = {
28    mission,
29    updateMissionStatus,
30    addCrewMember
31  };
32
33  return (
34    <MissionContext.Provider value={value}>
35      {children}
36    </MissionContext.Provider>
37  );
38}
39
40// Custom hook pour une utilisation plus simple
41function useMission() {
42  const context = useContext(MissionContext);
43  if (!context) {
44    throw new Error('useMission must be used within a MissionProvider');
45  }
46  return context;
47}
48
49// Utilisation dans les composants
50function MissionDashboard() {
51  const { mission } = useMission();
52
53  return (
54    <div>
55      <h1>Mission : {mission.name}</h1>
56      <p>Statut : {mission.status}</p>
57    </div>
58  );
59}
60
61function MissionControl() {
62  const { updateMissionStatus } = useMission();
63
64  return (
65    <div>
66      <h2>Centre de contrôle de mission</h2>
67      <button onClick={() => updateMissionStatus('Terminée avec succès')}>
68        Terminer la mission
69      </button>
70      <button onClick={() => updateMissionStatus('Avortée')}>
71        Avorter la mission
72      </button>
73    </div>
74  );
75}
76
77function CrewList() {
78  const { mission, addCrewMember } = useMission();
79
80  return (
81    <div>
82      <h2>Équipage de la mission</h2>
83      <ul>
84        {mission.crewMembers.map(member => (
85          <li key={member.id}>
86            {member.name} - {member.role}
87          </li>
88        ))}
89      </ul>
90      <button
91        onClick={() =>
92          addCrewMember({
93            id: Date.now(),
94            name: 'Nouvel astronaute',
95            role: 'Spécialiste de mission'
96          })
97        }
98      >
99        Ajouter un membre d'équipage
100      </button>
101    </div>
102  );
103}
104
105// Assembler le tout
106function App() {
107  return (
108    <MissionProvider>
109      <div className="mission-app">
110        <MissionDashboard />
111        <MissionControl />
112        <CrewList />
113      </div>
114    </MissionProvider>
115  );
116}

3. Custom hooks pour l'état

Comme nous l'avons vu dans le module précédent, les custom hooks sont un outil puissant pour encapsuler et réutiliser la logique liée à l'état.

1function useSpaceshipSystems() {
2  const [engines, setEngines] = useState({
3    mainEngine: { power: 0, status: 'offline' },
4    leftThruster: { power: 0, status: 'offline' },
5    rightThruster: { power: 0, status: 'offline' }
6  });
7
8  const [lifeSupportSystem, setLifeSupportSystem] = useState({
9    oxygen: 100,
10    temperature: 21,
11    pressure: 1
12  });
13
14  function startEngine(engineName) {
15    setEngines(prev => ({
16      ...prev,
17      [engineName]: {
18        ...prev[engineName],
19        status: 'online',
20        power: 10
21      }
22    }));
23  }
24
25  function setEnginePower(engineName, power) {
26    if (engines[engineName].status === 'offline') {
27      console.warn(`Cannot set power - engine ${engineName} is offline`);
28      return;
29    }
30
31    setEngines(prev => ({
32      ...prev,
33      [engineName]: {
34        ...prev[engineName],
35        power: Math.max(0, Math.min(100, power))
36      }
37    }));
38  }
39
40  function stopEngine(engineName) {
41    setEngines(prev => ({
42      ...prev,
43      [engineName]: {
44        ...prev[engineName],
45        status: 'offline',
46        power: 0
47      }
48    }));
49  }
50
51  function adjustLifeSupport({ oxygen, temperature, pressure }) {
52    setLifeSupportSystem(prev => ({
53      ...prev,
54      ...(oxygen !== undefined ? { oxygen } : {}),
55      ...(temperature !== undefined ? { temperature } : {}),
56      ...(pressure !== undefined ? { pressure } : {})
57    }));
58  }
59
60  return {
61    engines,
62    lifeSupportSystem,
63    startEngine,
64    setEnginePower,
65    stopEngine,
66    adjustLifeSupport
67  };
68}
69
70// Utilisation
71function SpaceshipControls() {
72  const {
73    engines,
74    lifeSupportSystem,
75    startEngine,
76    setEnginePower,
77    stopEngine,
78    adjustLifeSupport
79  } = useSpaceshipSystems();
80
81  return (
82    <div className="spaceship-controls">
83      <h2>Contrôle des systèmes du vaisseau</h2>
84
85      <div className="engines-section">
86        <h3>Moteurs</h3>
87        {Object.entries(engines).map(([name, engine]) => (
88          <div key={name} className="engine-control">
89            <h4>{formatEngineName(name)}</h4>
90            <p>Statut : {engine.status}</p>
91            <p>Puissance : {engine.power}%</p>
92
93            {engine.status === 'offline' ? (
94              <button onClick={() => startEngine(name)}>Démarrer</button>
95            ) : (
96              <>
97                <input
98                  type="range"
99                  min="0"
100                  max="100"
101                  value={engine.power}
102                  onChange={e => setEnginePower(name, Number(e.target.value))}
103                />
104                <button onClick={() => stopEngine(name)}>Arrêter</button>
105              </>
106            )}
107          </div>
108        ))}
109      </div>
110
111      <div className="life-support">
112        <h3>Système de support vital</h3>
113        <p>Niveau d'oxygène : {lifeSupportSystem.oxygen}%</p>
114        <p>Température : {lifeSupportSystem.temperature}°C</p>
115        <p>Pression : {lifeSupportSystem.pressure} atm</p>
116
117        <div className="controls">
118          <button
119            onClick={() =>
120              adjustLifeSupport({
121                oxygen: lifeSupportSystem.oxygen + 5
122              })
123            }
124          >
125            Augmenter le niveau d'oxygène
126          </button>
127          <button
128            onClick={() =>
129              adjustLifeSupport({
130                temperature: lifeSupportSystem.temperature - 1
131              })
132            }
133          >
134            Baisser la température
135          </button>
136        </div>
137      </div>
138    </div>
139  );
140}
141
142function formatEngineName(name) {
143  return name
144    .replace(/([A-Z])/g, ' $1')
145    .replace(/^./, str => str.toUpperCase());
146}

Patterns de composants UI

1. Pattern Container / Presentational

Ce pattern sépare la logique métier (container) de la présentation (composant presentational), ce qui augmente la réutilisabilité des composants et simplifie les tests.

1// Composant container - gère la logique et la récupération des données
2function PlanetDataContainer({ planetId, children }) {
3  const [planet, setPlanet] = useState(null);
4  const [loading, setLoading] = useState(true);
5  const [error, setError] = useState(null);
6
7  useEffect(() => {
8    setLoading(true);
9
10    fetch(`https://api.planets.com/${planetId}`)
11      .then(response => {
12        if (!response.ok) throw new Error('Failed to fetch planet data');
13        return response.json();
14      })
15      .then(data => {
16        setPlanet(data);
17        setLoading(false);
18      })
19      .catch(err => {
20        setError(err.message);
21        setLoading(false);
22      });
23  }, [planetId]);
24
25  // Appelle la fonction children avec les données
26  return children({ planet, loading, error });
27}
28
29// Composant presentational - gère uniquement l'affichage
30function PlanetInfo({ planet, loading, error }) {
31  if (loading) return <div>Chargement des données de la planète...</div>;
32  if (error) return <div>Erreur : {error}</div>;
33  if (!planet) return null;
34
35  return (
36    <div className="planet-info">
37      <h2>{planet.name}</h2>
38      <div className="planet-details">
39        <p>Type : {planet.type}</p>
40        <p>Diamètre : {planet.diameter} km</p>
41        <p>Distance du Soleil : {planet.distanceFromSun} millions de km</p>
42        <p>Période orbitale : {planet.orbitalPeriod} jours</p>
43        <p>Température de surface : {planet.surfaceTemperature}°C</p>
44      </div>
45
46      {planet.moons && planet.moons.length > 0 && (
47        <div className="moons">
48          <h3>Lunes ({planet.moons.length}) :</h3>
49          <ul>
50            {planet.moons.map(moon => (
51              <li key={moon.id}>{moon.name}</li>
52            ))}
53          </ul>
54        </div>
55      )}
56    </div>
57  );
58}
59
60// Utilisation
61function SolarSystemExplorer() {
62  const [selectedPlanet, setSelectedPlanet] = useState('earth');
63
64  return (
65    <div className="solar-system-explorer">
66      <h1>Exploration du système solaire</h1>
67
68      <div className="planet-selector">
69        <label>Choisis une planète : </label>
70        <select
71          value={selectedPlanet}
72          onChange={e => setSelectedPlanet(e.target.value)}
73        >
74          <option value="mercury">Mercure</option>
75          <option value="venus">Vénus</option>
76          <option value="earth">Terre</option>
77          <option value="mars">Mars</option>
78          <option value="jupiter">Jupiter</option>
79          <option value="saturn">Saturne</option>
80          <option value="uranus">Uranus</option>
81          <option value="neptune">Neptune</option>
82        </select>
83      </div>
84
85      <PlanetDataContainer planetId={selectedPlanet}>
86        {({ planet, loading, error }) => (
87          <PlanetInfo planet={planet} loading={loading} error={error} />
88        )}
89      </PlanetDataContainer>
90    </div>
91  );
92}

2. Composition vs Héritage

React préfère la composition à l'héritage. Ce pattern permet de créer des composants flexibles par imbrication et passage de props.

1// Composant de base - Card
2function Card({ title, children, footer, className }) {
3  return (
4    <div className={`card ${className || ''}`}>
5      {title && <div className="card-header">{title}</div>}
6      <div className="card-body">{children}</div>
7      {footer && <div className="card-footer">{footer}</div>}
8    </div>
9  );
10}
11
12// Composants spécialisés utilisant la composition
13function MissionCard({ mission, onComplete }) {
14  return (
15    <Card
16      title={<h3>{mission.name}</h3>}
17      footer={
18        <button onClick={() => onComplete(mission.id)}>
19          Terminer la mission
20        </button>
21      }
22      className="mission-card"
23    >
24      <p><strong>Objectif :</strong> {mission.objective}</p>
25      <p><strong>Statut :</strong> {mission.status}</p>
26      <p><strong>Progression :</strong> {mission.progress}%</p>
27      <div className="progress-bar">
28        <div
29          className="progress"
30          style={{ width: `${mission.progress}%` }}
31        />
32      </div>
33    </Card>
34  );
35}
36
37function AstronautCard({ astronaut, onAssign }) {
38  return (
39    <Card
40      title={<h3>{astronaut.name}</h3>}
41      footer={
42        <button onClick={() => onAssign(astronaut.id)}>
43          Affecter à la mission
44        </button>
45      }
46      className="astronaut-card"
47    >
48      <div className="astronaut-info">
49        <img
50          src={astronaut.avatar}
51          alt={astronaut.name}
52          className="astronaut-avatar"
53        />
54        <div>
55          <p><strong>Grade :</strong> {astronaut.rank}</p>
56          <p><strong>Spécialisation :</strong> {astronaut.specialization}</p>
57          <p><strong>Missions :</strong> {astronaut.missionCount}</p>
58        </div>
59      </div>
60    </Card>
61  );
62}
63
64// Utilisation
65function Dashboard() {
66  const missions = [
67    { id: 1, name: 'Exploration de Mars', objective: 'Explorer la surface', status: 'En cours', progress: 65 },
68    { id: 2, name: 'Station spatiale ISS', objective: 'Maintenance des modules', status: 'Planifiée', progress: 0 }
69  ];
70
71  const astronauts = [
72    { id: 1, name: 'John Smith', rank: 'Capitaine', specialization: 'Pilote', missionCount: 5, avatar: '/avatars/js.png' },
73    { id: 2, name: 'Anna Johnson', rank: 'Lieutenant', specialization: 'Ingénieure', missionCount: 3, avatar: '/avatars/aj.png' }
74  ];
75
76  return (
77    <div className="dashboard">
78      <h1>Panneau de contrôle du centre spatial</h1>
79
80      <section className="missions-section">
81        <h2>Missions actives</h2>
82        <div className="missions-grid">
83          {missions.map(mission => (
84            <MissionCard
85              key={mission.id}
86              mission={mission}
87              onComplete={id => console.log(`Completed mission ${id}`)}
88            />
89          ))}
90        </div>
91      </section>
92
93      <section className="astronauts-section">
94        <h2>Astronautes disponibles</h2>
95        <div className="astronauts-grid">
96          {astronauts.map(astronaut => (
97            <AstronautCard
98              key={astronaut.id}
99              astronaut={astronaut}
100              onAssign={id => console.log(`Assigned astronaut ${id}`)}
101            />
102          ))}
103        </div>
104      </section>
105    </div>
106  );
107}

3. Pattern Headless Components

Ce pattern sépare la logique comportementale d'un composant de son apparence, permettant une flexibilité maximale du style.

1// Composant headless - contient uniquement la logique, pas d'UI
2function useTabControl(initialTab = 0) {
3  const [activeTab, setActiveTab] = useState(initialTab);
4
5  function activateTab(index) {
6    setActiveTab(index);
7  }
8
9  return {
10    activeTab,
11    activateTab
12  };
13}
14
15// Utilisation avec n'importe quelle UI
16function MissionTabs() {
17  const tabs = [
18    { id: 'overview', label: 'Aperçu' },
19    { id: 'crew', label: 'Équipage' },
20    { id: 'timeline', label: 'Chronologie' },
21    { id: 'systems', label: 'Systèmes' }
22  ];
23
24  const { activeTab, activateTab } = useTabControl();
25
26  return (
27    <div className="mission-tabs">
28      <div className="tabs-header">
29        {tabs.map((tab, index) => (
30          <button
31            key={tab.id}
32            className={`tab-button ${activeTab === index ? 'active' : ''}`}
33            onClick={() => activateTab(index)}
34          >
35            {tab.label}
36          </button>
37        ))}
38      </div>
39
40      <div className="tab-content">
41        {activeTab === 0 && (
42          <div className="overview-tab">
43            <h3>Aperçu de la mission</h3>
44            <p>La mission Apollo 11 fut la première mission habitée vers la Lune...</p>
45          </div>
46        )}
47
48        {activeTab === 1 && (
49          <div className="crew-tab">
50            <h3>Équipage</h3>
51            <ul>
52              <li>Neil Armstrong - Commandant</li>
53              <li>Edwin "Buzz" Aldrin - Pilote du module lunaire</li>
54              <li>Michael Collins - Pilote du module de commande</li>
55            </ul>
56          </div>
57        )}
58
59        {activeTab === 2 && (
60          <div className="timeline-tab">
61            <h3>Chronologie</h3>
62            <div className="timeline">
63              <div className="timeline-event">
64                <span className="date">16 juillet 1969</span>
65                <span className="description">Lancement depuis Cape Kennedy</span>
66              </div>
67              <div className="timeline-event">
68                <span className="date">20 juillet 1969</span>
69                <span className="description">Alunissage</span>
70              </div>
71              <div className="timeline-event">
72                <span className="date">21 juillet 1969</span>
73                <span className="description">Premier pas sur la Lune</span>
74              </div>
75              <div className="timeline-event">
76                <span className="date">24 juillet 1969</span>
77                <span className="description">Retour sur Terre</span>
78              </div>
79            </div>
80          </div>
81        )}
82
83        {activeTab === 3 && (
84          <div className="systems-tab">
85            <h3>Systèmes du vaisseau</h3>
86            <p>Module de commande : Columbia</p>
87            <p>Module lunaire : Eagle</p>
88            <p>Lanceur : Saturn V</p>
89          </div>
90        )}
91      </div>
92    </div>
93  );
94}

Patterns d'optimisation

1. Mémoïsation

La mémoïsation aide à éviter les rendus inutiles en mémorisant les résultats de calculs coûteux.

1// Mémoïsation de composant
2const MemoizedTelemetryChart = React.memo(function TelemetryChart({ data }) {
3  // Simulation de calculs coûteux
4  const processedData = useMemo(() => {
5    console.log('Processing telemetry data...');
6
7    // Traitement de données coûteux
8    return data.map(point => ({
9      x: new Date(point.timestamp).toLocaleTimeString(),
10      y: calculateAverage(point.readings),
11      anomaly: detectAnomaly(point.readings)
12    }));
13  }, [data]); // Recalcule uniquement quand data change
14
15  return (
16    <div className="telemetry-chart">
17      <h3>Graphique de télémétrie</h3>
18      <div className="chart-container">
19        {/* Graphique avec données traitées */}
20        {processedData.map((point, index) => (
21          <div
22            key={index}
23            className={`data-point ${point.anomaly ? 'anomaly' : ''}`}
24            style={{
25              height: `${point.y * 2}px`,
26              backgroundColor: point.anomaly ? 'red' : 'blue'
27            }}
28            title={`${point.x}: ${point.y} ${point.anomaly ? '(anomaly)' : ''}`}
29          />
30        ))}
31      </div>
32    </div>
33  );
34}, (prevProps, nextProps) => {
35  // Logique de comparaison des props personnalisée
36  if (prevProps.data.length !== nextProps.data.length) {
37    return false; // re-render si la taille des données a changé
38  }
39
40  // Comparer uniquement les 5 derniers points de données
41  const prevLastFive = prevProps.data.slice(-5);
42  const nextLastFive = nextProps.data.slice(-5);
43
44  for (let i = 0; i < 5; i++) {
45    if (!prevLastFive[i] || !nextLastFive[i]) continue;
46
47    if (
48      prevLastFive[i].timestamp !== nextLastFive[i].timestamp ||
49      !arraysEqual(prevLastFive[i].readings, nextLastFive[i].readings)
50    ) {
51      return false; // re-render si les données diffèrent
52    }
53  }
54
55  return true; // ne pas re-render
56});
57
58// Fonctions utilitaires
59function calculateAverage(readings) {
60  return readings.reduce((sum, val) => sum + val, 0) / readings.length;
61}
62
63function detectAnomaly(readings) {
64  const avg = calculateAverage(readings);
65  const threshold = avg * 1.5;
66  return readings.some(val => val > threshold);
67}
68
69function arraysEqual(a, b) {
70  if (a.length !== b.length) return false;
71  for (let i = 0; i < a.length; i++) {
72    if (a[i] !== b[i]) return false;
73  }
74  return true;
75}

2. Virtualisation de listes

Pour les grandes listes de données, la virtualisation permet de ne rendre que les éléments actuellement visibles à l'écran.

1// Implémentation simple de virtualisation (dans des applications réelles
2// il vaut mieux utiliser des bibliothèques comme react-window ou react-virtualized)
3function VirtualizedAsteroidList({ asteroids, rowHeight = 50, visibleRows = 10 }) {
4  const [scrollTop, setScrollTop] = useState(0);
5  const containerRef = useRef();
6
7  const totalHeight = asteroids.length * rowHeight;
8  const startIndex = Math.floor(scrollTop / rowHeight);
9  const endIndex = Math.min(
10    startIndex + visibleRows + 1,
11    asteroids.length
12  );
13
14  const visibleAsteroids = useMemo(() => {
15    return asteroids.slice(startIndex, endIndex);
16  }, [asteroids, startIndex, endIndex]);
17
18  const handleScroll = useCallback(() => {
19    if (containerRef.current) {
20      setScrollTop(containerRef.current.scrollTop);
21    }
22  }, []);
23
24  return (
25    <div className="asteroids-list">
26      <h2>Catalogue d'astéroïdes</h2>
27      <p>Nombre d'astéroïdes : {asteroids.length}</p>
28
29      <div
30        ref={containerRef}
31        className="virtual-list-container"
32        style={{ height: rowHeight * visibleRows, overflow: 'auto' }}
33        onScroll={handleScroll}
34      >
35        <div
36          className="total-height"
37          style={{ height: totalHeight }}
38        >
39          <div
40            className="visible-items"
41            style={{
42              transform: `translateY(${startIndex * rowHeight}px)`
43            }}
44          >
45            {visibleAsteroids.map(asteroid => (
46              <div
47                key={asteroid.id}
48                className="asteroid-item"
49                style={{ height: rowHeight }}
50              >
51                <span className="name">{asteroid.name}</span>
52                <span className="diameter">Diamètre : {asteroid.diameter} km</span>
53                <span className="distance">
54                  Distance : {asteroid.distance} millions de km
55                </span>
56                <span
57                  className={`hazard-level ${asteroid.hazardLevel}`}
58                >
59                  {formatHazardLevel(asteroid.hazardLevel)}
60                </span>
61              </div>
62            ))}
63          </div>
64        </div>
65      </div>
66    </div>
67  );
68}
69
70function formatHazardLevel(level) {
71  switch (level) {
72    case 'high': return 'Menace élevée';
73    case 'medium': return 'Menace moyenne';
74    case 'low': return 'Menace faible';
75    default: return 'Aucune menace';
76  }
77}

3. Lazy loading

Le lazy loading des composants permet de différer le chargement de parties de l'application jusqu'à ce qu'elles soient nécessaires, ce qui peut considérablement accélérer le chargement initial de l'application.

1// Lazy loading des composants
2const LazyMarsExplorer = lazy(() => import('./MarsExplorer'));
3const LazyJupiterMoons = lazy(() => import('./JupiterMoons'));
4const LazySaturnRings = lazy(() => import('./SaturnRings'));
5
6function SolarSystemExplorer() {
7  const [activeSection, setActiveSection] = useState(null);
8
9  return (
10    <div className="solar-system-explorer">
11      <h1>Exploration du système solaire</h1>
12
13      <div className="planet-selector">
14        <button onClick={() => setActiveSection('mars')}>
15          Exploration de Mars
16        </button>
17        <button onClick={() => setActiveSection('jupiter')}>
18          Lunes de Jupiter
19        </button>
20        <button onClick={() => setActiveSection('saturn')}>
21          Anneaux de Saturne
22        </button>
23      </div>
24
25      <Suspense fallback={<div className="loading">Chargement du module d'exploration...</div>}>
26        {activeSection === 'mars' && <LazyMarsExplorer />}
27        {activeSection === 'jupiter' && <LazyJupiterMoons />}
28        {activeSection === 'saturn' && <LazySaturnRings />}
29      </Suspense>
30    </div>
31  );
32}

Résumé

Les design patterns en React fournissent des solutions éprouvées à des problèmes de programmation courants. Ils te permettent de créer du code qui est :

  1. Réutilisable - les composants et la logique peuvent être facilement utilisés à différents endroits de l'application
  2. Testable - la séparation des responsabilités facilite l'écriture des tests
  3. Scalable - l'application peut grandir sans augmentation drastique de la complexité
  4. Maintenable - une structure et une organisation claires facilitent l'introduction de changements

Au cours de notre voyage cosmique à travers React, ces patterns sont comme des routes interstellaires éprouvées qui te permettent d'atteindre ta destination plus vite et plus sûrement. Expérimente différents patterns et choisis ceux qui correspondent le mieux aux besoins spécifiques de ton application.

Code de cette leçon : App.jsx
1import React, { useState, useEffect, useCallback } from 'react';
2
3// Custom Hook : useLocalStorage
4function useLocalStorage(key, initialValue) {
5  // State pour stocker les valeurs
6  const [storedValue, setStoredValue] = useState(() => {
7    try {
8      const item = window.localStorage.getItem(key);
9      return item ? JSON.parse(item) : initialValue;
10    } catch (error) {
11      console.log('Error reading localStorage:', error);
12      return initialValue;
13    }
14  });
15
16  // Fonction pour mettre a jour les valeurs
17  const setValue = useCallback((value) => {
18    setStoredValue((prev) => (value instanceof Function ? value(prev) : value));
19  }, []);
20
21  // Enregistre dans localStorage à chaque changement
22  useEffect(() => {
23    try {
24      window.localStorage.setItem(key, JSON.stringify(storedValue));
25    } catch (error) {
26      console.log('Error saving to localStorage:', error);
27    }
28  }, [key, storedValue]);
29
30  return [storedValue, setValue];
31}
32
33// Custom Hook : useToggle
34function useToggle(initialValue = false) {
35  const [value, setValue] = useState(initialValue);
36
37  const toggle = useCallback(() => {
38    setValue(v => !v);
39  }, []);
40
41  const setTrue = useCallback(() => {
42    setValue(true);
43  }, []);
44
45  const setFalse = useCallback(() => {
46    setValue(false);
47  }, []);
48
49  return { value, toggle, setTrue, setFalse };
50}
51
52// Custom Hook : useCounter
53function useCounter(initialValue = 0, step = 1) {
54  const [count, setCount] = useState(initialValue);
55
56  const increment = useCallback(() => {
57    setCount(c => c + step);
58  }, [step]);
59
60  const decrement = useCallback(() => {
61    setCount(c => c - step);
62  }, [step]);
63
64  const reset = useCallback(() => {
65    setCount(initialValue);
66  }, [initialValue]);
67
68  return { count, increment, decrement, reset, setCount };
69}
70
71// Demonstration des custom hooks
72function CustomHooksDemo() {
73  // useLocalStorage - stockage des reglages
74  const [username, setUsername] = useLocalStorage('username', '');
75  const [settings, setSettings] = useLocalStorage('settings', {
76    darkMode: true,
77    notifications: true,
78    language: 'pl'
79  });
80
81  // useToggle - bascules
82  const darkMode = useToggle(settings.darkMode);
83  const showPanel = useToggle(false);
84  const isEditing = useToggle(false);
85
86  // useCounter - compteurs
87  const visits = useCounter(0);
88  const score = useCounter(100, 10);
89
90  // Effet de synchronisation avec localStorage
91  useEffect(() => {
92    setSettings(prev => ({ ...prev, darkMode: darkMode.value }));
93  }, [darkMode.value, setSettings]);
94
95  const containerStyle = {
96    minHeight: '100vh',
97    background: darkMode.value
98      ? 'linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)'
99      : 'linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%)',
100    padding: '30px',
101    fontFamily: "'Segoe UI', sans-serif",
102    color: darkMode.value ? '#fff' : '#1a1a2e',
103    transition: 'all 0.5s ease',
104  };
105
106  const cardStyle = {
107    background: darkMode.value ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)',
108    borderRadius: '15px',
109    padding: '25px',
110    marginBottom: '25px',
111    border: '1px solid ' + (darkMode.value ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'),
112  };
113
114  return (
115    <div style={containerStyle}>
116      <header style={{ textAlign: 'center', marginBottom: '40px' }}>
117        <h1 style={{ color: '#00d4ff', fontSize: '2.5em' }}>
118          Custom Hooks Demo
119        </h1>
120        <p style={{ opacity: 0.7 }}>Creer des custom hooks en React</p>
121      </header>
122
123      <div style={{ maxWidth: '900px', margin: '0 auto' }}>
124        {/* useLocalStorage Demo */}
125        <div style={cardStyle}>
126          <h2 style={{ color: '#ffd700', marginBottom: '20px' }}>
127            useLocalStorage
128          </h2>
129          <p style={{ marginBottom: '15px' }}>
130            Stocke les donnees dans localStorage avec synchronisation automatique
131          </p>
132
133          <div style={{ marginBottom: '15px' }}>
134            <label style={{ display: 'block', marginBottom: '5px' }}>
135              Nom d'utilisateur (enregistre dans localStorage) :
136            </label>
137            <input
138              type="text"
139              value={username}
140              onChange={(e) => setUsername(e.target.value)}
141              placeholder="Saisis ton nom"
142              style={{
143                padding: '10px 15px',
144                borderRadius: '8px',
145                border: '2px solid #00d4ff',
146                background: 'transparent',
147                color: 'inherit',
148                width: '300px',
149              }}
150            />
151          </div>
152
153          <p style={{ fontSize: '14px', opacity: 0.7 }}>
154            Rafraichis la page - la valeur sera conservee !
155          </p>
156        </div>
157
158        {/* useToggle Demo */}
159        <div style={cardStyle}>
160          <h2 style={{ color: '#ffd700', marginBottom: '20px' }}>
161            useToggle
162          </h2>
163
164          <div style={{ display: 'flex', gap: '20px', flexWrap: 'wrap' }}>
165            <div style={{ flex: '1', minWidth: '200px' }}>
166              <p style={{ marginBottom: '10px' }}>Mode sombre :</p>
167              <button
168                onClick={darkMode.toggle}
169                style={{
170                  padding: '10px 20px',
171                  background: darkMode.value ? '#4CAF50' : '#f44336',
172                  color: 'white',
173                  border: 'none',
174                  borderRadius: '8px',
175                  cursor: 'pointer',
176                }}
177              >
178                {darkMode.value ? 'ON' : 'OFF'}
179              </button>
180            </div>
181
182            <div style={{ flex: '1', minWidth: '200px' }}>
183              <p style={{ marginBottom: '10px' }}>Afficher le panneau :</p>
184              <button
185                onClick={showPanel.toggle}
186                style={{
187                  padding: '10px 20px',
188                  background: showPanel.value ? '#4CAF50' : '#9e9e9e',
189                  color: 'white',
190                  border: 'none',
191                  borderRadius: '8px',
192                  cursor: 'pointer',
193                }}
194              >
195                {showPanel.value ? 'Masquer' : 'Afficher'}
196              </button>
197              {showPanel.value && (
198                <div style={{
199                  marginTop: '15px',
200                  padding: '15px',
201                  background: 'rgba(0,212,255,0.1)',
202                  borderRadius: '8px',
203                }}>
204                  Panneau visible !
205                </div>
206              )}
207            </div>
208          </div>
209        </div>
210
211        {/* useCounter Demo */}
212        <div style={cardStyle}>
213          <h2 style={{ color: '#ffd700', marginBottom: '20px' }}>
214            useCounter
215          </h2>
216
217          <div style={{ display: 'flex', gap: '40px', flexWrap: 'wrap' }}>
218            <div>
219              <p style={{ marginBottom: '10px' }}>Visites (pas : 1) :</p>
220              <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
221                <button onClick={visits.decrement} style={btnStyle}>-</button>
222                <span style={{ fontSize: '2em', minWidth: '60px', textAlign: 'center' }}>
223                  {visits.count}
224                </span>
225                <button onClick={visits.increment} style={btnStyle}>+</button>
226                <button onClick={visits.reset} style={{ ...btnStyle, background: '#ff9800' }}>
227                  Reset
228                </button>
229              </div>
230            </div>
231
232            <div>
233              <p style={{ marginBottom: '10px' }}>Score (pas : 10) :</p>
234              <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
235                <button onClick={score.decrement} style={btnStyle}>-10</button>
236                <span style={{ fontSize: '2em', minWidth: '80px', textAlign: 'center' }}>
237                  {score.count}
238                </span>
239                <button onClick={score.increment} style={btnStyle}>+10</button>
240                <button onClick={score.reset} style={{ ...btnStyle, background: '#ff9800' }}>
241                  Reset
242                </button>
243              </div>
244            </div>
245          </div>
246        </div>
247      </div>
248
249      <footer style={{ textAlign: 'center', marginTop: '40px', opacity: 0.7 }}>
250        <p>Les custom hooks permettent d'extraire et de partager la logique</p>
251      </footer>
252    </div>
253  );
254}
255
256const btnStyle = {
257  padding: '10px 20px',
258  background: '#00d4ff',
259  color: '#000',
260  border: 'none',
261  borderRadius: '8px',
262  cursor: 'pointer',
263  fontWeight: 'bold',
264  fontSize: '16px',
265};
266
267export default CustomHooksDemo;

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Laquelle des affirmations suivantes est une règle d'utilisation des hooks ?

  2. 2. Qu'est-ce qui caractérise le pattern Render Props ?

Voici 2 des 4 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Éditeur de code

    Custom hook pour la station spatiale

  • Tri horizontal

    Ordonne la syntaxe d'appel d'un Higher-Order Component :

  • Tri vertical

    Ordonne la structure du custom hook :

  • Éditeur de code

    Hook avancé de data fetching

  • Éditeur de code

    HOC avec autorisation et surveillance

  • Clique dans l'ordre

    Ordonne la syntaxe correcte d'un Higher-Order Component :

  • Éditeur de code

    Render Props pour le réseau de capteurs

Articles utiles