Skip to content

Commit fa2e43b

Browse files
authored
chore(react-sdk): adding vercel edge example (#1241)
This PR will add a vercel edge example showcasing that our Next.js integration is compatible with the edge sdk (in fact it is almost identical to the nodejs version) ### Caveat I did not implement CI for this just yet. Still pending further discussions to properly set things up. ### Alternative This example might be able to replace the `complete` example we made for `vercel` sdk. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Mostly adds a new example app and wiring in workspace/release config; low blast radius outside docs/tooling. Risk is limited to potential dependency/version drift in the new example and release-please extra-files tracking. > > **Overview** > Adds a new `packages/sdk/react/examples/vercel-edge` Next.js App Router example demonstrating flag evaluation via `@launchdarkly/vercel-server-sdk` + Vercel Edge Config, with server-side `createLDServerSession`/`useLDServerSession` and client bootstrap through `LDIsomorphicProvider`. > > Updates the monorepo to include the example in Yarn workspaces, documents it in the React examples README, adds a basic Playwright e2e check, and extends `release-please-config.json` to keep the example’s `@launchdarkly/react-sdk` and `@launchdarkly/vercel-server-sdk` dependency pins in sync. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 26b0092. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/launchdarkly/js-core/pull/1241" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end -->
1 parent 0b32cb0 commit fa2e43b

15 files changed

Lines changed: 444 additions & 1 deletion

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"packages/sdk/react/contract-tests",
2323
"packages/sdk/react/examples/hello-react",
2424
"packages/sdk/react/examples/react-server-example",
25+
"packages/sdk/react/examples/vercel-edge",
2526
"packages/sdk/react-native",
2627
"packages/sdk/react-native/example",
2728
"packages/sdk/react-native/contract-tests/entity",

packages/sdk/react/examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ This directory contains example applications demonstrating the LaunchDarkly Reac
66
|---------|-------------|
77
| [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. |
88
| [react-server-example](./react-server-example/) | Next.js App Router example demonstrating server-side flag evaluation with React Server Components. |
9+
| [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`. |
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.*
7+
.yarn/*
8+
!.yarn/patches
9+
!.yarn/plugins
10+
!.yarn/releases
11+
!.yarn/versions
12+
13+
# testing
14+
/coverage
15+
/test-results
16+
17+
# next.js
18+
/.next/
19+
/out/
20+
/.swc/
21+
22+
# production
23+
/build
24+
25+
# misc
26+
.DS_Store
27+
*.pem
28+
29+
# debug
30+
npm-debug.log*
31+
yarn-debug.log*
32+
yarn-error.log*
33+
.pnpm-debug.log*
34+
35+
# env files (can opt-in for committing if needed)
36+
.env*
37+
38+
# vercel
39+
.vercel
40+
41+
# typescript
42+
*.tsbuildinfo
43+
next-env.d.ts
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# LaunchDarkly sample React + Vercel Edge application
2+
3+
We've built a simple web application that demonstrates how the LaunchDarkly React SDK works with
4+
the Vercel Edge SDK. The app evaluates feature flags using data stored in
5+
[Vercel Edge Config](https://vercel.com/docs/edge-config/overview) and renders the result using
6+
React Server Components.
7+
8+
The Vercel SDK reads flag data from Edge Config instead of connecting to LaunchDarkly servers
9+
directly, providing ultra-low latency flag evaluation at the edge.
10+
11+
The demo shows 2 ways to use React server-side rendering:
12+
13+
1. Using `createLDServerSession` and `useLDServerSession` to provide
14+
per-request session isolation: Nested Server Components access the session through React's `cache()`
15+
without any prop drilling.
16+
17+
2. Using the `LDIsomorphicProvider` to bootstrap the browser SDK with server-evaluated flag values.
18+
This allows the browser SDK to start immediately with real values.
19+
20+
Below, you'll find the build procedure. For more comprehensive instructions, you can visit your
21+
[Quickstart page](https://app.launchdarkly.com/quickstart#/) or the
22+
[React SDK reference guide](https://docs.launchdarkly.com/sdk/client-side/react/react-web).
23+
24+
This demo requires Node.js 18 or higher.
25+
26+
## Prerequisites
27+
28+
This example requires the [LaunchDarkly Vercel integration](https://vercel.com/integrations/launchdarkly)
29+
to be configured. The integration syncs your LaunchDarkly flag data to Vercel Edge Config so that
30+
the Vercel SDK can read it without connecting to LaunchDarkly servers.
31+
32+
## Build instructions
33+
34+
1. Set the `VERCEL_EDGE_CONFIG` environment variable to your Vercel Edge Config connection string.
35+
You can find this in your Vercel project settings under Edge Config.
36+
37+
```bash
38+
export VERCEL_EDGE_CONFIG="https://edge-config.vercel.com/ecfg_..."
39+
```
40+
41+
2. Set the `LD_CLIENT_SIDE_ID` environment variable to your LaunchDarkly client-side ID.
42+
The Vercel SDK uses this to look up flag data in Edge Config, and the same value is used
43+
to bootstrap the browser SDK.
44+
45+
```bash
46+
export LD_CLIENT_SIDE_ID="my-client-side-id"
47+
```
48+
49+
3. If there is an existing boolean feature flag in your LaunchDarkly project that you want to
50+
evaluate, set `LAUNCHDARKLY_FLAG_KEY`:
51+
52+
```bash
53+
export LAUNCHDARKLY_FLAG_KEY="my-flag-key"
54+
```
55+
56+
Otherwise, `sample-feature` will be used by default.
57+
58+
## Running
59+
60+
On the command line, run:
61+
62+
```bash
63+
yarn dev
64+
```
65+
66+
Then open [http://localhost:3000](http://localhost:3000) in your browser. You will see the
67+
spec message, current context name, and a full-page background: green when the flag is on,
68+
or grey when off.
69+
70+
To simulate a different user, append the `?context=` query parameter. Each tab gets a
71+
completely independent `LDServerSession` with its own context:
72+
73+
| URL | Context |
74+
|-----|---------|
75+
| `http://localhost:3000/` | Sandy (example-user-key) — default |
76+
| `http://localhost:3000/?context=sandy` | Sandy (example-user-key) |
77+
| `http://localhost:3000/?context=jamie` | Jamie (user-jamie) |
78+
| `http://localhost:3000/?context=alex` | Alex (user-alex) |
79+
80+
If you have targeting rules in LaunchDarkly that serve different values to different user keys,
81+
you will see different flag results for each context.
82+
83+
In a production app, the user identity would come from auth tokens, cookies, or session data
84+
instead of query parameters.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { useLDServerSession } from '@launchdarkly/react-sdk/server';
2+
3+
import BootstrappedClient from './BootstrappedClient';
4+
5+
// The flag key to evaluate. Override with the LAUNCHDARKLY_FLAG_KEY environment variable.
6+
const flagKey = process.env.LAUNCHDARKLY_FLAG_KEY || 'sample-feature';
7+
8+
export default async function App() {
9+
// The session was stored here by createLDServerSession() in the parent page.
10+
const session = useLDServerSession();
11+
12+
if (!session) {
13+
return (
14+
<p className="no-session">
15+
No LaunchDarkly session found. Ensure createLDServerSession() is called before rendering
16+
this component.
17+
</p>
18+
);
19+
}
20+
21+
const flagValue = await session.boolVariation(flagKey, false);
22+
const ctx = session.getContext() as { name?: string; key: string };
23+
24+
console.log('[LaunchDarkly] Flag evaluation:', {
25+
flagKey,
26+
flagValue,
27+
context: session.getContext(),
28+
});
29+
30+
return (
31+
<div className={`app ${flagValue ? 'app--on' : 'app--off'}`}>
32+
<p className="flag-key">Feature flag: {flagKey}</p>
33+
<p className="context">Context: {ctx.name ?? ctx.key}</p>
34+
<p>
35+
<strong>Server:</strong> feature flag evaluates to {String(flagValue)} (server-side
36+
rendered).
37+
</p>
38+
<BootstrappedClient flagKey={flagKey} />
39+
</div>
40+
);
41+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use client';
2+
3+
import { useBoolVariation } from '@launchdarkly/react-sdk';
4+
5+
/**
6+
* Client component that evaluates a flag via the bootstrapped react clientSDK.
7+
* The LDIsomorphicProvider evaluates all flags on the server and passes them
8+
* to the react client SDK as bootstrap data.
9+
*/
10+
export default function BootstrappedClient({ flagKey }: { flagKey: string }) {
11+
const flagValue = useBoolVariation(flagKey, false);
12+
13+
return (
14+
<p>
15+
<strong>Client:</strong> feature flag evaluates to {String(flagValue)} (bootstrapped).
16+
</p>
17+
);
18+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import './styles.css';
2+
3+
export default function RootLayout({
4+
children,
5+
}: Readonly<{
6+
children: React.ReactNode;
7+
}>) {
8+
return (
9+
<html lang="en">
10+
<body>{children}</body>
11+
</html>
12+
);
13+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { createClient } from '@vercel/edge-config';
2+
3+
import { createLDServerSession, LDIsomorphicProvider } from '@launchdarkly/react-sdk/server';
4+
import { init } from '@launchdarkly/vercel-server-sdk';
5+
6+
import App from './App';
7+
8+
// The Vercel SDK reads flag data from Vercel Edge Config instead of connecting
9+
// to LaunchDarkly servers directly, so it uses the client-side ID — not the
10+
// server SDK key.
11+
const clientSideId = process.env.LD_CLIENT_SIDE_ID || '';
12+
const edgeConfig = process.env.VERCEL_EDGE_CONFIG;
13+
const edgeConfigClient = edgeConfig ? createClient(edgeConfig) : null;
14+
const ldBaseClient = clientSideId && edgeConfigClient ? init(clientSideId, edgeConfigClient) : null;
15+
16+
// Select via ?context=sandy|jamie|alex (defaults to sandy).
17+
const PRESET_CONTEXTS = {
18+
sandy: { kind: 'user' as const, key: 'example-user-key', name: 'Sandy' },
19+
jamie: { kind: 'user' as const, key: 'user-jamie', name: 'Jamie' },
20+
alex: { kind: 'user' as const, key: 'user-alex', name: 'Alex' },
21+
};
22+
23+
export default async function Home({
24+
searchParams,
25+
}: {
26+
searchParams: Promise<{ context?: string }>;
27+
}) {
28+
if (!edgeConfigClient) {
29+
return (
30+
<div className="error">
31+
<p>
32+
Vercel Edge Config is required: set the VERCEL_EDGE_CONFIG environment variable and try
33+
again.
34+
</p>
35+
</div>
36+
);
37+
}
38+
39+
if (!ldBaseClient) {
40+
return (
41+
<div className="error">
42+
<p>
43+
LaunchDarkly client-side ID is required: set the LD_CLIENT_SIDE_ID environment variable
44+
and try again.
45+
</p>
46+
</div>
47+
);
48+
}
49+
50+
try {
51+
await ldBaseClient.waitForInitialization();
52+
} catch {
53+
return (
54+
<div className="error">
55+
<p>
56+
SDK failed to initialize. Please check your Edge Config connection and LaunchDarkly
57+
client-side ID for any issues.
58+
</p>
59+
</div>
60+
);
61+
}
62+
63+
// Resolve the evaluation context from the ?context= query parameter.
64+
// In a real app this would come from authentication tokens, cookies, or session data.
65+
const { context: contextKey = 'sandy' } = await searchParams;
66+
const context =
67+
PRESET_CONTEXTS[contextKey as keyof typeof PRESET_CONTEXTS] ?? PRESET_CONTEXTS.sandy;
68+
69+
// Create a per-request session bound to this user's context.
70+
// createLDServerSession also stores the session in React's cache() so any Server Component
71+
// in this render tree can retrieve it via useLDServerSession().
72+
const session = createLDServerSession(ldBaseClient, context);
73+
74+
// Wrap the app with LDIsomorphicProvider to bootstrap the browser SDK with
75+
// server-evaluated flag values.
76+
return (
77+
<LDIsomorphicProvider session={session} clientSideId={clientSideId}>
78+
<App />
79+
</LDIsomorphicProvider>
80+
);
81+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
*,
2+
*::before,
3+
*::after {
4+
margin: 0;
5+
padding: 0;
6+
box-sizing: border-box;
7+
}
8+
9+
/* ── Error states (page.tsx) ───────────────────────── */
10+
.error {
11+
min-height: 100vh;
12+
background-color: #373841;
13+
color: #ffffff;
14+
display: flex;
15+
align-items: center;
16+
justify-content: center;
17+
padding: 2rem;
18+
}
19+
20+
/* ── App ──────────────────────────────────────────── */
21+
.no-session {
22+
color: #ff6b6b;
23+
font-family: monospace;
24+
}
25+
26+
.app {
27+
min-height: 100vh;
28+
color: #ffffff;
29+
display: flex;
30+
flex-direction: column;
31+
align-items: center;
32+
justify-content: center;
33+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
34+
font-size: calc(10px + 2vmin);
35+
padding: 2rem;
36+
gap: 1rem;
37+
}
38+
39+
.app--on {
40+
background-color: #00844b;
41+
}
42+
.app--off {
43+
background-color: #373841;
44+
}
45+
46+
.context,
47+
.flag-key {
48+
font-size: 0.7em;
49+
opacity: 0.75;
50+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { expect, test } from '@playwright/test';
2+
3+
test('feature flag evaluates to true', async ({ page }) => {
4+
await page.goto('/');
5+
6+
await expect(page.getByText('feature flag evaluates to true', { exact: false })).toHaveCount(2, {
7+
timeout: 10000,
8+
});
9+
});

0 commit comments

Comments
 (0)