Feature-Flags in React umschalten, ohne dass sie hängen bleiben
localflag schaltet Feature-Flags in React typsicher und direkt im Browser um, mit einem eigenen Panel. Wo umgeschaltete Flags hängen bleiben, was useFeatureFlag wirklich prüft und warum nichts davon geheim ist.

Manche Funktionen sind fertig genug, um sie zu mergen, aber noch nicht fertig genug für alle, zum Beispiel ein neuer Checkout oder eine Einstellung, die erst ein paar Leute testen sollen. Dafür gibt es Feature-Flags: Der Code liegt schon im Build, angezeigt wird er erst, wenn das Flag an ist.
Für React-Projekte habe ich localflag gebaut. Das Paket ist klein, die Flags sind typsicher, und im mitgelieferten Panel schaltest du sie direkt im Browser um. Einen Server braucht es dafür nicht.
Flags anlegen
npm install @localflag/react
Das Paket setzt React ab Version 18 voraus. Die Flags stehen an einer Stelle, mit as const, damit TypeScript die Namen und Werte kennt:
// flags.ts
export const defaultFlags = {
newCheckout: false,
newHeader: true,
maxItems: 10,
apiVersion: "v1",
} as const;
export type AppFlags = typeof defaultFlags;
Wer zu jedem Flag eine kurze Erklärung möchte, nimmt createFlags. Das liefert die Flags und die Beschreibungen getrennt zurück. Gibst du die Beschreibungen dem Provider über descriptions mit, zeigt das Panel sie neben den Schaltern an.
Dann kommt der Provider um die App:
import { FeatureFlagProvider, LocalFlagDevTools } from "@localflag/react";
import { defaultFlags, type AppFlags } from "./flags";
export function App() {
return (
<FeatureFlagProvider<AppFlags> defaultFlags={defaultFlags}>
<Shop />
<LocalFlagDevTools />
</FeatureFlagProvider>
);
}
In den Komponenten fragst du die Flags mit Hooks ab, oder du nimmst die Komponente FeatureFlag mit einem Fallback:
<FeatureFlag<AppFlags> flag="newCheckout" fallback={<OldCheckout />}>
<NewCheckout />
</FeatureFlag>
Vertippst du dich beim Namen, meckert TypeScript, weil flag nur die Schlüssel aus AppFlags annimmt.
Das Panel
LocalFlagDevTools legt ein schwebendes Panel über die Seite. Dort schaltest du Flags um, ohne den Code anzufassen, und testest so beide Zustände einer Seite hintereinander, ohne neu zu bauen.
Auf- und zuklappen geht auch mit Cmd+Shift+F, unter Windows und Linux mit Ctrl+Shift+F. Mit shortcut={false} schaltest du das Kürzel ab. Eine andere Kombination kannst du dort zwar als String eintragen, in Version 0.0.6 wertet localflag den aber nicht aus und reagiert weiter auf Cmd/Ctrl+Shift+F.
Die Ecke legst du mit position fest oder im Panel unter „Options“. Die aktuelle Ecke speichert localflag schon beim ersten Laden unter localflag:position im localStorage, und dieser Wert gewinnt danach gegen die Prop. Änderst du position später im Code, bleibt das Panel in deinem Browser also da, wo es vorher war.
Umgeschaltete Flags bleiben hängen
Was du im Panel umschaltest, landet als Override im localStorage, unter dem Schlüssel localflag:overrides. Das ist Absicht: Nach einem Reload steht das Flag noch so, wie du es gestellt hast. Im README steht als Standard zwar feature-flags, der Code nimmt aber localflag:overrides. Nach diesem Schlüssel suchst du also, wenn du im Browser nachsiehst.
Die Overrides gewinnen immer gegen die Standardwerte. Änderst du später im Code newCheckout auf true, siehst du in deinem Browser trotzdem weiter false, wenn du das Flag dort einmal ausgeschaltet hattest. Das sieht dann so aus, als wäre die Änderung nicht angekommen. resetFlags() aus useFeatureFlagControls oder der Knopf „Reset overrides“ im Panel löscht alle Overrides. Mit storageKey gibst du jeder App einen eigenen Schlüssel, damit sich zwei Projekte auf localhost nicht gegenseitig die Flags verstellen.
useFeatureFlag fragt nur, ob ein Wert truthy ist
Flags dürfen Booleans, Strings oder Zahlen sein. useFeatureFlag gibt aber immer einen Boolean zurück, und zwar das Ergebnis von Boolean(wert). Für maxItems: 10 heißt das true, für 0 heißt es false. Ein String "false" ist ebenfalls true, weil er nicht leer ist.
Für alles, was kein echter Schalter ist, nimm deshalb useFeatureFlagValue:
const maxItems = useFeatureFlagValue<AppFlags>("maxItems");
const apiVersion = useFeatureFlagValue<AppFlags>("apiVersion");
Alles passiert im Browser
Die Standardwerte stecken im ausgelieferten JavaScript, die Overrides im localStorage des Besuchers. Das Panel blendet sich nur aus, wenn eine globale Variable __DEV__ auf false steht, und die setzt zum Beispiel Vite nicht von selbst. Wenn du <LocalFlagDevTools /> im Produktions-Build renderst, sehen es also auch deine Besucher.
Bind das Panel deshalb nur in der Entwicklung ein und speichere die Overrides auch nur dort. In einem Vite-Projekt sieht das so aus:
<FeatureFlagProvider<AppFlags>
defaultFlags={defaultFlags}
persistOverrides={import.meta.env.DEV}
>
<Shop />
{import.meta.env.DEV && <LocalFlagDevTools />}
</FeatureFlagProvider>
Ganz verstecken lässt sich ein Flag so trotzdem nicht. Die Standardwerte und der Code hinter dem Flag stecken im Bundle, und wer die Entwicklertools des Browsers öffnet, findet beides. Um etwas Halbfertiges auszublenden, reicht das. Was wirklich geheim bleiben muss, hat im Client-Code nichts verloren.
Next.js und Server-Rendering
Das Paket enthält keine "use client"-Direktive. Im App Router von Next.js legst du den Provider deshalb in eine eigene Datei, die mit "use client" beginnt, und bindest diese im Layout ein.
Beim Server-Rendering gibt es außerdem keinen localStorage. Der Server rendert also immer mit den Standardwerten. Liegen im Browser Overrides, startet der Client mit anderen Werten. Hängt die Ausgabe an so einem Flag, passt das HTML vom Server nicht zum ersten Rendern im Browser, und React meldet einen Hydration-Fehler. Für Seiten, die auf dem Server gerendert werden, stellst du persistOverrides deshalb besser auf false und schaltest Flags dort nur über die Standardwerte.
Wofür es passt
localflag ist ein Werkzeug für die Entwicklung und für kleine Projekte, in denen ein Flag im Code reicht. Du mergst eine halbfertige Funktion hinter einem Flag, schaltest sie lokal ein und aus und stellst den Standardwert um, wenn sie fertig ist. Danach kann das Flag wieder raus. Sollen Flags für bestimmte Nutzer gelten oder ohne neues Deployment umgeschaltet werden, braucht es einen Dienst mit Server dahinter.