From 0240aaaf9d5b9db284b595dbb5bb311bffe3e725 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 03:05:30 +0000 Subject: [PATCH] content: OpenFeature React: Audit and Migrate Your LaunchDarkly React App Co-Authored-By: Claude Code Claude-Session: https://claude.ai/code/session_01Jc2zW2UvcfHDtyrfivVcR4 --- .../docs/blog/openfeature-react-migration.md | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 src/content/docs/blog/openfeature-react-migration.md diff --git a/src/content/docs/blog/openfeature-react-migration.md b/src/content/docs/blog/openfeature-react-migration.md new file mode 100644 index 0000000..1bb45da --- /dev/null +++ b/src/content/docs/blog/openfeature-react-migration.md @@ -0,0 +1,173 @@ +--- +title: "OpenFeature React: Audit and Migrate Your LaunchDarkly React App" +description: "Migrate a React app from the LaunchDarkly SDK to OpenFeature. FlagLint audits flag debt, maps every flag key and call type, and builds your migration plan." +date: 2026-08-20 +--- + +# OpenFeature React: Audit and Migrate Your LaunchDarkly React App + +Your React app is probably riddled with `ldClient.variation()` calls and `useFlags()` hooks spread across 30–50 components. They work, they've always worked, and nobody remembers which flags are still live. Then LaunchDarkly's bill arrives — or your platform team announces an OpenFeature mandate — and you realise you have no idea how many flag keys exist across your component tree. + +That's the core problem with LaunchDarkly SDK usage in long-lived React codebases: the calls accumulate invisibly. You can't migrate what you haven't measured. + +This guide walks through auditing a React project with FlagLint, reading the results, and doing the actual LaunchDarkly SDK → OpenFeature React swap, component by component. + +## Why flag debt builds up faster in React + +In a Node.js service, flag evaluations tend to live close together — middleware, handlers, config loaders. In React, they scatter. A flag that controls a header renders in `AppShell.tsx`. The same flag shows up in `MobileNav.tsx`, `DesktopNav.tsx`, and `NavAnalytics.tsx`. One flag key, four call sites, none of them talking to each other. + +Three years in, you have 80 flags in LaunchDarkly, 140 call sites in React, and no map. Flags that shipped are still evaluated on every render. That's flag debt — stale signals running up cost and making every component harder to understand. + +The migration plan cannot start with "pick a component and swap the hook." It has to start with: what flag keys exist, what call type each uses, which ones have a high staleness signal, and what the current readiness score is. FlagLint produces that map. + +## Step 1: Run the FlagLint audit + +FlagLint targets TypeScript and JavaScript source files. Running it against a directory that contains only documentation shows the baseline case: + +```bash +npx flaglint@latest audit ./src/content/docs/ +``` + +Output: + +``` +- Auditing ./src/content/docs/... +No matching files found. Check your .flaglintrc include patterns. +``` + +Correct — that directory holds only Markdown. For a React project, point the audit at `src/`: + +```bash +npx flaglint@latest audit ./src/ +``` + +FlagLint walks every `.ts` and `.tsx` file, extracts every flag key, classifies each call type, assigns staleness signals, and prints the overall readiness score. + +## Step 2: Configure .flaglintrc for React + +Out of the box, FlagLint looks for calls from `launchdarkly-node-server-sdk`. To catch React-specific patterns — the `useFlags()` hook, the `withLDConsumer()` HOC, and the `useLDClient()` hook — add a `.flaglintrc` at the repo root: + +```json +{ + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["src/**/*.test.tsx", "src/**/*.spec.ts"], + "sdks": ["launchdarkly-js-client-sdk", "launchdarkly-react-client-sdk"] +} +``` + +The `launchdarkly-react-client-sdk` entry tells FlagLint to recognise the React SDK's call types. Without it, hook-based flag evaluations are invisible to the audit and won't appear in the flag debt count or the readiness score. + +Re-run after saving `.flaglintrc` and you'll see each flag key, its call type, and whether the staleness signal is high, medium, or low. + +## Step 3: Understand the readiness score + +The readiness score runs from 0 to 100. Zero means every flag evaluation in your codebase goes through the LaunchDarkly SDK. One hundred means every evaluation goes through an OpenFeature provider. + +FlagLint computes it by comparing LaunchDarkly SDK call sites to total flag call sites. A repo at score 40 has moved 40% of its flag evaluations to OpenFeature. The remaining 60% still carry LaunchDarkly SDK imports. + +The readiness score tells you at a glance how far through the migration plan you are, and gives you a concrete number to track PR-by-PR. See [LaunchDarkly Flag Debt: Audit, Estimate, and Prioritize Your Migration](/blog/launchdarkly-flag-debt) for a deeper breakdown of how to read the score alongside flag debt estimates. + +## Step 4: Before and after — LaunchDarkly SDK to OpenFeature React + +### Hooks (the common case) + +**Before — LaunchDarkly React SDK:** + +```tsx +import { useFlags } from 'launchdarkly-react-client-sdk'; + +function BillingPage() { + const { billingV2Rollout } = useFlags(); + return billingV2Rollout ? : ; +} +``` + +**After — OpenFeature React:** + +```tsx +import { useBooleanFlagValue } from '@openfeature/react-sdk'; + +function BillingPage() { + const billingV2Rollout = useBooleanFlagValue('billing-v2-rollout', false); + return billingV2Rollout ? : ; +} +``` + +Two things change. First, the import: `launchdarkly-react-client-sdk` → `@openfeature/react-sdk`. Second, the call type: `useFlags()` returns all flags at once as a camelCase object, so you destructure `billingV2Rollout`. The OpenFeature `useBooleanFlagValue()` evaluates a single flag key and takes the raw kebab-case key `'billing-v2-rollout'` as a string. + +FlagLint's audit marks the camelCase-to-kebab-case normalisation as a staleness signal to watch for. If a flag key was committed as `billingV2Rollout` in one component and `billing-v2-rollout` in another, the audit surfaces both call sites so you can consolidate before migrating. + +### HOC pattern + +Older React codebases use `withLDConsumer` instead of hooks: + +**Before:** + +```tsx +import { withLDConsumer } from 'launchdarkly-react-client-sdk'; + +function FeatureNav({ flags }) { + return flags.showNewNav ? : ; +} + +export default withLDConsumer()(FeatureNav); +``` + +**After:** + +```tsx +import { useBooleanFlagValue } from '@openfeature/react-sdk'; + +function FeatureNav() { + const showNewNav = useBooleanFlagValue('show-new-nav', false); + return showNewNav ? : ; +} + +export default FeatureNav; +``` + +The HOC disappears entirely. OpenFeature React hooks pull from context without a wrapper. FlagLint classifies `withLDConsumer` wraps as their own call type during the audit, so the migration plan counts them separately from hook-based evaluations. + +### Wiring the OpenFeature provider at app root + +Before any hook resolves, set the OpenFeature provider once at the top of your tree. The LaunchDarkly OpenFeature provider is the easiest starting point — it keeps your existing flags working during migration: + +```tsx +import { OpenFeatureProvider, OpenFeature } from '@openfeature/react-sdk'; +import { LaunchDarklyClientProvider } from '@launchdarkly/openfeature-js-client-sdk'; + +const ldClient = LaunchDarklyClientProvider.createClient('your-client-side-id'); +OpenFeature.setProvider(new LaunchDarklyClientProvider(ldClient)); + +function App() { + return ( + + + + ); +} +``` + +Once this is in place, every component that calls `useBooleanFlagValue()` evaluates against the LaunchDarkly OpenFeature provider automatically. You can swap the provider to Flipt, Unleash, or any other OpenFeature-compatible system later without touching component code. That's the architectural value: the migration plan stops being locked to one vendor. + +## Step 5: Clear stale flags before migrating + +Not every flag needs an OpenFeature equivalent. Some flags are done — the rollout shipped, the old branch is deleted, but the LaunchDarkly SDK call is still in the component tree because removing it never made the sprint. + +FlagLint assigns a staleness signal based on source history and call frequency. A flag key with a high staleness signal, a single call site, and a boolean call type is a strong candidate for outright deletion: remove the LaunchDarkly SDK call, remove the conditional branch, archive the flag in LaunchDarkly. That drops flag debt and improves the readiness score without writing any OpenFeature code. + +Kill high-staleness-signal flags first. Then migrate the rest. The [LaunchDarkly Feature Flag Cleanup guide](/blog/launchdarkly-feature-flag-cleanup-typescript) covers the removal workflow in detail for TypeScript codebases. + +## Step 6: Enforce in CI + +Once you start migrating, new LaunchDarkly SDK calls introduced in PRs will quietly undo your progress. FlagLint has a CI mode that fails the build when the readiness score drops below a threshold you set. Wire it into your pipeline so that every PR that adds a new LaunchDarkly SDK call type is blocked until the author migrates it to an OpenFeature React hook instead. See [Enforcing Your LaunchDarkly to OpenFeature Migration in GitHub Actions](/blog/enforce-launchdarkly-migration-github-actions) for the exact workflow and YAML. + +## Next steps + +1. **Run `npx flaglint@latest audit ./src/`** against your actual React source. Get the real flag key count and readiness score before estimating effort. +2. **Add `.flaglintrc`** with `launchdarkly-react-client-sdk` in the `sdks` list so hooks and HOCs appear in the flag debt count. +3. **Delete high-staleness-signal flags first.** Reducing the LaunchDarkly SDK call count before migrating keeps each PR focused. +4. **Wire the OpenFeature provider at app root.** Flags keep working via the LaunchDarkly OpenFeature provider while you migrate component by component. +5. **Track the readiness score per PR.** FlagLint in CI gives you a number that goes up. When it hits 100, the LaunchDarkly SDK import can come out of `package.json`. + +OpenFeature React is a hook-for-hook replacement at the component level. The migration plan is mechanical once you have the audit. FlagLint gives you that map.