JavaScript and React course Β· Module 15: Patterns and Architecture
Feature Flags and A/B Testing in React - Controlled deployments
In this lesson8
The new navigation system is ready, but nobody knows how it will behave on a real mission. If you release it to the whole fleet and something goes wrong, the only rescue is another deploy. That is why on a spaceship, before activating a new system for the entire fleet, you test it on one ship. If it works, you deploy it to the rest. If not, you turn it off with a single switch. This is exactly what Feature Flags are: a mechanism that lets you enable and disable features without deploying new code.
What are Feature Flags?
A Feature Flag (feature toggle) is a boolean value or configuration that determines whether a given feature is available. The simplest form is a plain object with flags and a condition in JSX:
1// Simplest form - object with flags
2const featureFlags = {
3 newDashboard: true,
4 darkMode: true,
5 experimentalNav: false,
6 betaSearch: false,
7};
8
9// Usage in a component
10function App() {
11 return (
12 <div>
13 {featureFlags.newDashboard
14 ? <NewDashboard />
15 : <OldDashboard />
16 }
17 {featureFlags.betaSearch && <BetaSearchBar />}
18 </div>
19 );
20}The code of the old and the new dashboard sits side by side, and which one the crew sees depends on a single value. The && operator shows the search bar only when its flag is on. This object is hard-coded, though, so changing a flag still requires a deployment.
Feature Flag Provider
In a real application, we store flags in context so they are available everywhere without importing the object. To read them, we write a small 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}Thanks to the ?? operator, an unknown flag gives false instead of undefined: a typo in a name simply turns the feature off, and the component always gets a boolean. A component now only asks about one flag by name:
1// Usage
2function NavigationBar() {
3 const showNewNav = useFeatureFlag('newNavigation');
4
5 return showNewNav ? <NewNavBar /> : <ClassicNavBar />;
6}NavigationBar does not know where the flags come from, it only knows their names. Since React 19, instead of <FeatureFlagContext.Provider value={flags}> you can write the shorter <FeatureFlagContext value={flags}>, and the older form still works.
Feature component - declarative flags
Instead of writing if/else in every component, we can create a declarative component that takes a flag name, content and an optional fallback:
1function Feature({ name, children, fallback = null }) {
2 const isEnabled = useFeatureFlag(name);
3 return isEnabled ? children : fallback;
4}Three lines are enough, because the hook does all the work. You can even fit the same component on a single line, as an arrow function that returns the result of the condition right away:
1const Feature = ({ name, children, fallback = null }) => useFeatureFlag(name) ? children : fallback;It is exactly the same component: the hook is called on every render and always in the same place, so the rules of hooks are respected. Pick the form your team finds easier to read. Now mission control reads like a checklist: show this panel when its flag is on, and replace that one with an older version:
1// Usage - readable and declarative
2function MissionControl() {
3 return (
4 <div>
5 <h1>Mission Control</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}When a flag is off and no fallback was given, Feature returns null and the panel simply disappears. The form <Feature name="newDashboard" fallback={<OldDashboard />}> with the new version as its child is the most common way to switch between the old and the new view.
Switches on the fly - flags in state
The provider receives flags through props, so you can keep them in state. Then an admin panel toggles features while the application is running, and every flag gets a 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 copies the object instead of modifying it, because React compares state by reference and would not notice changes made to the same object. All that is left is to connect the state, the provider and the panel in the root component:
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}Let us follow the path of a flag after a checkbox click, because the whole pattern rests on it:
- The flags live in the state of
App:FlagAdmincallssetFlagsand the state gets a new object. FeatureFlagProviderreceives that object through props and passes it into the context asvalue.useFeatureFlag('newDashboard')reads the value of one flag from the context.- Based on it,
Featurerenderschildrenorfallback. - The screen shows the new or the old dashboard.
The dashboard code did not change at all: what the crew sees depends only on the state at the top of the tree.
A/B Testing - variants for different users
A/B Testing is an extension of feature flags where different users see different versions of the interface, and you measure which one works better. The hook draws a variant once and remembers it in localStorage:
1function useABTest(testName, variants) {
2 const [variant, setVariant] = useState(null);
3
4 useEffect(() => {
5 // Check if user already has an assigned variant
6 const stored = localStorage.getItem(`ab_${testName}`);
7 if (stored && variants.includes(stored)) {
8 setVariant(stored);
9 } else {
10 // Randomly assign a variant
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}Thanks to the stored value, the same user will see the same version after a refresh. The code lives in useEffect, because localStorage exists only in the browser. This is what using it looks like:
1// Usage: the variants array in a constant outside the component
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="Start your mission" />;
9 }
10 if (variant === 'variant_a') {
11 return <ClassicHero ctaText="Join the fleet!" />;
12 }
13 return <ModernHero ctaText="Launch now" />;
14}Watch out for the first render: variant is still null, so LandingPage briefly shows ModernHero before the effect draws a variant. In real code, handle null, for example by returning a page skeleton. The variants array lives in the CTA_VARIANTS constant outside the component, because a new array on every render would run the effect again.
Percentage Rollout - gradual deployments
Instead of enabling a flag for everyone at once, you can deploy gradually, e.g. to 10% of users, then 50%, then 100%. Each browser draws once whether it lands in the group with the new feature:
1function usePercentageRollout(featureName, percentage) {
2 const [isEnabled, setIsEnabled] = useState(false);
3
4 useEffect(() => {
5 // Random draw once per browser, result saved in 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// Usage
21function Dashboard() {
22 const showNewChart = usePercentageRollout('new_chart_v2', 30); // 30% of users
23
24 return (
25 <div>
26 {showNewChart ? <NewInteractiveChart /> : <LegacyChart />}
27 </div>
28 );
29}With a threshold of 30, about 30% of browsers get the new chart, and the stored result does not change after a refresh. Professional systems compute a deterministic hash of the user id and the flag name instead: the result is the same on every device, and raising the threshold does not take the feature away from anyone who already has it.
Dynamic flags from the server
In production, flags often come from the server (LaunchDarkly, Flagsmith, Unleash). Only then do we keep the promise from the beginning of the lesson: you change a flag in a dashboard and deploy nothing:
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// Usage with 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}The hook fetches the flags once, and FeatureFlagProvider distributes them through the tree. Feature and useFeatureFlag did not change at all, only the data source did. When the server does not respond, .catch finishes loading with an empty object, which means experiments stay off.
When to use Feature Flags?
| Situation | Flag type |
|---|---|
| New feature under development | Boolean flag (true/false) |
| Testing different UI versions | A/B test (variants) |
| Gradual deployment to users | Percentage rollout |
| Quick disable of a problematic feature | Kill switch |
| Different features for different plans | User-based flag |
My advice: every flag is a debt. Once a feature works for everyone, remove the flag and the old code branch before they turn into a maze of conditions. Feature Flags are like a ship's central control panel: you switch any system on with one button, test a new engine on one ship before the whole fleet, and roll back changes when something goes wrong, without landing at a station and replacing parts. In the next lesson the ship's modules will start talking to each other through events.
Remember: a flag separates deploying code from turning a feature on.
Code for this lesson: App.jsx
1import React, { createContext, useContext, useState, useEffect } from 'react';
2
3// === FEATURE FLAG CONTEXT ===
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// === DECLARATIVE <Feature> COMPONENT ===
20function Feature({ name, children, fallback = null }) {
21 const isEnabled = useFeatureFlag(name);
22 return isEnabled ? children : fallback;
23}
24
25// === A/B TEST HOOK ===
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// === UI COMPONENTS ===
43function ClassicDashboard() {
44 return (
45 <div className="dashboard classic">
46 <h3>Classic Dashboard</h3>
47 <p>Standard mission overview with basic stats.</p>
48 <div className="stats-row">
49 <div className="stat">Fuel: 87%</div>
50 <div className="stat">Speed: 12k</div>
51 <div className="stat">Crew: 6</div>
52 </div>
53 </div>
54 );
55}
56
57function ModernDashboard() {
58 return (
59 <div className="dashboard modern">
60 <h3>Modern Dashboard (NEW!)</h3>
61 <p>Enhanced mission overview with real-time data.</p>
62 <div className="stats-row">
63 <div className="stat glow">Fuel: 87%</div>
64 <div className="stat glow">Speed: 12k km/s</div>
65 <div className="stat glow">Crew: 6 active</div>
66 <div className="stat glow">Shields: 94%</div>
67 </div>
68 </div>
69 );
70}
71
72function RadarV1() {
73 return <div className="radar v1"><h4>Radar v1</h4><p>Basic range: 100 ly</p></div>;
74}
75
76function RadarV2() {
77 return <div className="radar v2"><h4>Radar v2 (Beta)</h4><p>Extended range: 500 ly + anomaly detection</p></div>;
78}
79
80function CTAButton({ text, color }) {
81 return <button className="cta-btn" style={{ background: color }}>{text}</button>;
82}
83
84// === FLAG ADMIN PANEL ===
85function FlagAdmin({ flags, setFlags }) {
86 return (
87 <div className="admin-panel">
88 <h3>Feature Flag Admin</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 ? 'ON' : 'OFF'}</span>
93 </label>
94 ))}
95 </div>
96 );
97}
98
99// Constants outside the component: the same array on every render, so the A/B test effect does not start again
100const CTA_VARIANTS = ['blue', 'green', 'orange'];
101const ctaColors = { blue: '#1565c0', green: '#2e7d32', orange: '#e65100' };
102const variantLabels = { blue: 'Blue', green: 'Green', orange: 'Orange' };
103
104// === MAIN ===
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 & A/B Testing</h1>
118 <p className="subtitle">Toggle features and see A/B test variants</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>Experimental Warp Drive</h3>
133 <p>WARNING: This feature is in alpha testing!</p>
134 <button className="btn btn-danger">Engage Warp</button>
135 </div>
136 </Feature>
137
138 <div className="section">
139 <h3>A/B Test: CTA Button (variant: {variantLabels[ctaVariant]})</h3>
140 {ctaVariant && <CTAButton text="Start Mission" color={ctaColors[ctaVariant]} />}
141 <p className="meta">Your variant is persisted in localStorage</p>
142 </div>
143 </div>
144 </FeatureFlagProvider>
145 );
146}Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What is the main purpose of Feature Flags in a React application?
Hands-on tasks in the game
- Code editor
Build a feature flag system on context. ___BLANK1___: FeatureFlagProvider gives the context the flags it received in its prop. ___BLANK2___: useFeatureFlag reads the flags from FeatureFlagContext with the useContext hook (an unknown flag gives false thanks to ?? false). ___BLANK3___: Feature returns children when the flag is enabled and fallback otherwise. ___BLANK4___: in the FlagAdmin panel a click on a checkbox flips the value of that flag in a new flags object. App keeps the flags in state, so a click switches between the old and the new dashboard at once.
- Click in order
Click in order the pieces of the one-line Feature component: an enabled flag shows children, a disabled one shows fallback.