Skip to content
Draft
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"packages/sdk/react/contract-tests",
"packages/sdk/react/examples/hello-react",
"packages/sdk/react/examples/server-only",
"packages/sdk/react/examples/vercel-edge",
"packages/sdk/react-native",
"packages/sdk/react-native/example",
"packages/sdk/react-native/contract-tests/entity",
Expand Down
1 change: 1 addition & 0 deletions packages/sdk/react/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ This directory contains example applications demonstrating the LaunchDarkly Reac
|---------|-------------|
| [hello-react](./hello-react/) | Minimal Vite + React app that evaluates a boolean feature flag and displays the result with real-time updates. This is the recommended starting point. |
| [server-only](./server-only/) | Next.js App Router example demonstrating server-side flag evaluation with React Server Components. |
| [vercel-edge](./vercel-edge/) | Next.js App Router example using the Vercel Edge SDK to evaluate flags from Vercel Edge Config, with server-to-client bootstrap via `LDIsomorphicProvider`. |
43 changes: 43 additions & 0 deletions packages/sdk/react/examples/vercel-edge/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage
/test-results

# next.js
/.next/
/out/
/.swc/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
84 changes: 84 additions & 0 deletions packages/sdk/react/examples/vercel-edge/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# LaunchDarkly sample React + Vercel Edge application

We've built a simple web application that demonstrates how the LaunchDarkly React SDK works with
the Vercel Edge SDK. The app evaluates feature flags using data stored in
[Vercel Edge Config](https://vercel.com/docs/edge-config/overview) and renders the result using
React Server Components.

The Vercel SDK reads flag data from Edge Config instead of connecting to LaunchDarkly servers
directly, providing ultra-low latency flag evaluation at the edge.

The demo shows 2 ways to use React server-side rendering:

1. Using `createLDServerSession` and `useLDServerSession` to provide
per-request session isolation: Nested Server Components access the session through React's `cache()`
without any prop drilling.

2. Using the `LDIsomorphicProvider` to bootstrap the browser SDK with server-evaluated flag values.
This allows the browser SDK to start immediately with real values.

Below, you'll find the build procedure. For more comprehensive instructions, you can visit your
[Quickstart page](https://app.launchdarkly.com/quickstart#/) or the
[React SDK reference guide](https://docs.launchdarkly.com/sdk/client-side/react/react-web).

This demo requires Node.js 18 or higher.

## Prerequisites

This example requires the [LaunchDarkly Vercel integration](https://vercel.com/integrations/launchdarkly)
to be configured. The integration syncs your LaunchDarkly flag data to Vercel Edge Config so that
the Vercel SDK can read it without connecting to LaunchDarkly servers.

## Build instructions

1. Set the `VERCEL_EDGE_CONFIG` environment variable to your Vercel Edge Config connection string.
You can find this in your Vercel project settings under Edge Config.

```bash
export VERCEL_EDGE_CONFIG="https://edge-config.vercel.com/ecfg_..."
```

2. Set the `LD_CLIENT_SIDE_ID` environment variable to your LaunchDarkly client-side ID.
The Vercel SDK uses this to look up flag data in Edge Config, and the same value is used
to bootstrap the browser SDK.

```bash
export LD_CLIENT_SIDE_ID="my-client-side-id"
```

3. If there is an existing boolean feature flag in your LaunchDarkly project that you want to
evaluate, set `LAUNCHDARKLY_FLAG_KEY`:

```bash
export LAUNCHDARKLY_FLAG_KEY="my-flag-key"
```

Otherwise, `sample-feature` will be used by default.

## Running

On the command line, run:

```bash
yarn dev
```

Then open [http://localhost:3000](http://localhost:3000) in your browser. You will see the
spec message, current context name, and a full-page background: green when the flag is on,
or grey when off.

To simulate a different user, append the `?context=` query parameter. Each tab gets a
completely independent `LDServerSession` with its own context:

| URL | Context |
|-----|---------|
| `http://localhost:3000/` | Sandy (example-user-key) — default |
| `http://localhost:3000/?context=sandy` | Sandy (example-user-key) |
| `http://localhost:3000/?context=jamie` | Jamie (user-jamie) |
| `http://localhost:3000/?context=alex` | Alex (user-alex) |

If you have targeting rules in LaunchDarkly that serve different values to different user keys,
you will see different flag results for each context.

In a production app, the user identity would come from auth tokens, cookies, or session data
instead of query parameters.
41 changes: 41 additions & 0 deletions packages/sdk/react/examples/vercel-edge/app/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useLDServerSession } from '@launchdarkly/react-sdk/server';

import BootstrappedClient from './BootstrappedClient';

// The flag key to evaluate. Override with the LAUNCHDARKLY_FLAG_KEY environment variable.
const flagKey = process.env.LAUNCHDARKLY_FLAG_KEY || 'sample-feature';

export default async function App() {
// The session was stored here by createLDServerSession() in the parent page.
const session = useLDServerSession();

if (!session) {
return (
<p className="no-session">
No LaunchDarkly session found. Ensure createLDServerSession() is called before rendering
this component.
</p>
);
}

const flagValue = await session.boolVariation(flagKey, false);
const ctx = session.getContext() as { name?: string; key: string };

console.log('[LaunchDarkly] Flag evaluation:', {

Check warning on line 24 in packages/sdk/react/examples/vercel-edge/app/App.tsx

View workflow job for this annotation

GitHub Actions / build-test-react (20)

Unexpected console statement

Check warning on line 24 in packages/sdk/react/examples/vercel-edge/app/App.tsx

View workflow job for this annotation

GitHub Actions / build-test-react (22)

Unexpected console statement
flagKey,
flagValue,
context: session.getContext(),
});

return (
<div className={`app ${flagValue ? 'app--on' : 'app--off'}`}>
<p className="flag-key">Feature flag: {flagKey}</p>
<p className="context">Context: {ctx.name ?? ctx.key}</p>
<p>
<strong>Server:</strong> feature flag evaluates to {String(flagValue)} (server-side
rendered).
</p>
<BootstrappedClient flagKey={flagKey} />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use client';

import { useBoolVariation } from '@launchdarkly/react-sdk';

/**
* Client component that evaluates a flag via the bootstrapped react clientSDK.
* The LDIsomorphicProvider evaluates all flags on the server and passes them
* to the react client SDK as bootstrap data.
*/
export default function BootstrappedClient({ flagKey }: { flagKey: string }) {
const flagValue = useBoolVariation(flagKey, false);

return (
<p>
<strong>Client:</strong> feature flag evaluates to {String(flagValue)} (bootstrapped).
</p>
);
}
13 changes: 13 additions & 0 deletions packages/sdk/react/examples/vercel-edge/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import './styles.css';

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
81 changes: 81 additions & 0 deletions packages/sdk/react/examples/vercel-edge/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { createClient } from '@vercel/edge-config';

import { createLDServerSession, LDIsomorphicProvider } from '@launchdarkly/react-sdk/server';
import { init } from '@launchdarkly/vercel-server-sdk';

import App from './App';

// The Vercel SDK reads flag data from Vercel Edge Config instead of connecting
// to LaunchDarkly servers directly, so it uses the client-side ID — not the
// server SDK key.
const clientSideId = process.env.LD_CLIENT_SIDE_ID || '';
const edgeConfig = process.env.VERCEL_EDGE_CONFIG;
const edgeConfigClient = edgeConfig ? createClient(edgeConfig) : null;
const ldBaseClient = clientSideId && edgeConfigClient ? init(clientSideId, edgeConfigClient) : null;

// Select via ?context=sandy|jamie|alex (defaults to sandy).
const PRESET_CONTEXTS = {
sandy: { kind: 'user' as const, key: 'example-user-key', name: 'Sandy' },
jamie: { kind: 'user' as const, key: 'user-jamie', name: 'Jamie' },
alex: { kind: 'user' as const, key: 'user-alex', name: 'Alex' },
};

export default async function Home({
searchParams,
}: {
searchParams: Promise<{ context?: string }>;
}) {
if (!edgeConfigClient) {
return (
<div className="error">
<p>
Vercel Edge Config is required: set the VERCEL_EDGE_CONFIG environment variable and try
again.
</p>
</div>
);
}

if (!ldBaseClient) {
return (
<div className="error">
<p>
LaunchDarkly client-side ID is required: set the LD_CLIENT_SIDE_ID environment variable
and try again.
</p>
</div>
);
}

try {
await ldBaseClient.waitForInitialization();
} catch {
return (
<div className="error">
<p>
SDK failed to initialize. Please check your Edge Config connection and LaunchDarkly
client-side ID for any issues.
</p>
</div>
);
}

// Resolve the evaluation context from the ?context= query parameter.
// In a real app this would come from authentication tokens, cookies, or session data.
const { context: contextKey = 'sandy' } = await searchParams;
const context =
PRESET_CONTEXTS[contextKey as keyof typeof PRESET_CONTEXTS] ?? PRESET_CONTEXTS.sandy;

// Create a per-request session bound to this user's context.
// createLDServerSession also stores the session in React's cache() so any Server Component
// in this render tree can retrieve it via useLDServerSession().
const session = createLDServerSession(ldBaseClient, context);

// Wrap the app with LDIsomorphicProvider to bootstrap the browser SDK with
// server-evaluated flag values.
return (
<LDIsomorphicProvider session={session} clientSideId={clientSideId}>
<App />
</LDIsomorphicProvider>
);
}
50 changes: 50 additions & 0 deletions packages/sdk/react/examples/vercel-edge/app/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}

/* ── Error states (page.tsx) ───────────────────────── */
.error {
min-height: 100vh;
background-color: #373841;
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}

/* ── App ──────────────────────────────────────────── */
.no-session {
color: #ff6b6b;
font-family: monospace;
}

.app {
min-height: 100vh;
color: #ffffff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: calc(10px + 2vmin);
padding: 2rem;
gap: 1rem;
}

.app--on {
background-color: #00844b;
}
.app--off {
background-color: #373841;
}

.context,
.flag-key {
font-size: 0.7em;
opacity: 0.75;
}
9 changes: 9 additions & 0 deletions packages/sdk/react/examples/vercel-edge/e2e/verify.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { expect, test } from '@playwright/test';

test('feature flag evaluates to true', async ({ page }) => {
await page.goto('/');

await expect(page.getByText('feature flag evaluates to true', { exact: false })).toHaveCount(2, {
timeout: 10000,
});
});
14 changes: 14 additions & 0 deletions packages/sdk/react/examples/vercel-edge/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { NextConfig } from 'next';
import path from 'path';

const nextConfig: NextConfig = {
// We suppress strict mode for this example to make the render log only one
// evaluation. While it is correct to double evaluate with strict mode on, that
// behavior is not immediately obvious to some users.
reactStrictMode: false,
turbopack: {
root: path.resolve(__dirname, '../../../../..'),
},
};

export default nextConfig;
Loading
Loading