Skip to content

Commit 60d4b08

Browse files
authored
docs: react19 example app (#270)
* feat(examples/react19): minimal client-side React 19 Vite setup with working counter demo * feat(examples/react19): add app from react18 example * feat(examples/react19): clearer examples * feat(examples/react19): emit events
1 parent ca4edf1 commit 60d4b08

File tree

11 files changed

+1280
-3
lines changed

11 files changed

+1280
-3
lines changed

examples/react19/.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../.env

examples/react19/.eslintrc.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"env": {
3+
"browser": true,
4+
"es2020": true
5+
},
6+
"parserOptions": {
7+
"ecmaVersion": 2020,
8+
"sourceType": "module"
9+
},
10+
"rules": {
11+
"no-restricted-syntax": ["error", "WithStatement"]
12+
}
13+
}

examples/react19/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Example of using Confidence in React 19
2+
3+
This is a barebones client-side React 19 app using Vite, with no server-side rendering or extra tooling.
4+
5+
## Getting started
6+
7+
1. Install dependencies:
8+
```sh
9+
yarn install
10+
```
11+
2. Start the dev server:
12+
```sh
13+
yarn dev
14+
```
15+
16+
## Notes
17+
18+
- Uses @spotify-confidence/react and @spotify-confidence/sdk from the monorepo.
19+
- No SSR, no testing libraries, no extra tooling.

examples/react19/package.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "react19",
3+
"version": "0.1.0",
4+
"private": true,
5+
"dependencies": {
6+
"@spotify-confidence/react": "workspace:*",
7+
"@spotify-confidence/sdk": "workspace:*",
8+
"react": "^19.0.0",
9+
"react-dom": "^19.0.0"
10+
},
11+
"devDependencies": {
12+
"@vitejs/plugin-react": "^4.2.0",
13+
"vite": "^5.2.0"
14+
},
15+
"scripts": {
16+
"dev": "vite",
17+
"build": "vite build"
18+
}
19+
}

examples/react19/src/App.tsx

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
import React, { Suspense, useCallback, useEffect, useState } from 'react';
2+
import { Confidence } from '@spotify-confidence/sdk';
3+
import { ConfidenceProvider, useConfidence, useFlag, useEvaluateFlag } from '@spotify-confidence/react';
4+
5+
const state = {
6+
get failRequests(): boolean {
7+
return document.location.hash === '#fail';
8+
},
9+
set failRequests(value: boolean) {
10+
document.location.hash = value ? '#fail' : '';
11+
},
12+
};
13+
14+
const handleFailRequestsOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
15+
state.failRequests = e.target.checked;
16+
};
17+
18+
// Get the client secret from Vite env
19+
const clientSecret = import.meta.env.VITE_CLIENT_SECRET;
20+
if (!clientSecret) {
21+
throw new Error('VITE_CLIENT_SECRET not set in .env');
22+
}
23+
24+
// Create the Confidence instance
25+
const confidence = Confidence.create({
26+
clientSecret,
27+
environment: 'client',
28+
timeout: 3000,
29+
logger: console,
30+
// eslint-disable-next-line no-console
31+
fetchImplementation: (req: Request) => {
32+
// eslint-disable-next-line no-console
33+
console.log('request', req.url);
34+
return state.failRequests ? Promise.resolve(new Response(null, { status: 500 })) : fetch(req);
35+
},
36+
});
37+
38+
// ErrorBoundary component to catch and display errors
39+
class ErrorBoundary extends React.Component<{ children: React.ReactNode }, { error: Error | null }> {
40+
static getDerivedStateFromError(error: Error) {
41+
return { error };
42+
}
43+
constructor(props: { children: React.ReactNode }) {
44+
super(props);
45+
this.state = { error: null };
46+
}
47+
render() {
48+
if (this.state.error) {
49+
return (
50+
<div style={{ color: 'red', padding: 24, fontFamily: 'sans-serif' }}>
51+
<h2>Something went wrong</h2>
52+
<pre style={{ background: '#f6f8fa', padding: 8 }}>{this.state.error.message}</pre>
53+
<button onClick={() => window.location.reload()}>Reload Page</button>
54+
</div>
55+
);
56+
}
57+
return this.props.children;
58+
}
59+
}
60+
61+
/**
62+
* This app demonstrates the main features of the Confidence SDK React integration:
63+
* - useFlag: Get a flag value reactively
64+
* - useEvaluateFlag: Get a flag value with evaluation details
65+
* - ConfidenceProvider.WithContext: Provide context to a subtree
66+
* - Adding context information (targeting_key, level)
67+
* - Toggling between user-a and user-b as targeting_key
68+
*/
69+
function App() {
70+
// State for toggling between user-a and user-b
71+
const [targetingKey, setTargetingKey] = useState('user-a');
72+
// State for adding a custom context value
73+
const [customLevel, setCustomLevel] = useState('Outer');
74+
75+
// Toggle between user-a and user-b
76+
const toggleTargetingKey = useCallback(() => {
77+
setTargetingKey(prev => (prev === 'user-a' ? 'user-b' : 'user-a'));
78+
}, []);
79+
80+
return (
81+
<ErrorBoundary>
82+
<ConfidenceProvider confidence={confidence}>
83+
<Suspense fallback="Loading Confidence SDK...">
84+
<ConfidenceDemo
85+
targetingKey={targetingKey}
86+
customLevel={customLevel}
87+
setCustomLevel={setCustomLevel}
88+
toggleTargetingKey={toggleTargetingKey}
89+
/>
90+
</Suspense>
91+
</ConfidenceProvider>
92+
</ErrorBoundary>
93+
);
94+
}
95+
96+
interface ConfidenceDemoProps {
97+
targetingKey: string;
98+
customLevel: string;
99+
setCustomLevel: React.Dispatch<React.SetStateAction<string>>;
100+
toggleTargetingKey: () => void;
101+
}
102+
function ConfidenceDemo({ targetingKey, customLevel, setCustomLevel, toggleTargetingKey }: ConfidenceDemoProps) {
103+
// All Confidence hooks and context logic are now inside the provider
104+
const confidenceInstance = useConfidence();
105+
useEffect(() => {
106+
confidenceInstance.setContext({ targeting_key: targetingKey });
107+
}, [targetingKey, customLevel, confidenceInstance]);
108+
109+
return (
110+
<main style={{ fontFamily: 'sans-serif', padding: 24, maxWidth: 700, margin: '0 auto' }}>
111+
<h1>Confidence React 19 Example</h1>
112+
<p>
113+
This example demonstrates the main features of the Confidence SDK React integration. See code comments for more
114+
details.
115+
</p>
116+
<hr />
117+
<section>
118+
<h2>
119+
1. <code>useFlag</code> hook
120+
</h2>
121+
<p>
122+
<b>useFlag</b> returns the value of a feature flag and updates reactively when the flag or context changes.
123+
</p>
124+
<pre style={{ background: '#f6f8fa', padding: 8 }}>
125+
{`const flagValue = useFlag('web-sdk-e2e-flag.str', 'default');`}
126+
</pre>
127+
<React.Suspense fallback={<span>Loading flag value...</span>}>
128+
<FlagValueDisplay />
129+
</React.Suspense>
130+
</section>
131+
<section>
132+
<h2>
133+
2. <code>useEvaluateFlag</code> hook
134+
</h2>
135+
<p>
136+
<b>useEvaluateFlag</b> returns the value and evaluation details (reason, variant, etc) for a flag.
137+
</p>
138+
<pre style={{ background: '#f6f8fa', padding: 8 }}>
139+
{`const flagEval = useEvaluateFlag('web-sdk-e2e-flag.str', 'default');`}
140+
</pre>
141+
<React.Suspense fallback={<span>Loading evaluation details...</span>}>
142+
<FlagEvalDisplay />
143+
</React.Suspense>
144+
</section>
145+
<section>
146+
<h2>3. Context: targeting_key (user-a/user-b)</h2>
147+
<p>
148+
The <b>targeting_key</b> is used to simulate different users. Toggle between <b>user-a</b> and <b>user-b</b>{' '}
149+
to see how the flag value changes.
150+
</p>
151+
<div style={{ marginBottom: 12 }}>
152+
<button onClick={toggleTargetingKey}>Toggle targeting_key (current: {targetingKey})</button>
153+
</div>
154+
<div>
155+
<b>Current context:</b>
156+
<pre style={{ background: '#f6f8fa', padding: 8 }}>
157+
{JSON.stringify(confidenceInstance.getContext(), null, 2)}
158+
</pre>
159+
</div>
160+
</section>
161+
<section>
162+
<h2>
163+
4. <code>ConfidenceProvider.WithContext</code>
164+
</h2>
165+
<p>
166+
<b>WithContext</b> allows you to provide additional context to a subtree. Here, we set <code>level</code> to{' '}
167+
<b>{customLevel}</b> for the inner section only.
168+
</p>
169+
<div style={{ border: '1px solid #ddd', padding: 12, marginBottom: 12 }}>
170+
<button onClick={() => setCustomLevel((l: string) => (l === 'Outer' ? 'Inner' : 'Outer'))}>
171+
Toggle custom level (current: {customLevel})
172+
</button>
173+
<ConfidenceProvider.WithContext context={{ level: customLevel }}>
174+
<React.Suspense fallback={<span>Loading inner flags...</span>}>
175+
<InnerFlags />
176+
</React.Suspense>
177+
</ConfidenceProvider.WithContext>
178+
</div>
179+
</section>
180+
<section>
181+
<h2>5. Tracking events</h2>
182+
<p>
183+
Use <code>confidence.track(eventName, payload)</code> to emit a custom tracking event. The current context
184+
will be attached automatically.
185+
</p>
186+
<TrackEventDemo confidence={confidenceInstance} />
187+
</section>
188+
</main>
189+
);
190+
}
191+
192+
function FlagValueDisplay() {
193+
const flagValue = useFlag('web-sdk-e2e-flag.str', 'default');
194+
return (
195+
<div>
196+
Flag value: <b>{String(flagValue)}</b>
197+
</div>
198+
);
199+
}
200+
201+
function FlagEvalDisplay() {
202+
const flagEval = useEvaluateFlag('web-sdk-e2e-flag.str', 'default');
203+
return (
204+
<div>
205+
<b>Evaluation details:</b>
206+
<pre style={{ background: '#f6f8fa', padding: 8 }}>{JSON.stringify(flagEval, null, 2)}</pre>
207+
</div>
208+
);
209+
}
210+
211+
// Example of using WithContext to provide a different context to a subtree
212+
function InnerFlags() {
213+
const confidence = useConfidence();
214+
const flagValue = useFlag('web-sdk-e2e-flag.str', 'default');
215+
const flagEval = useEvaluateFlag('web-sdk-e2e-flag.str', 'default');
216+
const context = confidence.getContext();
217+
return (
218+
<div style={{ border: '1px dashed #aaa', padding: 8, marginTop: 8, display: 'flex', gap: 16 }}>
219+
<div style={{ flex: 1 }}>
220+
<div>
221+
Inner <b>useFlag</b> value: <b>{String(flagValue)}</b>
222+
</div>
223+
<div>
224+
Inner <b>useEvaluateFlag</b> details:
225+
</div>
226+
<pre style={{ background: '#f6f8fa', padding: 8 }}>{JSON.stringify(flagEval, null, 2)}</pre>
227+
</div>
228+
<div style={{ flex: 1 }}>
229+
<div>
230+
Current <b>context</b>:
231+
</div>
232+
<pre style={{ background: '#f6f8fa', padding: 8 }}>{JSON.stringify(context, null, 2)}</pre>
233+
</div>
234+
</div>
235+
);
236+
}
237+
238+
interface TrackEventDemoProps {
239+
confidence: ReturnType<typeof useConfidence>;
240+
}
241+
function TrackEventDemo({ confidence }: TrackEventDemoProps) {
242+
const [eventName, setEventName] = useState('demo_event');
243+
const [payload, setPayload] = useState('{"clicked": true}');
244+
const [lastEvent, setLastEvent] = useState<{ name: string; data: any } | null>(null);
245+
const [error, setError] = useState<string | null>(null);
246+
247+
const handleTrack = () => {
248+
let data: any = undefined;
249+
setError(null);
250+
try {
251+
data = payload ? JSON.parse(payload) : undefined;
252+
} catch (e) {
253+
setError('Payload must be valid JSON');
254+
return;
255+
}
256+
confidence.track(eventName, data);
257+
setLastEvent({ name: eventName, data });
258+
};
259+
260+
return (
261+
<div style={{ border: '1px solid #ddd', padding: 12, marginTop: 12 }}>
262+
<div style={{ display: 'flex', gap: 8, alignItems: 'center', marginBottom: 8 }}>
263+
<label>
264+
Event name:{' '}
265+
<input type="text" value={eventName} onChange={e => setEventName(e.target.value)} style={{ width: 120 }} />
266+
</label>
267+
<label>
268+
Payload (JSON):{' '}
269+
<input type="text" value={payload} onChange={e => setPayload(e.target.value)} style={{ width: 220 }} />
270+
</label>
271+
<button onClick={handleTrack}>Track event</button>
272+
</div>
273+
{error && <div style={{ color: 'red', marginBottom: 8 }}>{error}</div>}
274+
{lastEvent && (
275+
<div style={{ fontSize: 14 }}>
276+
<b>Last event tracked:</b>
277+
<pre style={{ background: '#f6f8fa', padding: 8 }}>{JSON.stringify(lastEvent, null, 2)}</pre>
278+
</div>
279+
)}
280+
</div>
281+
);
282+
}
283+
284+
export default App;

examples/react19/src/index.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1" />
6+
<meta name="theme-color" content="#000000" />
7+
<meta name="description" content="Web site created using React 19 and Vite" />
8+
<title>Confidence React 19 Example</title>
9+
</head>
10+
<body>
11+
<noscript>You need to enable JavaScript to run this app.</noscript>
12+
<div id="root"></div>
13+
<script type="module" src="/index.tsx"></script>
14+
</body>
15+
</html>

examples/react19/src/index.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import React from 'react';
2+
import { createRoot } from 'react-dom/client';
3+
import App from './App';
4+
5+
const root = createRoot(document.getElementById('root') as HTMLElement);
6+
7+
root.render(
8+
<React.StrictMode>
9+
<App />
10+
</React.StrictMode>,
11+
);

examples/react19/src/vite-env.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/// <reference types="vite/client" />

examples/react19/tsconfig.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2017",
4+
"lib": ["dom", "dom.iterable", "esnext"],
5+
"allowJs": true,
6+
"skipLibCheck": true,
7+
"esModuleInterop": true,
8+
"allowSyntheticDefaultImports": true,
9+
"strict": true,
10+
"forceConsistentCasingInFileNames": true,
11+
"noFallthroughCasesInSwitch": true,
12+
"module": "esnext",
13+
"moduleResolution": "bundler",
14+
"resolveJsonModule": true,
15+
"isolatedModules": true,
16+
"noEmit": true,
17+
"jsx": "react-jsx"
18+
},
19+
"include": ["src"],
20+
"eslintConfig": {
21+
"rules": {
22+
"no-console": "off"
23+
}
24+
}
25+
}

0 commit comments

Comments
 (0)