featuretoggle-sdk-react 1.0.3 → 1.0.6

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 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.0).
13
+ Peers: `react` (≥18), `featuretoggle-sdk-typescript` (^1.0.4).
14
14
 
15
15
  ## Quick start
16
16
 
@@ -34,7 +34,7 @@ function Checkout() {
34
34
 
35
35
  ## Integration patterns
36
36
 
37
- See the [integration cookbook](https://github.com/feature-toggle/feature-toggle-monorepo/blob/main/docs/17-sdk-integration-patterns.md) for SSR seed, BYO client, `autoInit={false}`, and more (patterns I–N).
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",
3
+ "version": "1.0.6",
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.1"
34
+ "featuretoggle-sdk-typescript": "^1.0.4"
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.1",
42
+ "featuretoggle-sdk-typescript": "^1.0.4",
42
43
  "react": "^19.2.0",
43
44
  "react-dom": "^19.2.0",
44
45
  "tsup": "^8.5.0",