Cours JavaScript et React · Module 15: Patterns et architecture
Feature Flags et A/B Testing en React - Des déploiements contrôlés
Dans cette leçon8
Le nouveau système de navigation est prêt, mais personne ne sait comment il se comportera lors d'une vraie mission. Si tu le livres à toute la flotte et que quelque chose tourne mal, le seul recours est un nouveau déploiement. C'est pourquoi, sur un vaisseau spatial, avant d'activer un nouveau système pour toute la flotte, on le teste sur un seul vaisseau. S'il fonctionne, on le déploie sur les autres. Sinon, on le coupe d'un seul interrupteur. C'est exactement ce que sont les Feature Flags : un mécanisme qui permet d'activer et de désactiver des fonctionnalités sans déployer de nouveau code.
Que sont les Feature Flags ?
Un Feature Flag (drapeau de fonctionnalité) est une valeur booléenne ou une configuration qui détermine si une fonctionnalité donnée est disponible. La forme la plus simple est un objet ordinaire contenant les flags, avec une condition dans le JSX :
1// La forme la plus simple - un objet de flags
2const featureFlags = {
3 newDashboard: true,
4 darkMode: true,
5 experimentalNav: false,
6 betaSearch: false,
7};
8
9// Utilisation dans un composant
10function App() {
11 return (
12 <div>
13 {featureFlags.newDashboard
14 ? <NewDashboard />
15 : <OldDashboard />
16 }
17 {featureFlags.betaSearch && <BetaSearchBar />}
18 </div>
19 );
20}Le code de l'ancien et du nouveau tableau de bord se trouve côte à côte, et celui que voit l'équipage dépend d'une seule valeur. L'opérateur && n'affiche la barre de recherche que si son flag est activé. Cet objet est cependant écrit en dur dans le code, donc changer un flag exige toujours un déploiement.
Feature Flag Provider
Dans une vraie application, on stocke les flags dans un contexte pour qu'ils soient accessibles partout sans importer l'objet. Pour les lire, on écrit un petit hook :
1const FeatureFlagContext = createContext({});
2
3function FeatureFlagProvider({ flags, children }) {
4 return (
5 <FeatureFlagContext.Provider value={flags}>
6 {children}
7 </FeatureFlagContext.Provider>
8 );
9}
10
11function useFeatureFlag(flagName) {
12 const flags = useContext(FeatureFlagContext);
13 return flags[flagName] ?? false;
14}Grâce à l'opérateur ??, un flag inconnu donne false au lieu de undefined : une faute de frappe dans un nom désactive simplement la fonctionnalité, et le composant reçoit toujours un booléen. Un composant ne demande plus qu'un seul flag, par son nom :
1// Utilisation
2function NavigationBar() {
3 const showNewNav = useFeatureFlag('newNavigation');
4
5 return showNewNav ? <NewNavBar /> : <ClassicNavBar />;
6}NavigationBar ne sait pas d'où viennent les flags, il ne connaît que leurs noms. Depuis React 19, au lieu de <FeatureFlagContext.Provider value={flags}>, tu peux écrire plus court <FeatureFlagContext value={flags}>, et l'ancienne syntaxe fonctionne toujours.
Composant Feature - des flags déclaratifs
Au lieu d'écrire des if/else dans chaque composant, on peut créer un composant déclaratif qui reçoit le nom d'un flag, un contenu et un fallback facultatif :
1function Feature({ name, children, fallback = null }) {
2 const isEnabled = useFeatureFlag(name);
3 return isEnabled ? children : fallback;
4}Trois lignes suffisent, car c'est le hook qui fait tout le travail. Tu peux même faire tenir le même composant sur une seule ligne, sous forme de fonction fléchée qui renvoie directement le résultat de la condition :
1const Feature = ({ name, children, fallback = null }) => useFeatureFlag(name) ? children : fallback;C'est exactement le même composant : le hook est appelé à chaque rendu et toujours au même endroit, donc les règles des hooks sont respectées. Choisis l'écriture que ton équipe lit le plus facilement. Le centre de contrôle se lit maintenant comme une check-list : affiche ce panneau quand son flag est activé, et remplace celui-là par une version plus ancienne :
1// Utilisation - lisible et déclaratif
2function MissionControl() {
3 return (
4 <div>
5 <h1>Centre de contrôle</h1>
6
7 <Feature name="advancedRadar">
8 <AdvancedRadarPanel />
9 </Feature>
10
11 <Feature
12 name="newStatusBar"
13 fallback={<LegacyStatusBar />}
14 >
15 <ModernStatusBar />
16 </Feature>
17
18 <Feature name="experimentalWarpDrive">
19 <WarpDriveControls />
20 </Feature>
21 </div>
22 );
23}Quand un flag est désactivé et qu'aucun fallback n'est fourni, Feature renvoie null et le panneau disparaît tout simplement. La forme <Feature name="newDashboard" fallback={<OldDashboard />}> avec la nouvelle version comme enfant est la façon la plus courante de basculer entre l'ancienne et la nouvelle vue.
Interrupteurs en vol - des flags dans l'état
Le provider reçoit les flags par les props, donc tu peux les garder dans un état. Un panneau d'administration active alors des fonctionnalités pendant que l'application tourne, et chaque flag reçoit une case à cocher :
1function FlagAdmin({ flags, setFlags }) {
2 const toggle = (name) =>
3 setFlags(prev => ({ ...prev, [name]: !prev[name] }));
4
5 return (
6 <div className="flag-admin">
7 {Object.keys(flags).map(name => (
8 <label key={name}>
9 <input type="checkbox" checked={flags[name]} onChange={() => toggle(name)} />
10 {name}
11 </label>
12 ))}
13 </div>
14 );
15}toggle copie l'objet au lieu de le modifier, car React compare l'état par référence et ne remarquerait pas des changements faits dans le même objet. Il ne reste plus qu'à relier l'état, le provider et le panneau dans le composant racine :
1function App() {
2 const [flags, setFlags] = useState({ newDashboard: true, advancedRadar: false });
3
4 return (
5 <FeatureFlagProvider flags={flags}>
6 <FlagAdmin flags={flags} setFlags={setFlags} />
7 <Feature name="newDashboard" fallback={<OldDashboard />}>
8 <NewDashboard />
9 </Feature>
10 </FeatureFlagProvider>
11 );
12}Suivons le chemin d'un flag après un clic sur une case, car tout le pattern repose dessus :
- Les flags vivent dans l'état de
App:FlagAdminappellesetFlagset l'état reçoit un nouvel objet. FeatureFlagProviderreçoit cet objet par les props et le transmet au contexte commevalue.useFeatureFlag('newDashboard')lit dans le contexte la valeur d'un seul flag.- En fonction de cette valeur,
Featureaffichechildrenoufallback. - L'écran montre le nouveau ou l'ancien tableau de bord.
Le code des tableaux de bord n'a pas changé du tout : ce que voit l'équipage dépend uniquement de l'état en haut de l'arbre.
A/B Testing - des variantes pour différents utilisateurs
L'A/B Testing est une extension des feature flags dans laquelle différents utilisateurs voient différentes versions de l'interface, et toi, tu mesures laquelle fonctionne le mieux. Le hook tire une variante une seule fois et la mémorise dans le localStorage :
1function useABTest(testName, variants) {
2 const [variant, setVariant] = useState(null);
3
4 useEffect(() => {
5 // Vérifier si l'utilisateur a déjà une variante assignée
6 const stored = localStorage.getItem(`ab_${testName}`);
7 if (stored && variants.includes(stored)) {
8 setVariant(stored);
9 } else {
10 // Assigner une variante aléatoirement
11 const randomVariant = variants[
12 Math.floor(Math.random() * variants.length)
13 ];
14 localStorage.setItem(`ab_${testName}`, randomVariant);
15 setVariant(randomVariant);
16 }
17 }, [testName, variants]);
18
19 return variant;
20}Grâce à cette sauvegarde, le même utilisateur verra la même version après un rafraîchissement. Le code se trouve dans useEffect, car le localStorage n'existe que dans le navigateur. Voici à quoi ressemble l'utilisation :
1// Utilisation : le tableau des variantes dans une constante hors du composant
2const CTA_VARIANTS = ['control', 'variant_a', 'variant_b'];
3
4function LandingPage() {
5 const variant = useABTest('landing_cta', CTA_VARIANTS);
6
7 if (variant === 'control') {
8 return <ClassicHero ctaText="Commence ta mission" />;
9 }
10 if (variant === 'variant_a') {
11 return <ClassicHero ctaText="Rejoins la flotte !" />;
12 }
13 return <ModernHero ctaText="Décolle maintenant" />;
14}Attention au premier rendu : variant vaut encore null, donc LandingPage affiche un instant ModernHero avant que l'effet ne tire une variante. Dans du vrai code, gère le cas null, par exemple en renvoyant un squelette de page. Le tableau des variantes se trouve dans la constante CTA_VARIANTS hors du composant, car un nouveau tableau à chaque rendu relancerait l'effet.
Percentage Rollout - déploiements progressifs
Au lieu d'activer un flag pour tout le monde d'un coup, tu peux déployer progressivement, par exemple pour 10 % des utilisateurs, puis 50 %, puis 100 %. Chaque navigateur tire au sort une seule fois s'il entre dans le groupe qui reçoit la nouvelle fonctionnalité :
1function usePercentageRollout(featureName, percentage) {
2 const [isEnabled, setIsEnabled] = useState(false);
3
4 useEffect(() => {
5 // Tirage aléatoire une fois par navigateur, résultat gardé dans le localStorage
6 const stored = localStorage.getItem(`rollout_${featureName}`);
7 if (stored !== null) {
8 setIsEnabled(stored === 'true');
9 } else {
10 const random = Math.random() * 100;
11 const enabled = random < percentage;
12 localStorage.setItem(`rollout_${featureName}`, String(enabled));
13 setIsEnabled(enabled);
14 }
15 }, [featureName, percentage]);
16
17 return isEnabled;
18}
19
20// Utilisation
21function Dashboard() {
22 const showNewChart = usePercentageRollout('new_chart_v2', 30); // 30% des utilisateurs
23
24 return (
25 <div>
26 {showNewChart ? <NewInteractiveChart /> : <LegacyChart />}
27 </div>
28 );
29}Avec un seuil de 30, environ 30 % des navigateurs reçoivent le nouveau graphique, et le résultat sauvegardé ne change pas après un rafraîchissement. Les systèmes professionnels calculent plutôt un hash déterministe à partir de l'identifiant de l'utilisateur et du nom du flag : le résultat est le même sur tous les appareils, et relever le seuil ne retire la fonctionnalité à personne qui l'a déjà.
Flags dynamiques depuis le serveur
En production, les flags viennent souvent du serveur (LaunchDarkly, Flagsmith, Unleash). C'est seulement là qu'on tient la promesse du début de la leçon : tu changes un flag dans un tableau de bord et tu ne déploies rien :
1function useRemoteFlags() {
2 const [flags, setFlags] = useState({});
3 const [loading, setLoading] = useState(true);
4
5 useEffect(() => {
6 fetch('/api/feature-flags')
7 .then(res => res.json())
8 .then(data => {
9 setFlags(data);
10 setLoading(false);
11 })
12 .catch(() => setLoading(false));
13 }, []);
14
15 return { flags, loading };
16}
17
18// Utilisation avec FeatureFlagProvider
19function App() {
20 const { flags, loading } = useRemoteFlags();
21
22 if (loading) return <Spinner />;
23
24 return (
25 <FeatureFlagProvider flags={flags}>
26 <Dashboard />
27 </FeatureFlagProvider>
28 );
29}Le hook récupère les flags une fois, et FeatureFlagProvider les diffuse dans l'arbre. Feature et useFeatureFlag n'ont pas changé du tout, seule la source des données a changé. Si le serveur ne répond pas, .catch termine le chargement avec un objet vide, ce qui laisse les expériences désactivées.
Quand utiliser les Feature Flags ?
| Situation | Type de flag |
|---|---|
| Nouvelle fonctionnalité en développement | Boolean flag (true/false) |
| Test de différentes versions de l'UI | A/B test (variantes) |
| Déploiement progressif auprès des utilisateurs | Percentage rollout |
| Désactivation rapide d'une fonctionnalité à problème | Kill switch |
| Fonctionnalités différentes selon les offres | User-based flag |
Mon conseil : chaque flag est une dette. Dès qu'une fonctionnalité marche pour tout le monde, supprime le flag et l'ancienne branche du code avant qu'ils ne deviennent un labyrinthe de conditions. Les Feature Flags sont comme le panneau de contrôle central d'un vaisseau : tu allumes n'importe quel système d'un seul bouton, tu testes un nouveau moteur sur un vaisseau avant toute la flotte, et tu annules les changements quand quelque chose tourne mal, sans atterrir à la station pour remplacer des pièces. Dans la prochaine leçon, les modules du vaisseau commenceront à se parler par des événements.
Retiens : un flag sépare le déploiement du code de l'activation d'une fonctionnalité.
Code de cette leçon : App.jsx
1import React, { createContext, useContext, useState, useEffect } from 'react';
2
3// === CONTEXTE DES FEATURE FLAGS ===
4const FeatureFlagContext = createContext({});
5
6function FeatureFlagProvider({ flags, children }) {
7 return (
8 <FeatureFlagContext.Provider value={flags}>
9 {children}
10 </FeatureFlagContext.Provider>
11 );
12}
13
14function useFeatureFlag(flagName) {
15 const flags = useContext(FeatureFlagContext);
16 return flags[flagName] ?? false;
17}
18
19// === COMPOSANT DÉCLARATIF <Feature> ===
20function Feature({ name, children, fallback = null }) {
21 const isEnabled = useFeatureFlag(name);
22 return isEnabled ? children : fallback;
23}
24
25// === HOOK DU TEST A/B ===
26function useABTest(testName, variants) {
27 const [variant, setVariant] = useState(null);
28 useEffect(() => {
29 const key = 'ab_' + testName;
30 const stored = localStorage.getItem(key);
31 if (stored && variants.includes(stored)) {
32 setVariant(stored);
33 } else {
34 const picked = variants[Math.floor(Math.random() * variants.length)];
35 localStorage.setItem(key, picked);
36 setVariant(picked);
37 }
38 }, [testName, variants]);
39 return variant;
40}
41
42// === COMPOSANTS D’INTERFACE ===
43function ClassicDashboard() {
44 return (
45 <div className="dashboard classic">
46 <h3>Tableau de bord classique</h3>
47 <p>Vue standard de la mission avec les statistiques de base.</p>
48 <div className="stats-row">
49 <div className="stat">Carburant : 87 %</div>
50 <div className="stat">Vitesse : 12 000</div>
51 <div className="stat">Équipage : 6</div>
52 </div>
53 </div>
54 );
55}
56
57function ModernDashboard() {
58 return (
59 <div className="dashboard modern">
60 <h3>Tableau de bord moderne (NOUVEAU !)</h3>
61 <p>Vue enrichie de la mission avec des données en direct.</p>
62 <div className="stats-row">
63 <div className="stat glow">Carburant : 87 %</div>
64 <div className="stat glow">Vitesse : 12 000 km/s</div>
65 <div className="stat glow">Équipage : 6 actifs</div>
66 <div className="stat glow">Boucliers : 94 %</div>
67 </div>
68 </div>
69 );
70}
71
72function RadarV1() {
73 return <div className="radar v1"><h4>Radar v1</h4><p>Portée de base : 100 al</p></div>;
74}
75
76function RadarV2() {
77 return <div className="radar v2"><h4>Radar v2 (bêta)</h4><p>Portée étendue : 500 al + détection d’anomalies</p></div>;
78}
79
80function CTAButton({ text, color }) {
81 return <button className="cta-btn" style={{ background: color }}>{text}</button>;
82}
83
84// === PANNEAU D’ADMINISTRATION DES FLAGS ===
85function FlagAdmin({ flags, setFlags }) {
86 return (
87 <div className="admin-panel">
88 <h3>Administration des flags</h3>
89 {Object.entries(flags).map(([key, val]) => (
90 <label key={key} className="flag-toggle">
91 <input type="checkbox" checked={val} onChange={() => setFlags(p => ({ ...p, [key]: !p[key] }))} />
92 <span className={"flag-name " + (val ? 'on' : 'off')}>{key} : {val ? 'ACTIVÉ' : 'DÉSACTIVÉ'}</span>
93 </label>
94 ))}
95 </div>
96 );
97}
98
99// Constantes hors du composant : le même tableau à chaque rendu, donc l’effet du test A/B ne redémarre pas
100const CTA_VARIANTS = ['blue', 'green', 'orange'];
101const ctaColors = { blue: '#1565c0', green: '#2e7d32', orange: '#e65100' };
102const variantLabels = { blue: 'bleu', green: 'vert', orange: 'orange' };
103
104// === COMPOSANT PRINCIPAL ===
105export default function App() {
106 const [flags, setFlags] = useState({
107 modernDashboard: true,
108 advancedRadar: false,
109 experimentalWarp: false,
110 });
111
112 const ctaVariant = useABTest('cta_color', CTA_VARIANTS);
113
114 return (
115 <FeatureFlagProvider flags={flags}>
116 <div className="app">
117 <h1>Feature Flags et A/B Testing</h1>
118 <p className="subtitle">Active des fonctionnalités et observe les variantes du test A/B</p>
119
120 <FlagAdmin flags={flags} setFlags={setFlags} />
121
122 <Feature name="modernDashboard" fallback={<ClassicDashboard />}>
123 <ModernDashboard />
124 </Feature>
125
126 <Feature name="advancedRadar" fallback={<RadarV1 />}>
127 <RadarV2 />
128 </Feature>
129
130 <Feature name="experimentalWarp">
131 <div className="warp-panel">
132 <h3>Propulsion warp expérimentale</h3>
133 <p>ATTENTION : cette fonctionnalité est en test alpha !</p>
134 <button className="btn btn-danger">Activer le warp</button>
135 </div>
136 </Feature>
137
138 <div className="section">
139 <h3>Test A/B : bouton CTA (variante : {variantLabels[ctaVariant]})</h3>
140 {ctaVariant && <CTAButton text="Commencer la mission" color={ctaColors[ctaVariant]} />}
141 <p className="meta">Ta variante est enregistrée dans le localStorage</p>
142 </div>
143 </div>
144 </FeatureFlagProvider>
145 );
146}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. Quel est l'objectif principal des Feature Flags dans une application React ?
Tâches pratiques dans le jeu
- Éditeur de code
Construis un système de feature flags sur un context. ___BLANK1___ : FeatureFlagProvider donne au context les flags reçus dans sa prop. ___BLANK2___ : useFeatureFlag lit les flags de FeatureFlagContext avec le hook useContext (un flag inconnu donne false grâce à ?? false). ___BLANK3___ : Feature renvoie children quand le flag est activé, sinon fallback. ___BLANK4___ : dans le panneau FlagAdmin, un clic sur une case à cocher inverse la valeur de ce flag dans un nouvel objet de flags. App garde les flags dans son état, donc un clic bascule tout de suite entre l'ancien et le nouveau tableau de bord.
- Clique dans l'ordre
Clique dans l'ordre les éléments du composant Feature écrit en une ligne : un flag activé affiche children, un flag désactivé affiche fallback.