@variantlab/react 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +236 -70
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -13,50 +13,92 @@ npm install @variantlab/core@alpha @variantlab/react@alpha
|
|
|
13
13
|
|
|
14
14
|
**Peer dependencies:** `react ^18.2.0 || ^19.0.0`
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Complete example
|
|
19
|
+
|
|
20
|
+
### `experiments.json`
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"version": 1,
|
|
25
|
+
"experiments": [
|
|
26
|
+
{
|
|
27
|
+
"id": "hero-layout",
|
|
28
|
+
"name": "Hero section layout",
|
|
29
|
+
"type": "render",
|
|
30
|
+
"default": "centered",
|
|
31
|
+
"variants": [
|
|
32
|
+
{ "id": "centered" },
|
|
33
|
+
{ "id": "split" }
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "cta-copy",
|
|
38
|
+
"name": "CTA button text",
|
|
39
|
+
"type": "value",
|
|
40
|
+
"default": "buy-now",
|
|
41
|
+
"variants": [
|
|
42
|
+
{ "id": "buy-now", "value": "Buy now" },
|
|
43
|
+
{ "id": "get-started", "value": "Get started" },
|
|
44
|
+
{ "id": "try-free", "value": "Try it free" }
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "pricing",
|
|
49
|
+
"name": "Pricing tier",
|
|
50
|
+
"type": "value",
|
|
51
|
+
"default": "low",
|
|
52
|
+
"assignment": { "strategy": "sticky-hash" },
|
|
53
|
+
"variants": [
|
|
54
|
+
{ "id": "low", "value": 9.99, "weight": 50 },
|
|
55
|
+
{ "id": "high", "value": 14.99, "weight": 50 }
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
```
|
|
17
61
|
|
|
18
|
-
###
|
|
62
|
+
### `App.tsx`
|
|
19
63
|
|
|
20
64
|
```tsx
|
|
21
65
|
import { createEngine } from "@variantlab/core";
|
|
22
66
|
import { VariantLabProvider } from "@variantlab/react";
|
|
23
67
|
import experiments from "./experiments.json";
|
|
24
68
|
|
|
25
|
-
const engine = createEngine(experiments
|
|
69
|
+
const engine = createEngine(experiments, {
|
|
70
|
+
context: {
|
|
71
|
+
userId: "user-123",
|
|
72
|
+
platform: "web",
|
|
73
|
+
locale: "en",
|
|
74
|
+
},
|
|
75
|
+
});
|
|
26
76
|
|
|
27
77
|
export default function App() {
|
|
28
78
|
return (
|
|
29
79
|
<VariantLabProvider engine={engine}>
|
|
30
|
-
<
|
|
80
|
+
<HomePage />
|
|
31
81
|
</VariantLabProvider>
|
|
32
82
|
);
|
|
33
83
|
}
|
|
34
84
|
```
|
|
35
85
|
|
|
36
|
-
###
|
|
86
|
+
### `HomePage.tsx`
|
|
37
87
|
|
|
38
88
|
```tsx
|
|
39
|
-
import { useVariant, useVariantValue } from "@variantlab/react";
|
|
40
|
-
|
|
41
|
-
function HeroSection() {
|
|
42
|
-
// Get the assigned variant ID
|
|
43
|
-
const variant = useVariant("hero-layout"); // "centered" | "split"
|
|
44
|
-
|
|
45
|
-
return variant === "split" ? <SplitHero /> : <CenteredHero />;
|
|
46
|
-
}
|
|
89
|
+
import { useVariant, useVariantValue, Variant, VariantErrorBoundary } from "@variantlab/react";
|
|
47
90
|
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
91
|
+
function HomePage() {
|
|
92
|
+
return (
|
|
93
|
+
<main>
|
|
94
|
+
<VariantErrorBoundary experimentId="hero-layout" fallback={<p>Error loading hero</p>}>
|
|
95
|
+
<HeroSection />
|
|
96
|
+
</VariantErrorBoundary>
|
|
97
|
+
<CheckoutButton />
|
|
98
|
+
<PricingDisplay />
|
|
99
|
+
</main>
|
|
100
|
+
);
|
|
53
101
|
}
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
### 3. Or use components
|
|
57
|
-
|
|
58
|
-
```tsx
|
|
59
|
-
import { Variant, VariantValue } from "@variantlab/react";
|
|
60
102
|
|
|
61
103
|
function HeroSection() {
|
|
62
104
|
return (
|
|
@@ -70,105 +112,229 @@ function HeroSection() {
|
|
|
70
112
|
}
|
|
71
113
|
|
|
72
114
|
function CheckoutButton() {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
115
|
+
const copy = useVariantValue<string>("cta-copy");
|
|
116
|
+
return <button>{copy}</button>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function PricingDisplay() {
|
|
120
|
+
const price = useVariantValue<number>("pricing");
|
|
121
|
+
return <span>${price}/month</span>;
|
|
78
122
|
}
|
|
79
123
|
```
|
|
80
124
|
|
|
81
|
-
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Hooks
|
|
82
128
|
|
|
83
|
-
### `useVariant(experimentId)`
|
|
129
|
+
### `useVariant(experimentId)` — get the active variant ID
|
|
84
130
|
|
|
85
|
-
|
|
131
|
+
Use this for **render experiments** where you switch between different components or layouts.
|
|
86
132
|
|
|
87
133
|
```tsx
|
|
88
|
-
|
|
134
|
+
import { useVariant } from "@variantlab/react";
|
|
135
|
+
|
|
136
|
+
function HeroSection() {
|
|
137
|
+
const layout = useVariant("hero-layout");
|
|
138
|
+
// Returns: "centered" | "split"
|
|
139
|
+
|
|
140
|
+
if (layout === "split") {
|
|
141
|
+
return <SplitHero />;
|
|
142
|
+
}
|
|
143
|
+
return <CenteredHero />;
|
|
144
|
+
}
|
|
89
145
|
```
|
|
90
146
|
|
|
91
|
-
### `useVariantValue<T>(experimentId)`
|
|
147
|
+
### `useVariantValue<T>(experimentId)` — get the experiment value
|
|
92
148
|
|
|
93
|
-
|
|
149
|
+
Use this for **value experiments** where variants carry data (strings, numbers, booleans, objects).
|
|
94
150
|
|
|
95
151
|
```tsx
|
|
96
|
-
|
|
97
|
-
|
|
152
|
+
import { useVariantValue } from "@variantlab/react";
|
|
153
|
+
|
|
154
|
+
function CheckoutButton() {
|
|
155
|
+
const buttonText = useVariantValue<string>("cta-copy");
|
|
156
|
+
// Returns: "Buy now" | "Get started" | "Try it free"
|
|
157
|
+
return <button>{buttonText}</button>;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function PricingDisplay() {
|
|
161
|
+
const price = useVariantValue<number>("pricing");
|
|
162
|
+
// Returns: 9.99 | 14.99
|
|
163
|
+
return <span>${price}/month</span>;
|
|
164
|
+
}
|
|
98
165
|
```
|
|
99
166
|
|
|
100
|
-
### `useExperiment(experimentId)`
|
|
167
|
+
### `useExperiment(experimentId)` — get full experiment state
|
|
101
168
|
|
|
102
|
-
Returns the
|
|
169
|
+
Returns the variant ID, experiment config, and whether it's been manually overridden. Useful for debug UIs or analytics.
|
|
103
170
|
|
|
104
171
|
```tsx
|
|
105
|
-
|
|
172
|
+
import { useExperiment } from "@variantlab/react";
|
|
173
|
+
|
|
174
|
+
function ExperimentInfo() {
|
|
175
|
+
const { variantId, experiment, isOverridden } = useExperiment("hero-layout");
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div>
|
|
179
|
+
<p>Experiment: {experiment.name}</p>
|
|
180
|
+
<p>Current variant: {variantId}</p>
|
|
181
|
+
{isOverridden && <p style={{ color: "orange" }}>⚠ Manually overridden</p>}
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
106
185
|
```
|
|
107
186
|
|
|
108
|
-
### `useSetVariant()`
|
|
187
|
+
### `useSetVariant()` — override a variant
|
|
109
188
|
|
|
110
|
-
Returns a function to
|
|
189
|
+
Returns a function to force-assign a variant. Useful for building debug UIs, admin panels, or testing during development.
|
|
111
190
|
|
|
112
191
|
```tsx
|
|
113
|
-
|
|
114
|
-
|
|
192
|
+
import { useSetVariant, useVariant } from "@variantlab/react";
|
|
193
|
+
|
|
194
|
+
function VariantPicker() {
|
|
195
|
+
const setVariant = useSetVariant();
|
|
196
|
+
const current = useVariant("hero-layout");
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<div>
|
|
200
|
+
<p>Current: {current}</p>
|
|
201
|
+
<button onClick={() => setVariant("hero-layout", "centered")}>Centered</button>
|
|
202
|
+
<button onClick={() => setVariant("hero-layout", "split")}>Split</button>
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
115
206
|
```
|
|
116
207
|
|
|
117
|
-
### `useVariantLabEngine()`
|
|
208
|
+
### `useVariantLabEngine()` — access the engine directly
|
|
118
209
|
|
|
119
|
-
Returns the engine instance
|
|
210
|
+
Returns the raw engine instance for advanced operations like resetting all overrides, updating context, or subscribing to changes.
|
|
120
211
|
|
|
121
212
|
```tsx
|
|
122
|
-
|
|
123
|
-
|
|
213
|
+
import { useVariantLabEngine } from "@variantlab/react";
|
|
214
|
+
|
|
215
|
+
function SettingsPanel() {
|
|
216
|
+
const engine = useVariantLabEngine();
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<div>
|
|
220
|
+
<button onClick={() => engine.resetAll()}>Reset all experiments</button>
|
|
221
|
+
<button onClick={() => engine.updateContext({ locale: "bn" })}>
|
|
222
|
+
Switch to Bengali
|
|
223
|
+
</button>
|
|
224
|
+
</div>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
124
227
|
```
|
|
125
228
|
|
|
126
|
-
### `useRouteExperiments()`
|
|
229
|
+
### `useRouteExperiments()` — get experiments targeting the current route
|
|
127
230
|
|
|
128
|
-
Returns experiments
|
|
231
|
+
Returns only experiments whose targeting rules match the current URL path. Useful for showing relevant experiments in a debug panel.
|
|
129
232
|
|
|
130
233
|
```tsx
|
|
131
|
-
|
|
234
|
+
import { useRouteExperiments } from "@variantlab/react";
|
|
235
|
+
|
|
236
|
+
function RouteDebugPanel() {
|
|
237
|
+
const experiments = useRouteExperiments();
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<ul>
|
|
241
|
+
{experiments.map((exp) => (
|
|
242
|
+
<li key={exp.id}>{exp.name}: {exp.variantId}</li>
|
|
243
|
+
))}
|
|
244
|
+
</ul>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
132
247
|
```
|
|
133
248
|
|
|
249
|
+
---
|
|
250
|
+
|
|
134
251
|
## Components
|
|
135
252
|
|
|
136
|
-
### `<Variant>`
|
|
253
|
+
### `<Variant>` — render-swap by variant ID
|
|
254
|
+
|
|
255
|
+
Renders the child matching the active variant. Cleaner than if/switch when you have distinct JSX per variant.
|
|
256
|
+
|
|
257
|
+
```tsx
|
|
258
|
+
import { Variant } from "@variantlab/react";
|
|
259
|
+
|
|
260
|
+
function OnboardingPage() {
|
|
261
|
+
return (
|
|
262
|
+
<Variant experimentId="onboarding-flow" fallback={<ClassicOnboarding />}>
|
|
263
|
+
{{
|
|
264
|
+
classic: <ClassicOnboarding />,
|
|
265
|
+
"quick-start": <QuickStartOnboarding />,
|
|
266
|
+
guided: <GuidedOnboarding />,
|
|
267
|
+
}}
|
|
268
|
+
</Variant>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### `<VariantValue>` — render-prop for value experiments
|
|
137
274
|
|
|
138
|
-
|
|
275
|
+
Passes the experiment value to a render function. Useful when you want to keep the value inline.
|
|
139
276
|
|
|
140
277
|
```tsx
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
</
|
|
278
|
+
import { VariantValue } from "@variantlab/react";
|
|
279
|
+
|
|
280
|
+
function WelcomeBanner() {
|
|
281
|
+
return (
|
|
282
|
+
<VariantValue experimentId="cta-copy">
|
|
283
|
+
{(value) => <h2>{value}</h2>}
|
|
284
|
+
</VariantValue>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
147
287
|
```
|
|
148
288
|
|
|
149
|
-
### `<
|
|
289
|
+
### `<VariantErrorBoundary>` — crash-safe experiments
|
|
150
290
|
|
|
151
|
-
|
|
291
|
+
Wraps an experiment in an error boundary. If a variant crashes N times within a time window, the engine automatically rolls back to the default variant.
|
|
152
292
|
|
|
153
293
|
```tsx
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
294
|
+
import { VariantErrorBoundary } from "@variantlab/react";
|
|
295
|
+
|
|
296
|
+
function SafeHeroSection() {
|
|
297
|
+
return (
|
|
298
|
+
<VariantErrorBoundary
|
|
299
|
+
experimentId="hero-layout"
|
|
300
|
+
fallback={<p>Something went wrong. Showing default layout.</p>}
|
|
301
|
+
>
|
|
302
|
+
<HeroSection />
|
|
303
|
+
</VariantErrorBoundary>
|
|
304
|
+
);
|
|
305
|
+
}
|
|
157
306
|
```
|
|
158
307
|
|
|
159
|
-
### `<
|
|
308
|
+
### `<VariantLabProvider>` — context provider
|
|
160
309
|
|
|
161
|
-
|
|
310
|
+
Wraps your app and provides the engine to all hooks and components. Must be near the top of your component tree.
|
|
162
311
|
|
|
163
312
|
```tsx
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
313
|
+
import { VariantLabProvider } from "@variantlab/react";
|
|
314
|
+
|
|
315
|
+
export default function App() {
|
|
316
|
+
return (
|
|
317
|
+
<VariantLabProvider engine={engine}>
|
|
318
|
+
{/* All useVariant/useVariantValue/etc. hooks work inside here */}
|
|
319
|
+
<Router />
|
|
320
|
+
</VariantLabProvider>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
167
323
|
```
|
|
168
324
|
|
|
325
|
+
---
|
|
326
|
+
|
|
169
327
|
## Type safety with codegen
|
|
170
328
|
|
|
171
|
-
|
|
329
|
+
Generate TypeScript types from your config so typos become compile errors:
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
npx @variantlab/cli@alpha generate
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
After running, `useVariant("hero-layout")` returns `"centered" | "split"` as a literal union type. Passing a non-existent experiment ID like `useVariant("typo")` is a compile error.
|
|
336
|
+
|
|
337
|
+
---
|
|
172
338
|
|
|
173
339
|
## License
|
|
174
340
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@variantlab/react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "React hooks and components for variantlab.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -40,18 +40,18 @@
|
|
|
40
40
|
],
|
|
41
41
|
"repository": {
|
|
42
42
|
"type": "git",
|
|
43
|
-
"url": "git+https://github.com/
|
|
43
|
+
"url": "git+https://github.com/Minhaj-Rabby/variantlab.git",
|
|
44
44
|
"directory": "packages/react"
|
|
45
45
|
},
|
|
46
46
|
"bugs": {
|
|
47
|
-
"url": "https://github.com/
|
|
47
|
+
"url": "https://github.com/Minhaj-Rabby/variantlab/issues"
|
|
48
48
|
},
|
|
49
|
-
"homepage": "https://github.com/
|
|
49
|
+
"homepage": "https://github.com/Minhaj-Rabby/variantlab#readme",
|
|
50
50
|
"engines": {
|
|
51
51
|
"node": ">=18.17"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@variantlab/core": "0.1.
|
|
54
|
+
"@variantlab/core": "0.1.3"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"react": "^18.2.0 || ^19.0.0"
|