featuretoggle-sdk-react 1.0.3 → 1.0.4
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/INTEGRATION.md +297 -0
- package/README.md +2 -2
- package/package.json +5 -4
package/INTEGRATION.md
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# React integration patterns
|
|
2
|
+
|
|
3
|
+
Recipes for apps using `featuretoggle-sdk-react`. Full-stack setups also use [`featuretoggle-sdk-typescript`](https://www.npmjs.com/package/featuretoggle-sdk-typescript) (browser client) and [`featuretoggle-sdk-typescript/server`](https://www.npmjs.com/package/featuretoggle-sdk-typescript) (Node loaders, API routes, SSR).
|
|
4
|
+
|
|
5
|
+
No single golden path — pick what fits your runtime, framework, and freshness needs.
|
|
6
|
+
|
|
7
|
+
## Package picker
|
|
8
|
+
|
|
9
|
+
| Your runtime | Package |
|
|
10
|
+
|--------------|---------|
|
|
11
|
+
| React UI (browser) | `featuretoggle-sdk-react` + `featuretoggle-sdk-typescript` |
|
|
12
|
+
| Route loader / API route / middleware | `featuretoggle-sdk-typescript/server` |
|
|
13
|
+
| SSR + hydrated React | Server entry in loader **and** React adapter in client tree |
|
|
14
|
+
|
|
15
|
+
Both TypeScript entries ship **ESM + CJS + types**. Plain JavaScript works — TypeScript is optional.
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
// ESM
|
|
19
|
+
import { FeatureToggle } from "featuretoggle-sdk-typescript";
|
|
20
|
+
import { FeatureToggleServer } from "featuretoggle-sdk-typescript/server";
|
|
21
|
+
|
|
22
|
+
// CJS
|
|
23
|
+
const { FeatureToggle } = require("featuretoggle-sdk-typescript");
|
|
24
|
+
const { FeatureToggleServer } = require("featuretoggle-sdk-typescript/server");
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Security
|
|
30
|
+
|
|
31
|
+
### API keys in the browser are public
|
|
32
|
+
|
|
33
|
+
Keys in client bundles (`VITE_*`, inlined env) can be extracted. Use **test keys** (`ft_test_`) from `development` on **localhost** in the browser. Use **live keys** (`ft_live_`) from `staging` / `production` on trusted backends via `featuretoggle-sdk-typescript/server`.
|
|
34
|
+
|
|
35
|
+
### Read-only, not secret
|
|
36
|
+
|
|
37
|
+
Keys grant read access to all enabled flags for one environment. Revoke compromised keys in the dashboard; the core SDK clears its cache on `401`.
|
|
38
|
+
|
|
39
|
+
### Feature flags are not authorization
|
|
40
|
+
|
|
41
|
+
Client `useFeature().enabled` is for UX only — gate sensitive routes and APIs on the server (see [API route / middleware gate](#api-route--middleware-gate)).
|
|
42
|
+
|
|
43
|
+
### Sanitize flag values
|
|
44
|
+
|
|
45
|
+
Treat `value` from hooks as untrusted before rendering HTML or executing as code.
|
|
46
|
+
|
|
47
|
+
### SSR seed exposure
|
|
48
|
+
|
|
49
|
+
Patterns that pass `initialFeatures` embed flag state in HTML or loader data visible to the client — do not seed flags that must stay server-only.
|
|
50
|
+
|
|
51
|
+
### SSR localhost and test keys
|
|
52
|
+
|
|
53
|
+
Server `fetch` without `Origin` returns **403** for test keys. Use a custom `fetch` with `Origin: http://localhost:<port>`, client-only init, or `ft_live_` in a deployed environment.
|
|
54
|
+
|
|
55
|
+
### Server singleton concurrency
|
|
56
|
+
|
|
57
|
+
When sharing one `FeatureToggleServer` per process, **await `refresh()`** before reads under concurrent requests, or use a [per-request server instance](#per-request-server-instance).
|
|
58
|
+
|
|
59
|
+
### Custom fetch
|
|
60
|
+
|
|
61
|
+
The optional `fetch` option on `FeatureToggle` is for tests. Do not log `Authorization` headers in production wrappers.
|
|
62
|
+
|
|
63
|
+
Full security notes for the core SDK: [featuretoggle-sdk-typescript README](https://github.com/feature-toggle/sdk-typescript#security).
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Server patterns (loaders and APIs)
|
|
68
|
+
|
|
69
|
+
Use these in route loaders, middleware, and API handlers. Pair with the React patterns below for full-stack apps.
|
|
70
|
+
|
|
71
|
+
### Module singleton (default)
|
|
72
|
+
|
|
73
|
+
One `FeatureToggleServer` per process; refresh when you need freshness.
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { FeatureToggleServer } from "featuretoggle-sdk-typescript/server";
|
|
77
|
+
|
|
78
|
+
let ft: FeatureToggleServer | null = null;
|
|
79
|
+
|
|
80
|
+
export async function getServerFt() {
|
|
81
|
+
if (!ft) {
|
|
82
|
+
ft = new FeatureToggleServer({ apiKey: process.env.FT_API_KEY! });
|
|
83
|
+
await ft.init();
|
|
84
|
+
}
|
|
85
|
+
return ft;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// per request
|
|
89
|
+
const ft = await getServerFt();
|
|
90
|
+
await ft.refresh();
|
|
91
|
+
if (ft.isEnabled("beta")) {
|
|
92
|
+
/* branch */
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Per-request server instance
|
|
97
|
+
|
|
98
|
+
Strict isolation; one bulk fetch per request. Simplest mental model; higher origin load.
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
export async function handleRequest() {
|
|
102
|
+
const ft = new FeatureToggleServer({ apiKey: process.env.FT_API_KEY! });
|
|
103
|
+
await ft.init();
|
|
104
|
+
return ft.isEnabled("feature-x");
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### API route / middleware gate
|
|
109
|
+
|
|
110
|
+
Server entry only — no React. **This is the security boundary** for sensitive flags: gate redirects, JSON responses, and authorization checks here. Client-side hooks alone are not authorization.
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
const ft = await getServerFt();
|
|
114
|
+
await ft.refresh();
|
|
115
|
+
if (!ft.isEnabled("api-v2")) {
|
|
116
|
+
return new Response("Not found", { status: 404 });
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### TTL refresh (singleton variant)
|
|
121
|
+
|
|
122
|
+
Call `refresh()` only when cache age exceeds your TTL (track `lastRefreshAt` in app code). Fewer origin calls; slightly staler reads.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## React patterns
|
|
127
|
+
|
|
128
|
+
`featuretoggle-sdk-react` peers: `react`, `featuretoggle-sdk-typescript` only.
|
|
129
|
+
|
|
130
|
+
### Client-only SPA (simplest)
|
|
131
|
+
|
|
132
|
+
Use a non-production key in the browser (see [API keys in the browser are public](#api-keys-in-the-browser-are-public)).
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
import { FeatureToggleProvider, useFeature } from "featuretoggle-sdk-react";
|
|
136
|
+
|
|
137
|
+
function App() {
|
|
138
|
+
return (
|
|
139
|
+
<FeatureToggleProvider apiKey={import.meta.env.VITE_FT_API_KEY}>
|
|
140
|
+
<Checkout />
|
|
141
|
+
</FeatureToggleProvider>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function Checkout() {
|
|
146
|
+
const { enabled, value, loading } = useFeature("new-checkout");
|
|
147
|
+
if (loading) return null;
|
|
148
|
+
return enabled ? <NewCheckout /> : <LegacyCheckout />;
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Bring-your-own client
|
|
153
|
+
|
|
154
|
+
Pass a pre-constructed `FeatureToggle` (tests, shared instance, custom `fetch` mock).
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
import { FeatureToggle } from "featuretoggle-sdk-typescript";
|
|
158
|
+
import { FeatureToggleProvider } from "featuretoggle-sdk-react";
|
|
159
|
+
|
|
160
|
+
const ft = new FeatureToggle({ apiKey, fetch: mockFetch });
|
|
161
|
+
await ft.init();
|
|
162
|
+
|
|
163
|
+
<FeatureToggleProvider client={ft}>
|
|
164
|
+
<App />
|
|
165
|
+
</FeatureToggleProvider>;
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The provider does **not** call `close()` on unmount when `client` is supplied — you own lifecycle.
|
|
169
|
+
|
|
170
|
+
### SSR seed (no flash)
|
|
171
|
+
|
|
172
|
+
Fetch on the server with `FeatureToggleServer`, then pass bulk features into the provider. Hooks start with `loading: false` when a seed is present; the client still calls `init()` and opens the live stream.
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
// loader (server)
|
|
176
|
+
import { FeatureToggleServer } from "featuretoggle-sdk-typescript/server";
|
|
177
|
+
|
|
178
|
+
const ft = new FeatureToggleServer({ apiKey: process.env.FT_API_KEY! });
|
|
179
|
+
await ft.init();
|
|
180
|
+
await ft.refresh();
|
|
181
|
+
const features = ft.getFeatures();
|
|
182
|
+
|
|
183
|
+
// client root
|
|
184
|
+
<FeatureToggleProvider
|
|
185
|
+
apiKey={clientApiKey}
|
|
186
|
+
initialFeatures={features}
|
|
187
|
+
initialEtag={etagFromLoader}
|
|
188
|
+
>
|
|
189
|
+
<App />
|
|
190
|
+
</FeatureToggleProvider>;
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Do not seed flags that must remain server-only (see [SSR seed exposure](#ssr-seed-exposure)).
|
|
194
|
+
|
|
195
|
+
### SSR props (component-level)
|
|
196
|
+
|
|
197
|
+
Server evaluates a flag and passes a boolean or value as props. The hook takes over after client init. Prefer booleans over raw values when the value must not appear in HTML.
|
|
198
|
+
|
|
199
|
+
```tsx
|
|
200
|
+
function Banner({ initialEnabled }: { initialEnabled: boolean }) {
|
|
201
|
+
const { enabled, loading } = useFeature("banner-v2");
|
|
202
|
+
const show = loading ? initialEnabled : enabled;
|
|
203
|
+
return show ? <NewBanner /> : null;
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
No provider seed required — good for one-off SSR markup.
|
|
208
|
+
|
|
209
|
+
### Companion hook (bulk / refresh)
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
import { useFeatureToggle } from "featuretoggle-sdk-react";
|
|
213
|
+
|
|
214
|
+
function FeatureList() {
|
|
215
|
+
const { refresh, getFeatures, loading, error } = useFeatureToggle();
|
|
216
|
+
// ...
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Use when you need filtered lists or manual reload — not per-key hooks.
|
|
221
|
+
|
|
222
|
+
### Manual init
|
|
223
|
+
|
|
224
|
+
Defer `init()` until your app is ready (e.g. after auth, route transition).
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
<FeatureToggleProvider apiKey={apiKey} autoInit={false}>
|
|
228
|
+
<App />
|
|
229
|
+
</FeatureToggleProvider>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
```tsx
|
|
233
|
+
function Startup() {
|
|
234
|
+
const { init } = useFeatureToggle();
|
|
235
|
+
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
void init();
|
|
238
|
+
}, [init]);
|
|
239
|
+
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## SSR split (server loader + client provider)
|
|
247
|
+
|
|
248
|
+
Full-stack apps combine **server core** and **this React adapter** — there is no separate server provider package.
|
|
249
|
+
|
|
250
|
+
| Step | Package | Role |
|
|
251
|
+
|------|---------|------|
|
|
252
|
+
| Loader / middleware | `featuretoggle-sdk-typescript/server` | Redirects, SSR branches, seed data |
|
|
253
|
+
| Client layout | `featuretoggle-sdk-react` | Hooks, live updates |
|
|
254
|
+
| Optional seed | `initialFeatures` on provider | Match SSR without loading flash |
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
Route loader FeatureToggleServer
|
|
258
|
+
│ │
|
|
259
|
+
├──── refresh() ───────┤
|
|
260
|
+
│ │
|
|
261
|
+
▼ ▼
|
|
262
|
+
SSR HTML / loader data ──► FeatureToggleProvider ──► useFeature
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Server reads stay imperative in loaders and route handlers. The React tree handles client hydration and subscriptions.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Framework notes
|
|
270
|
+
|
|
271
|
+
These are recipes, not shipped packages:
|
|
272
|
+
|
|
273
|
+
| Framework | Approach |
|
|
274
|
+
|-----------|----------|
|
|
275
|
+
| TanStack Router / Start | Server loader uses `FeatureToggleServer`; client route wraps [client-only SPA](#client-only-spa-simplest) or [SSR seed](#ssr-seed-no-flash) |
|
|
276
|
+
| Next.js App Router | Server Component or loader uses server entry; `'use client'` boundary wraps the provider |
|
|
277
|
+
| TanStack Query | Key `['features']`; `queryFn` wraps `getFeatures()`; invalidate on `client.subscribe()` |
|
|
278
|
+
| Redux / Zustand | `subscribe()` dispatches a slice update |
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Freshness vs cost
|
|
283
|
+
|
|
284
|
+
| Pattern | Freshness | Origin load |
|
|
285
|
+
|---------|-----------|-------------|
|
|
286
|
+
| Client stream (default) | Good for open tabs | Low when ETag / 304 works |
|
|
287
|
+
| SSR seed + client stream | Good SSR + live client | Medium |
|
|
288
|
+
| Server per-request refresh | Best per page load | Highest |
|
|
289
|
+
| Server singleton + TTL refresh | Configurable | Lower |
|
|
290
|
+
|
|
291
|
+
For server singleton patterns, **await `refresh()`** before reads under concurrent requests, or use a per-request `FeatureToggleServer` instance.
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## Core SDK without React
|
|
296
|
+
|
|
297
|
+
If you need imperative client usage or `subscribe()` without hooks, see [featuretoggle-sdk-typescript](https://www.npmjs.com/package/featuretoggle-sdk-typescript) — SPA singleton, seeded cache, and manual lifecycle patterns live there.
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ React adapter for [featuretoggle-sdk-typescript](https://www.npmjs.com/package/f
|
|
|
10
10
|
bun add featuretoggle-sdk-react featuretoggle-sdk-typescript react
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Peers: `react` (≥18), `featuretoggle-sdk-typescript` (^1.0.
|
|
13
|
+
Peers: `react` (≥18), `featuretoggle-sdk-typescript` (^1.0.2).
|
|
14
14
|
|
|
15
15
|
## Quick start
|
|
16
16
|
|
|
@@ -34,7 +34,7 @@ function Checkout() {
|
|
|
34
34
|
|
|
35
35
|
## Integration patterns
|
|
36
36
|
|
|
37
|
-
See
|
|
37
|
+
See [INTEGRATION.md](./INTEGRATION.md) for server loader patterns, SSR seed, bring-your-own client, `autoInit={false}`, and security notes.
|
|
38
38
|
|
|
39
39
|
## API
|
|
40
40
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "featuretoggle-sdk-react",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
|
-
"dist"
|
|
19
|
+
"dist",
|
|
20
|
+
"INTEGRATION.md"
|
|
20
21
|
],
|
|
21
22
|
"publishConfig": {
|
|
22
23
|
"access": "public"
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
},
|
|
31
32
|
"peerDependencies": {
|
|
32
33
|
"react": ">=18",
|
|
33
|
-
"featuretoggle-sdk-typescript": "^1.0.
|
|
34
|
+
"featuretoggle-sdk-typescript": "^1.0.2"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
37
|
"@eslint/js": "^9.28.0",
|
|
@@ -38,7 +39,7 @@
|
|
|
38
39
|
"@types/react": "^19.2.0",
|
|
39
40
|
"@happy-dom/global-registrator": "^20.0.10",
|
|
40
41
|
"eslint": "^9.28.0",
|
|
41
|
-
"featuretoggle-sdk-typescript": "^1.0.
|
|
42
|
+
"featuretoggle-sdk-typescript": "^1.0.2",
|
|
42
43
|
"react": "^19.2.0",
|
|
43
44
|
"react-dom": "^19.2.0",
|
|
44
45
|
"tsup": "^8.5.0",
|