Skip to content

Commit a638247

Browse files
committed
chore: adding SSR example and example test
1 parent 5defaa7 commit a638247

15 files changed

Lines changed: 421 additions & 3 deletions

File tree

.github/workflows/react.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,19 @@ jobs:
9696
aws_assume_role: ${{ vars.AWS_ROLE_ARN }}
9797
before_test: 'yarn workspace @internal/react-sdk-example-client-only playwright install --with-deps chromium'
9898

99+
run-server-only-example:
100+
runs-on: ubuntu-latest
101+
permissions:
102+
id-token: write
103+
steps:
104+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
105+
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
106+
with:
107+
node-version: 20
108+
- uses: ./actions/run-example
109+
with:
110+
workspace_name: '@internal/react-sdk-example-server-only'
111+
aws_assume_role: ${{ vars.AWS_ROLE_ARN }}
112+
before_test: 'yarn workspace @internal/react-sdk-example-server-only playwright install --with-deps chromium'
113+
99114
# TODO: Add contract tests

actions/run-example/action.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ runs:
2424
aws_assume_role: ${{ inputs.aws_assume_role }}
2525
ssm_parameter_pairs: '/sdk/common/hello-apps/client-key = LAUNCHDARKLY_CLIENT_SIDE_ID'
2626

27+
- uses: launchdarkly/gh-actions/actions/release-secrets@1a3dc56945c8e87bc952119b055f9481b4d642b0
28+
name: 'Get the server-side SDK key'
29+
with:
30+
aws_assume_role: ${{ inputs.aws_assume_role }}
31+
ssm_parameter_pairs: '/sdk/common/hello-apps/server-key = LAUNCHDARKLY_SDK_KEY'
32+
2733
- uses: launchdarkly/gh-actions/actions/release-secrets@1a3dc56945c8e87bc952119b055f9481b4d642b0
2834
name: 'Get the test feature flag key'
2935
with:

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"packages/sdk/react",
2222
"packages/sdk/react/contract-tests",
2323
"packages/sdk/react/examples/client-only",
24+
"packages/sdk/react/examples/server-only",
2425
"packages/sdk/react-native",
2526
"packages/sdk/react-native/example",
2627
"packages/sdk/react-native/contract-tests/adapter",

packages/sdk/react/examples/.gitignore

Lines changed: 0 additions & 3 deletions
This file was deleted.
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: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# LaunchDarkly sample React server-side application
2+
3+
We've built a simple web application that demonstrates how the LaunchDarkly React SDK works with
4+
React Server Components (RSC). The app evaluates a feature flag on the server and renders the
5+
result — no client-side JavaScript required.
6+
7+
The demo also shows how `createLDServerSession` and `useLDServerSession` work together to provide
8+
per-request session isolation: every HTTP request creates its own `LDServerSession` bound to
9+
that request's user context. Nested Server Components access the session through React's `cache()`
10+
without any prop drilling.
11+
12+
Below, you'll find the build procedure. For more comprehensive instructions, you can visit your
13+
[Quickstart page](https://app.launchdarkly.com/quickstart#/) or the
14+
[React SDK reference guide](https://docs.launchdarkly.com/sdk/client-side/react/react-web).
15+
16+
This demo requires Node.js 18 or higher.
17+
18+
## How it works
19+
20+
| Module | Role |
21+
|--------|------|
22+
| `ldBaseClient` (module-level) | A singleton Node SDK client, initialized once per process. Shared across all requests. |
23+
| `createLDServerSession(ldBaseClient, context)` | Called once per request in `app/page.tsx`. Binds the request context to the client and stores the session in React's `cache()`. |
24+
| `useLDServerSession()` (in `FeatureSection.tsx`) | Retrieves the session from React's per-request cache. No props needed — React isolates each request automatically. |
25+
26+
To observe per-request isolation, open browser tabs with different `context` query parameters.
27+
Each tab gets a completely independent `LDServerSession` with its own context:
28+
29+
```
30+
http://localhost:3000/?context=sandy
31+
http://localhost:3000/?context=jamie
32+
http://localhost:3000/?context=alex
33+
```
34+
35+
In a production app, the user identity would come from auth tokens, cookies, or session data
36+
instead of query parameters.
37+
38+
## Build instructions
39+
40+
1. Set the value of the `LAUNCHDARKLY_SDK_KEY` environment variable to your LaunchDarkly SDK key.
41+
42+
```bash
43+
export LAUNCHDARKLY_SDK_KEY="my-sdk-key"
44+
```
45+
46+
2. If there is an existing boolean feature flag in your LaunchDarkly project that you want to
47+
evaluate, set `LAUNCHDARKLY_FLAG_KEY`:
48+
49+
```bash
50+
export LAUNCHDARKLY_FLAG_KEY="my-flag-key"
51+
```
52+
53+
Otherwise, `sample-feature` will be used by default.
54+
55+
3. On the command line, run:
56+
57+
```bash
58+
yarn dev
59+
```
60+
61+
Then open [http://localhost:3000](http://localhost:3000) in your browser. You will see the
62+
spec message, current context name, and a full-page background: green when the
63+
flag is on, or grey when off.
64+
65+
4. To simulate a different user, append the `?context=` query parameter:
66+
67+
| URL | Context |
68+
|-----|---------|
69+
| `http://localhost:3000/` | Sandy (example-user-key) — default |
70+
| `http://localhost:3000/?context=sandy` | Sandy (example-user-key) |
71+
| `http://localhost:3000/?context=jamie` | Jamie (user-jamie) |
72+
| `http://localhost:3000/?context=alex` | Alex (user-alex) |
73+
74+
If you have targeting rules in LaunchDarkly that serve different values to different user keys,
75+
you will see different flag results for each context.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { useLDServerSession } from '@launchdarkly/react-sdk/server';
2+
3+
// The flag key to evaluate. Override with the LAUNCHDARKLY_FLAG_KEY environment variable.
4+
const flagKey = process.env.LAUNCHDARKLY_FLAG_KEY || 'sample-feature';
5+
6+
export default async function FeatureSection() {
7+
// The session was stored here by createLDServerSession() in the parent page.
8+
const session = useLDServerSession();
9+
10+
if (!session) {
11+
return (
12+
<p className="no-session">
13+
No LaunchDarkly session found. Ensure createLDServerSession() is called before rendering
14+
this component.
15+
</p>
16+
);
17+
}
18+
19+
const flagValue = await session.boolVariation(flagKey, false);
20+
const ctx = session.getContext() as { name?: string; key: string };
21+
22+
console.log('[LaunchDarkly] Flag evaluation:', {
23+
flagKey,
24+
flagValue,
25+
context: session.getContext(),
26+
});
27+
28+
return (
29+
<div className={`app ${flagValue ? 'app--on' : 'app--off'}`}>
30+
<p>{`The ${flagKey} feature flag evaluates to ${String(flagValue)}.`}</p>
31+
<p className="context">Context: {ctx.name ?? ctx.key}</p>
32+
<div className="docs">
33+
<p>
34+
Append <code>?context=</code> to switch evaluation contexts:
35+
</p>
36+
<ul>
37+
<li>
38+
<code>?context=sandy</code> — Sandy (example-user-key) <em>default</em>
39+
</li>
40+
<li>
41+
<code>?context=jamie</code> — Jamie (user-jamie)
42+
</li>
43+
<li>
44+
<code>?context=alex</code> — Alex (user-alex)
45+
</li>
46+
</ul>
47+
</div>
48+
</div>
49+
);
50+
}
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: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { init } from '@launchdarkly/node-server-sdk';
2+
import { createLDServerSession } from '@launchdarkly/react-sdk/server';
3+
4+
import FeatureSection from './FeatureSection';
5+
6+
// The base client is a module-level singleton — initialized once for the lifetime of the
7+
// Node.js process and shared across all incoming requests.
8+
const sdkKey = process.env.LAUNCHDARKLY_SDK_KEY || '';
9+
const ldBaseClient = sdkKey ? init(sdkKey) : null;
10+
11+
// Select via ?context=sandy|jamie|alex (defaults to sandy).
12+
const PRESET_CONTEXTS = {
13+
sandy: { kind: 'user' as const, key: 'example-user-key', name: 'Sandy' },
14+
jamie: { kind: 'user' as const, key: 'user-jamie', name: 'Jamie' },
15+
alex: { kind: 'user' as const, key: 'user-alex', name: 'Alex' },
16+
};
17+
18+
export default async function Home({
19+
searchParams,
20+
}: {
21+
searchParams: Promise<{ context?: string }>;
22+
}) {
23+
if (!ldBaseClient) {
24+
return (
25+
<div className="error">
26+
<p>
27+
LaunchDarkly SDK key is required: set the LAUNCHDARKLY_SDK_KEY environment variable and
28+
try again.
29+
</p>
30+
</div>
31+
);
32+
}
33+
34+
try {
35+
await ldBaseClient.waitForInitialization({ timeout: 10 });
36+
} catch {
37+
return (
38+
<div className="error">
39+
<p>
40+
SDK failed to initialize. Please check your internet connection and SDK credential for any
41+
typo.
42+
</p>
43+
</div>
44+
);
45+
}
46+
47+
// Resolve the evaluation context from the ?context= query parameter.
48+
// In a real app this would come from authentication tokens, cookies, or session data.
49+
const { context: contextKey = 'sandy' } = await searchParams;
50+
const context =
51+
PRESET_CONTEXTS[contextKey as keyof typeof PRESET_CONTEXTS] ?? PRESET_CONTEXTS.sandy;
52+
53+
// Create a per-request session bound to this user's context.
54+
// createLDServerSession also stores the session in React's cache() so any Server Component
55+
// in this render tree can retrieve it via useLDServerSession() — no prop drilling needed.
56+
createLDServerSession(ldBaseClient, context);
57+
58+
return <FeatureSection />;
59+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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+
/* ── FeatureSection ────────────────────────────────── */
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 { background-color: #00844B; }
40+
.app--off { background-color: #373841; }
41+
42+
.context {
43+
font-size: 0.7em;
44+
opacity: 0.75;
45+
}
46+
47+
.docs {
48+
margin-top: 2rem;
49+
font-size: 0.55em;
50+
opacity: 0.6;
51+
text-align: left;
52+
}
53+
54+
.docs p { margin-bottom: 0.5em; }
55+
.docs ul { padding-left: 1.5em; line-height: 1.8; }

0 commit comments

Comments
 (0)