Skip to content

Commit c043a28

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

15 files changed

Lines changed: 553 additions & 3 deletions

File tree

.github/workflows/react.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,18 @@ 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+
steps:
102+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
103+
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
104+
with:
105+
node-version: 20
106+
- name: Install dependencies
107+
run: yarn workspaces focus @internal/react-sdk-example-server-only
108+
- name: Build SDK dependencies
109+
run: yarn workspaces foreach -pR --topological-dev --from '@internal/react-sdk-example-server-only' run build
110+
- name: Run tests
111+
run: yarn workspace @internal/react-sdk-example-server-only test
112+
99113
# TODO: Add contract tests

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: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
16+
# next.js
17+
/.next/
18+
/out/
19+
/.swc/
20+
21+
# production
22+
/build
23+
24+
# misc
25+
.DS_Store
26+
*.pem
27+
28+
# debug
29+
npm-debug.log*
30+
yarn-debug.log*
31+
yarn-error.log*
32+
.pnpm-debug.log*
33+
34+
# env files (can opt-in for committing if needed)
35+
.env*
36+
37+
# vercel
38+
.vercel
39+
40+
# typescript
41+
*.tsbuildinfo
42+
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: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
import { act, render, screen } from '@testing-library/react';
5+
import React from 'react';
6+
7+
const originalEnv = process.env;
8+
9+
describe('Home page', () => {
10+
beforeEach(() => {
11+
jest.resetModules();
12+
process.env = { ...originalEnv };
13+
});
14+
15+
afterAll(() => {
16+
process.env = originalEnv;
17+
});
18+
19+
it('displays flag evaluates to true with green background when flag is on', async () => {
20+
process.env.LAUNCHDARKLY_SDK_KEY = 'test-sdk-key';
21+
process.env.LAUNCHDARKLY_FLAG_KEY = 'sample-feature';
22+
23+
const mockSession = {
24+
boolVariation: jest.fn().mockResolvedValue(true),
25+
getContext: jest.fn().mockReturnValue({ key: 'example-user-key', name: 'Sandy' }),
26+
};
27+
28+
jest.doMock('@launchdarkly/react-sdk/server', () => ({
29+
createLDServerSession: jest.fn().mockReturnValue(mockSession),
30+
useLDServerSession: jest.fn().mockReturnValue(mockSession),
31+
}));
32+
33+
// Pre-resolve the async server component before rendering in jsdom.
34+
const { default: FeatureSection } = await import('../app/FeatureSection');
35+
const element = await FeatureSection();
36+
37+
await act(async () => {
38+
render(element as React.ReactElement);
39+
});
40+
41+
expect(
42+
screen.getByText('The sample-feature feature flag evaluates to true.'),
43+
).toBeInTheDocument();
44+
const container = screen
45+
.getByText('The sample-feature feature flag evaluates to true.')
46+
.closest('div');
47+
expect(container).toHaveClass('app--on');
48+
});
49+
50+
it('displays flag evaluates to false with dark background when flag is off', async () => {
51+
process.env.LAUNCHDARKLY_SDK_KEY = 'test-sdk-key';
52+
process.env.LAUNCHDARKLY_FLAG_KEY = 'sample-feature';
53+
54+
const mockSession = {
55+
boolVariation: jest.fn().mockResolvedValue(false),
56+
getContext: jest.fn().mockReturnValue({ key: 'example-user-key', name: 'Sandy' }),
57+
};
58+
59+
jest.doMock('@launchdarkly/react-sdk/server', () => ({
60+
createLDServerSession: jest.fn().mockReturnValue(mockSession),
61+
useLDServerSession: jest.fn().mockReturnValue(mockSession),
62+
}));
63+
64+
// Pre-resolve the async server component before rendering in jsdom.
65+
const { default: FeatureSection } = await import('../app/FeatureSection');
66+
const element = await FeatureSection();
67+
68+
await act(async () => {
69+
render(element as React.ReactElement);
70+
});
71+
72+
expect(
73+
screen.getByText('The sample-feature feature flag evaluates to false.'),
74+
).toBeInTheDocument();
75+
const container = screen
76+
.getByText('The sample-feature feature flag evaluates to false.')
77+
.closest('div');
78+
expect(container).toHaveClass('app--off');
79+
});
80+
81+
it('uses LAUNCHDARKLY_FLAG_KEY env var as the flag key', async () => {
82+
process.env.LAUNCHDARKLY_SDK_KEY = 'test-sdk-key';
83+
process.env.LAUNCHDARKLY_FLAG_KEY = 'my-custom-flag';
84+
85+
const mockSession = {
86+
boolVariation: jest.fn().mockResolvedValue(true),
87+
getContext: jest.fn().mockReturnValue({ key: 'example-user-key', name: 'Sandy' }),
88+
};
89+
90+
jest.doMock('@launchdarkly/react-sdk/server', () => ({
91+
createLDServerSession: jest.fn().mockReturnValue(mockSession),
92+
useLDServerSession: jest.fn().mockReturnValue(mockSession),
93+
}));
94+
95+
// Pre-resolve the async server component before rendering in jsdom.
96+
const { default: FeatureSection } = await import('../app/FeatureSection');
97+
const element = await FeatureSection();
98+
99+
await act(async () => {
100+
render(element as React.ReactElement);
101+
});
102+
103+
expect(
104+
screen.getByText('The my-custom-flag feature flag evaluates to true.'),
105+
).toBeInTheDocument();
106+
});
107+
108+
it('displays an error message when the SDK key is missing', async () => {
109+
process.env.LAUNCHDARKLY_SDK_KEY = '';
110+
111+
jest.doMock('@launchdarkly/node-server-sdk', () => ({
112+
init: jest.fn(),
113+
}));
114+
jest.doMock('@launchdarkly/react-sdk/server', () => ({
115+
createLDServerSession: jest.fn(),
116+
}));
117+
118+
const { default: Home } = await import('../app/page');
119+
const element = await Home({ searchParams: Promise.resolve({}) });
120+
121+
await act(async () => {
122+
render(element as React.ReactElement);
123+
});
124+
125+
expect(screen.getByText(/LAUNCHDARKLY_SDK_KEY environment variable/)).toBeInTheDocument();
126+
});
127+
128+
it('displays an error message when the SDK fails to initialize', async () => {
129+
process.env.LAUNCHDARKLY_SDK_KEY = 'test-sdk-key';
130+
131+
const mockClient = {
132+
waitForInitialization: jest.fn().mockRejectedValue(new Error('timeout')),
133+
initialized: jest.fn().mockReturnValue(false),
134+
};
135+
136+
jest.doMock('@launchdarkly/node-server-sdk', () => ({
137+
init: jest.fn().mockReturnValue(mockClient),
138+
}));
139+
jest.doMock('@launchdarkly/react-sdk/server', () => ({
140+
createLDServerSession: jest.fn(),
141+
}));
142+
143+
const { default: Home } = await import('../app/page');
144+
const element = await Home({ searchParams: Promise.resolve({}) });
145+
146+
await act(async () => {
147+
render(element as React.ReactElement);
148+
});
149+
150+
expect(screen.getByText(/SDK failed to initialize/)).toBeInTheDocument();
151+
});
152+
});
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+
}

0 commit comments

Comments
 (0)