Kurs JavaScript i React · Moduł 15: Wzorce i architektura
Feature Flags i A/B Testing w React - Kontrolowane wdrożenia
W tej lekcji8
Nowy system nawigacyjny jest gotowy, ale nikt nie wie, jak zachowa się w prawdziwej misji. Jeśli wypuścisz go całej flocie i coś pójdzie nie tak, ratunkiem jest dopiero kolejny deploy. Dlatego na statku kosmicznym, zanim aktywujesz nowy system dla całej floty, testujesz go na jednym statku. Jeśli działa, wdrażasz go na reszcie. Jeśli nie, wyłączasz go jednym przełącznikiem. To właśnie Feature Flags: mechanizm pozwalający włączać i wyłączać funkcjonalności bez deploymentu nowego kodu.
Czym są Feature Flags?
Feature Flag (flaga funkcjonalności) to wartość boolean lub konfiguracja, która decyduje, czy dana funkcja jest dostępna. Najprostsza forma to zwykły obiekt z flagami i warunek w JSX:
1// Najprostsza forma - obiekt z flagami
2const featureFlags = {
3 newDashboard: true,
4 darkMode: true,
5 experimentalNav: false,
6 betaSearch: false,
7};
8
9// Użycie w komponencie
10function App() {
11 return (
12 <div>
13 {featureFlags.newDashboard
14 ? <NewDashboard />
15 : <OldDashboard />
16 }
17 {featureFlags.betaSearch && <BetaSearchBar />}
18 </div>
19 );
20}Kod starego i nowego dashboardu leży obok siebie, a to, który zobaczy załoga, zależy od jednej wartości. Operator && pokazuje pasek wyszukiwania tylko przy włączonej fladze. Ten obiekt jest jednak wpisany w kod, więc zmiana flagi nadal wymaga deploymentu.
Feature Flag Provider
W prawdziwej aplikacji flagi przechowujemy w kontekście, żeby były dostępne wszędzie bez importowania obiektu. Do ich odczytu piszemy mały 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}Dzięki operatorowi ?? nieznana flaga daje false zamiast undefined: literówka w nazwie po prostu wyłącza funkcję, a komponent zawsze dostaje wartość logiczną. Komponent pyta już tylko o jedną flagę po nazwie:
1// Użycie
2function NavigationBar() {
3 const showNewNav = useFeatureFlag('newNavigation');
4
5 return showNewNav ? <NewNavBar /> : <ClassicNavBar />;
6}NavigationBar nie wie, skąd pochodzą flagi, zna tylko ich nazwy. Od React 19 zamiast <FeatureFlagContext.Provider value={flags}> możesz pisać krócej <FeatureFlagContext value={flags}>, a starszy zapis nadal działa.
Komponent Feature - deklaratywne flagi
Zamiast pisać if/else w każdym komponencie, możemy stworzyć deklaratywny komponent, który przyjmuje nazwę flagi, zawartość i opcjonalny fallback:
1function Feature({ name, children, fallback = null }) {
2 const isEnabled = useFeatureFlag(name);
3 return isEnabled ? children : fallback;
4}Trzy linijki wystarczą, bo całą pracę wykonuje hook. Ten sam komponent zmieścisz nawet w jednej linii, jako funkcję strzałkową, która od razu zwraca wynik warunku:
1const Feature = ({ name, children, fallback = null }) => useFeatureFlag(name) ? children : fallback;To dokładnie ten sam komponent: hook jest wywoływany przy każdym renderze i zawsze w tym samym miejscu, więc reguły hooków są zachowane. Wybierz zapis, który jest czytelniejszy dla Twojego zespołu. Teraz centrum dowodzenia czyta się jak lista kontrolna: ten panel pokaż przy włączonej fladze, a tamten zastąp starszą wersją:
1// Użycie - czytelne i deklaratywne
2function MissionControl() {
3 return (
4 <div>
5 <h1>Centrum dowodzenia</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}Gdy flaga jest wyłączona, a fallback nie podano, Feature zwraca null i panel po prostu znika. Zapis <Feature name="newDashboard" fallback={<OldDashboard />}> z nową wersją jako dzieckiem to najczęstszy sposób przełączania między starym i nowym widokiem.
Przełączniki w locie - flagi w stanie
Provider dostaje flagi przez props, więc można trzymać je w stanie. Wtedy panel administratora przełącza funkcje w trakcie działania aplikacji, a każda flaga dostaje checkbox:
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 kopiuje obiekt zamiast go modyfikować, bo React porównuje stan po referencji i zmiany w tym samym obiekcie by nie zauważył. Pozostaje połączyć stan, provider i panel w komponencie głównym:
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}Prześledźmy drogę flagi po kliknięciu checkboxa, bo na niej opiera się cały wzorzec:
- Flagi siedzą w stanie
App:FlagAdminwywołujesetFlagsi stan dostaje nowy obiekt. FeatureFlagProviderotrzymuje ten obiekt przez props i przekazuje go do kontekstu jakovalue.useFeatureFlag('newDashboard')czyta z kontekstu wartość jednej flagi.Featurena tej podstawie renderujechildrenalbofallback.- Ekran pokazuje nowy albo stary dashboard.
Kod dashboardów nie zmienił się ani trochę: o tym, co widzi załoga, decyduje wyłącznie stan na szczycie drzewa.
A/B Testing - warianty dla różnych użytkowników
A/B Testing to rozszerzenie feature flags, w którym różni użytkownicy widzą różne wersje interfejsu, a Ty mierzysz, która działa lepiej. Hook losuje wariant raz i zapamiętuje go w localStorage:
1function useABTest(testName, variants) {
2 const [variant, setVariant] = useState(null);
3
4 useEffect(() => {
5 // Sprawdź, czy użytkownik już ma przypisany wariant
6 const stored = localStorage.getItem(`ab_${testName}`);
7 if (stored && variants.includes(stored)) {
8 setVariant(stored);
9 } else {
10 // Losowo przypisz wariant
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}Dzięki zapisowi ten sam użytkownik po odświeżeniu zobaczy tę samą wersję. Kod siedzi w useEffect, bo localStorage istnieje tylko w przeglądarce. Tak wygląda użycie:
1// Użycie: tablica wariantów w stałej poza komponentem
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="Rozpocznij misję" />;
9 }
10 if (variant === 'variant_a') {
11 return <ClassicHero ctaText="Dołącz do floty!" />;
12 }
13 return <ModernHero ctaText="Startuj teraz" />;
14}Uwaga na pierwszy render: variant to jeszcze null, więc LandingPage na moment pokaże ModernHero, zanim efekt wylosuje wariant. W prawdziwym kodzie obsłuż null, na przykład zwracając szkielet strony. Tablica wariantów leży w stałej CTA_VARIANTS poza komponentem, bo nowa tablica przy każdym renderze uruchamiałaby efekt ponownie.
Percentage Rollout - stopniowe wdrożenia
Zamiast włączać flagę dla wszystkich naraz, można wdrażać stopniowo, np. 10% użytkowników, potem 50%, potem 100%. Każda przeglądarka losuje raz, czy trafia do grupy z nową funkcją:
1function usePercentageRollout(featureName, percentage) {
2 const [isEnabled, setIsEnabled] = useState(false);
3
4 useEffect(() => {
5 // Losowanie raz na przeglądarkę, wynik zapisany w 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// Użycie
21function Dashboard() {
22 const showNewChart = usePercentageRollout('new_chart_v2', 30); // 30% użytkowników
23
24 return (
25 <div>
26 {showNewChart ? <NewInteractiveChart /> : <LegacyChart />}
27 </div>
28 );
29}Przy progu 30 nowy wykres dostanie około 30% przeglądarek, a zapisany wynik nie zmienia się przy odświeżeniu. Profesjonalne systemy liczą zamiast tego deterministyczny hash z identyfikatora użytkownika i nazwy flagi: wynik jest taki sam na każdym urządzeniu, a podniesienie progu nie odbiera funkcji nikomu, kto już ją ma.
Dynamiczne flagi z serwera
W produkcji flagi często przychodzą z serwera (LaunchDarkly, Flagsmith, Unleash). Dopiero wtedy spełniamy obietnicę z początku lekcji: zmieniasz flagę w panelu i niczego nie wdrażasz:
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// Użycie z 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}Hook pobiera flagi raz, a FeatureFlagProvider rozsyła je po drzewie. Feature i useFeatureFlag nie zmieniły się wcale, zmieniło się tylko źródło danych. Gdy serwer nie odpowie, .catch kończy ładowanie z pustym obiektem, czyli z wyłączonymi eksperymentami.
Kiedy używać Feature Flags?
| Sytuacja | Typ flagi |
|---|---|
| Nowa funkcja w trakcie developmentu | Boolean flag (true/false) |
| Testowanie różnych wersji UI | A/B test (warianty) |
| Stopniowe wdrożenie dla użytkowników | Percentage rollout |
| Szybkie wyłączenie problematycznej funkcji | Kill switch |
| Różne funkcje dla różnych planów | User-based flag |
Moja rada: każda flaga to dług. Gdy funkcja działa już u wszystkich, usuń flagę i starą gałąź kodu, zanim zrobi się z nich labirynt warunków. Feature Flags to jak centralny panel kontroli statku: włączasz dowolny system jednym przyciskiem, testujesz nowy silnik na jednym statku przed całą flotą i cofasz zmiany, gdy coś pójdzie nie tak, bez lądowania na stacji i wymiany części. W następnej lekcji moduły statku zaczną rozmawiać ze sobą przez zdarzenia.
Pamiętaj: flaga oddziela wdrożenie kodu od włączenia funkcji.
Kod do tej lekcji: App.jsx
1import React, { createContext, useContext, useState, useEffect } from 'react';
2
3// === KONTEKST 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// === DEKLARATYWNY KOMPONENT <Feature> ===
20function Feature({ name, children, fallback = null }) {
21 const isEnabled = useFeatureFlag(name);
22 return isEnabled ? children : fallback;
23}
24
25// === HOOK TESTU 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// === KOMPONENTY UI ===
43function ClassicDashboard() {
44 return (
45 <div className="dashboard classic">
46 <h3>Klasyczny dashboard</h3>
47 <p>Standardowy przegląd misji z podstawowymi statystykami.</p>
48 <div className="stats-row">
49 <div className="stat">Paliwo: 87%</div>
50 <div className="stat">Prędkość: 12 tys.</div>
51 <div className="stat">Załoga: 6</div>
52 </div>
53 </div>
54 );
55}
56
57function ModernDashboard() {
58 return (
59 <div className="dashboard modern">
60 <h3>Nowoczesny dashboard (NOWOŚĆ!)</h3>
61 <p>Rozszerzony przegląd misji z danymi na żywo.</p>
62 <div className="stats-row">
63 <div className="stat glow">Paliwo: 87%</div>
64 <div className="stat glow">Prędkość: 12 tys. km/s</div>
65 <div className="stat glow">Załoga: 6 aktywnych</div>
66 <div className="stat glow">Osłony: 94%</div>
67 </div>
68 </div>
69 );
70}
71
72function RadarV1() {
73 return <div className="radar v1"><h4>Radar v1</h4><p>Zasięg podstawowy: 100 l.ś.</p></div>;
74}
75
76function RadarV2() {
77 return <div className="radar v2"><h4>Radar v2 (Beta)</h4><p>Zasięg rozszerzony: 500 l.ś. + wykrywanie anomalii</p></div>;
78}
79
80function CTAButton({ text, color }) {
81 return <button className="cta-btn" style={{ background: color }}>{text}</button>;
82}
83
84// === PANEL ADMINISTRATORA FLAG ===
85function FlagAdmin({ flags, setFlags }) {
86 return (
87 <div className="admin-panel">
88 <h3>Panel administratora flag</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 ? 'WŁĄCZONA' : 'WYŁĄCZONA'}</span>
93 </label>
94 ))}
95 </div>
96 );
97}
98
99// Stałe poza komponentem: ta sama tablica przy każdym renderze, więc efekt testu A/B nie startuje ponownie
100const CTA_VARIANTS = ['blue', 'green', 'orange'];
101const ctaColors = { blue: '#1565c0', green: '#2e7d32', orange: '#e65100' };
102const variantLabels = { blue: 'niebieski', green: 'zielony', orange: 'pomarańczowy' };
103
104// === GŁÓWNY KOMPONENT ===
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 i A/B Testing</h1>
118 <p className="subtitle">Przełączaj funkcje i sprawdzaj warianty testu 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>Eksperymentalny napęd warp</h3>
133 <p>UWAGA: ta funkcja jest w fazie testów alfa!</p>
134 <button className="btn btn-danger">Włącz warp</button>
135 </div>
136 </Feature>
137
138 <div className="section">
139 <h3>Test A/B: przycisk CTA (wariant: {variantLabels[ctaVariant]})</h3>
140 {ctaVariant && <CTAButton text="Rozpocznij misję" color={ctaColors[ctaVariant]} />}
141 <p className="meta">Twój wariant jest zapisany w localStorage</p>
142 </div>
143 </div>
144 </FeatureFlagProvider>
145 );
146}Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jaki jest główny cel Feature Flags w aplikacji React?
Zadania praktyczne w grze
- Edytor kodu
Zbuduj system flag funkcji na kontekście. ___BLANK1___: FeatureFlagProvider podaje kontekstowi flagi, które dostał w propsie. ___BLANK2___: useFeatureFlag odczytuje flagi z FeatureFlagContext hookiem useContext (nieznana flaga daje false dzięki ?? false). ___BLANK3___: Feature zwraca children, gdy flaga jest włączona, a w przeciwnym razie fallback. ___BLANK4___: w panelu FlagAdmin kliknięcie checkboxa odwraca wartość tej flagi w nowym obiekcie flag. App trzyma flagi w stanie, więc kliknięcie od razu przełącza stary i nowy dashboard.
- Klikanie w kolejności
Kliknij w kolejności elementy jednolinijkowego komponentu Feature: włączona flaga pokazuje children, wyłączona fallback.