Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 173 additions & 0 deletions src/content/docs/blog/openfeature-react-migration.md
Original file line number Diff line number Diff line change
@@ -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 ? <BillingV2 /> : <BillingV1 />;
}
```

**After — OpenFeature React:**

```tsx
import { useBooleanFlagValue } from '@openfeature/react-sdk';

function BillingPage() {
const billingV2Rollout = useBooleanFlagValue('billing-v2-rollout', false);
return billingV2Rollout ? <BillingV2 /> : <BillingV1 />;
}
```

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 ? <NewNav /> : <OldNav />;
}

export default withLDConsumer()(FeatureNav);
```

**After:**

```tsx
import { useBooleanFlagValue } from '@openfeature/react-sdk';

function FeatureNav() {
const showNewNav = useBooleanFlagValue('show-new-nav', false);
return showNewNav ? <NewNav /> : <OldNav />;
}

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 (
<OpenFeatureProvider>
<Router />
</OpenFeatureProvider>
);
}
```

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.
Loading