@replanejs/next 0.7.3 → 0.7.5

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 CHANGED
@@ -2,424 +2,288 @@
2
2
 
3
3
  Next.js SDK for Replane - feature flags and remote configuration with SSR support.
4
4
 
5
- ## Features
6
-
7
- - **SSR Hydration**: Fetch configs on the server, instantly hydrate on the client
8
- - **Zero Loading States**: Users see correct feature flags immediately
9
- - **Real-time Updates**: Optional live connection for instant config changes
10
- - **Type-safe**: Full TypeScript support with generics
11
- - **Next.js Optimized**: Works with App Router, Pages Router, and Server Components
12
-
13
5
  ## Installation
14
6
 
15
7
  ```bash
16
8
  npm install @replanejs/next
17
9
  # or
18
10
  pnpm add @replanejs/next
19
- # or
20
- yarn add @replanejs/next
21
11
  ```
22
12
 
23
13
  ## Quick Start
24
14
 
25
- ### 1. Set up environment variables
15
+ ### App Router (Recommended)
26
16
 
27
- ```env
28
- # Server-side (not exposed to browser)
29
- REPLANE_BASE_URL=https://your-replane-instance.com
30
- REPLANE_SDK_KEY=rp_your_server_sdk_key
31
-
32
- # Client-side (exposed to browser, for real-time updates)
33
- NEXT_PUBLIC_REPLANE_BASE_URL=https://your-replane-instance.com
34
- NEXT_PUBLIC_REPLANE_SDK_KEY=rp_your_client_sdk_key
35
- ```
36
-
37
- ### 2. Create the provider in your layout (App Router)
17
+ **1. Set up ReplaneRoot in your layout:**
38
18
 
39
19
  ```tsx
40
20
  // app/layout.tsx
41
- import { getReplaneSnapshot } from "@replanejs/next/server";
42
- import { ReplaneNextProvider } from "@replanejs/next";
43
-
44
- export default async function RootLayout({
45
- children,
46
- }: {
47
- children: React.ReactNode;
48
- }) {
49
- const snapshot = await getReplaneSnapshot({
50
- baseUrl: process.env.REPLANE_BASE_URL!,
51
- sdkKey: process.env.REPLANE_SDK_KEY!,
52
- });
21
+ import { ReplaneRoot } from "@replanejs/next";
22
+
23
+ interface AppConfigs {
24
+ theme: { darkMode: boolean; primaryColor: string };
25
+ features: { betaEnabled: boolean };
26
+ }
53
27
 
28
+ export default async function RootLayout({ children }: { children: React.ReactNode }) {
54
29
  return (
55
30
  <html lang="en">
56
31
  <body>
57
- <ReplaneNextProvider
58
- snapshot={snapshot}
59
- connection={{
32
+ <ReplaneRoot<AppConfigs>
33
+ options={{
60
34
  baseUrl: process.env.NEXT_PUBLIC_REPLANE_BASE_URL!,
61
35
  sdkKey: process.env.NEXT_PUBLIC_REPLANE_SDK_KEY!,
62
36
  }}
63
37
  >
64
38
  {children}
65
- </ReplaneNextProvider>
39
+ </ReplaneRoot>
66
40
  </body>
67
41
  </html>
68
42
  );
69
43
  }
70
44
  ```
71
45
 
72
- ### 3. Use configs in your components
46
+ **2. Use configs in client components:**
73
47
 
74
48
  ```tsx
75
- // app/components/feature.tsx
49
+ // components/ThemeToggle.tsx
76
50
  "use client";
77
51
 
78
52
  import { useConfig } from "@replanejs/next";
79
53
 
80
- export function FeatureComponent() {
81
- const isEnabled = useConfig<boolean>("my-feature");
82
- const maxItems = useConfig<number>("max-items");
83
-
84
- if (!isEnabled) {
85
- return null;
86
- }
87
-
88
- return <div>Feature enabled! Max items: {maxItems}</div>;
54
+ export function ThemeToggle() {
55
+ const theme = useConfig<{ darkMode: boolean }>("theme");
56
+ return <div>{theme.darkMode ? "Dark Mode" : "Light Mode"}</div>;
89
57
  }
90
58
  ```
91
59
 
92
- ## API Reference
93
-
94
- ### Server Functions
95
-
96
- #### `getReplaneSnapshot(options)`
60
+ ### Pages Router
97
61
 
98
- Fetches configs from Replane and returns a serializable snapshot for client-side hydration.
62
+ **1. Set up ReplaneProvider in _app.tsx:**
99
63
 
100
64
  ```tsx
101
- import { getReplaneSnapshot } from "@replanejs/next/server";
102
-
103
- const snapshot = await getReplaneSnapshot({
104
- // Required
105
- baseUrl: "https://your-replane-instance.com",
106
- sdkKey: "rp_your_sdk_key",
107
-
108
- // Optional
109
- fetchFn: customFetch, // Custom fetch for caching
110
- requestTimeoutMs: 2000, // Request timeout (default: 2000)
111
- initializationTimeoutMs: 5000, // Init timeout (default: 5000)
112
- context: { userId: "123" }, // Context for override evaluation
113
- required: ["feature-a", "feature-b"], // Required configs
114
- fallbacks: { "feature-a": false }, // Fallback values
115
- });
116
- ```
117
-
118
- #### `getConfig(options)`
119
-
120
- Get a single config value directly on the server.
121
-
122
- ```tsx
123
- import { getConfig } from "@replanejs/next/server";
65
+ // pages/_app.tsx
66
+ import type { AppContext, AppProps } from "next/app";
67
+ import App from "next/app";
68
+ import { ReplaneProvider, getReplaneSnapshot, type ReplaneSnapshot } from "@replanejs/next";
124
69
 
125
- const maintenanceMode = await getConfig<boolean>({
126
- name: "maintenance-mode",
127
- baseUrl: process.env.REPLANE_BASE_URL!,
128
- sdkKey: process.env.REPLANE_SDK_KEY!,
129
- context: { region: "us-east" },
130
- });
70
+ interface AppConfigs {
71
+ theme: { darkMode: boolean; primaryColor: string };
72
+ features: { betaEnabled: boolean };
73
+ }
131
74
 
132
- if (maintenanceMode) {
133
- return <MaintenancePage />;
75
+ interface AppPropsWithReplane extends AppProps {
76
+ replaneSnapshot: ReplaneSnapshot<AppConfigs>;
134
77
  }
135
- ```
136
78
 
137
- ### Client Components
79
+ export default function MyApp({ Component, pageProps, replaneSnapshot }: AppPropsWithReplane) {
80
+ return (
81
+ <ReplaneProvider
82
+ snapshot={replaneSnapshot}
83
+ options={{
84
+ baseUrl: process.env.NEXT_PUBLIC_REPLANE_BASE_URL!,
85
+ sdkKey: process.env.NEXT_PUBLIC_REPLANE_SDK_KEY!,
86
+ }}
87
+ >
88
+ <Component {...pageProps} />
89
+ </ReplaneProvider>
90
+ );
91
+ }
138
92
 
139
- #### `ReplaneNextProvider`
93
+ // Fetch Replane snapshot for all pages
94
+ MyApp.getInitialProps = async (appContext: AppContext) => {
95
+ const appProps = await App.getInitialProps(appContext);
140
96
 
141
- Main provider component for SSR hydration.
97
+ const replaneSnapshot = await getReplaneSnapshot<AppConfigs>({
98
+ baseUrl: process.env.REPLANE_BASE_URL!,
99
+ sdkKey: process.env.REPLANE_SDK_KEY!,
100
+ });
142
101
 
143
- ```tsx
144
- import { ReplaneNextProvider } from "@replanejs/next";
145
-
146
- <ReplaneNextProvider
147
- snapshot={snapshot} // Required: from getReplaneSnapshot()
148
- connection={{
149
- // Optional: for real-time updates
150
- baseUrl: "https://...",
151
- sdkKey: "rp_...",
152
- requestTimeoutMs: 2000,
153
- retryDelayMs: 200,
154
- inactivityTimeoutMs: 30000,
155
- }}
156
- context={{ userId: "123" }} // Optional: override context on client
157
- >
158
- {children}
159
- </ReplaneNextProvider>;
102
+ return { ...appProps, replaneSnapshot };
103
+ };
160
104
  ```
161
105
 
162
- #### `ReplaneScriptProvider`
163
-
164
- Alternative hydration pattern using embedded scripts.
106
+ **2. Use configs in any component:**
165
107
 
166
108
  ```tsx
167
- // In layout (Server Component)
168
- import { getReplaneSnapshotScript, ReplaneScriptProvider } from "@replanejs/next";
169
-
170
- export default async function RootLayout({ children }) {
171
- const snapshot = await getReplaneSnapshot({ ... });
109
+ // components/FeatureFlag.tsx
110
+ import { useConfig } from "@replanejs/next";
172
111
 
173
- return (
174
- <html>
175
- <head>
176
- <script
177
- dangerouslySetInnerHTML={{
178
- __html: getReplaneSnapshotScript(snapshot),
179
- }}
180
- />
181
- </head>
182
- <body>
183
- <ReplaneScriptProvider connection={{ baseUrl, sdkKey }}>
184
- {children}
185
- </ReplaneScriptProvider>
186
- </body>
187
- </html>
188
- );
112
+ export function FeatureFlag() {
113
+ const features = useConfig<{ betaEnabled: boolean }>("features");
114
+ return features.betaEnabled ? <BetaFeature /> : null;
189
115
  }
190
116
  ```
191
117
 
192
- ### Hooks
118
+ ## Typed Hooks (Recommended)
193
119
 
194
- #### `useConfig<T>(name, options?)`
120
+ For better type safety and autocomplete, create typed hooks for your application:
195
121
 
196
- Subscribe to a specific config with reactive updates.
122
+ **1. Define your config types:**
197
123
 
198
- ```tsx
199
- import { useConfig } from "@replanejs/next";
124
+ ```ts
125
+ // replane/types.ts
126
+ export interface AppConfigs {
127
+ theme: {
128
+ darkMode: boolean;
129
+ primaryColor: string;
130
+ };
131
+ features: {
132
+ betaEnabled: boolean;
133
+ maxItems: number;
134
+ };
135
+ }
136
+ ```
200
137
 
201
- function MyComponent() {
202
- // Basic usage
203
- const feature = useConfig<boolean>("feature-flag");
138
+ **2. Create typed hooks:**
204
139
 
205
- // With context override
206
- const price = useConfig<number>("pricing", {
207
- context: { plan: "premium" },
208
- });
140
+ ```ts
141
+ // replane/hooks.ts
142
+ import { createConfigHook, createReplaneHook } from "@replanejs/next";
143
+ import type { AppConfigs } from "./types";
209
144
 
210
- return <div>{feature ? "Enabled" : "Disabled"}</div>;
211
- }
212
- ```
145
+ // Typed hook for accessing individual configs
146
+ export const useAppConfig = createConfigHook<AppConfigs>();
213
147
 
214
- #### `useReplane()`
148
+ // Typed hook for accessing the Replane client
149
+ export const useAppReplane = createReplaneHook<AppConfigs>();
150
+ ```
215
151
 
216
- Access the underlying Replane client.
152
+ **3. Use in components:**
217
153
 
218
154
  ```tsx
219
- import { useReplane } from "@replanejs/next";
155
+ // components/ConfigDisplay.tsx
156
+ "use client";
220
157
 
221
- function MyComponent() {
222
- const { client } = useReplane();
158
+ import { useAppConfig, useAppReplane } from "@/replane/hooks";
223
159
 
224
- // Access client methods
225
- const snapshot = client.getSnapshot();
160
+ export function ConfigDisplay() {
161
+ // Config names autocomplete, values are fully typed
162
+ const theme = useAppConfig("theme");
163
+ // theme.darkMode is boolean, theme.primaryColor is string
226
164
 
227
- return <div>...</div>;
165
+ // Or use the client directly for more control
166
+ const replane = useAppReplane();
167
+ const snapshot = replane.getSnapshot();
168
+
169
+ return <div style={{ color: theme.primaryColor }}>...</div>;
228
170
  }
229
171
  ```
230
172
 
231
- ## Advanced Usage
173
+ ## API Reference
232
174
 
233
- ### Next.js Caching
175
+ ### Components
234
176
 
235
- Use Next.js fetch caching with `getReplaneSnapshot`:
177
+ #### `ReplaneRoot`
236
178
 
237
- ```tsx
238
- // ISR: Revalidate every 60 seconds
239
- const snapshot = await getReplaneSnapshot({
240
- baseUrl: process.env.REPLANE_BASE_URL!,
241
- sdkKey: process.env.REPLANE_SDK_KEY!,
242
- fetchFn: (url, init) =>
243
- fetch(url, {
244
- ...init,
245
- next: { revalidate: 60 },
246
- }),
247
- });
248
- ```
179
+ Server component for App Router that fetches configs and provides them to the app.
249
180
 
250
181
  ```tsx
251
- // On-demand revalidation with tags
252
- const snapshot = await getReplaneSnapshot({
253
- baseUrl: process.env.REPLANE_BASE_URL!,
254
- sdkKey: process.env.REPLANE_SDK_KEY!,
255
- fetchFn: (url, init) =>
256
- fetch(url, {
257
- ...init,
258
- next: { tags: ["replane-config"] },
259
- }),
260
- });
261
-
262
- // In a server action or route handler:
263
- // revalidateTag('replane-config');
182
+ <ReplaneRoot<AppConfigs>
183
+ options={{
184
+ baseUrl: string;
185
+ sdkKey: string;
186
+ // ... other ReplaneClientOptions
187
+ }}
188
+ >
189
+ {children}
190
+ </ReplaneRoot>
264
191
  ```
265
192
 
266
- ### Static Snapshot (No Real-time Updates)
193
+ #### `ReplaneProvider`
267
194
 
268
- For static sites or when real-time updates aren't needed:
195
+ Client-side provider for Pages Router or custom setups.
269
196
 
270
197
  ```tsx
271
- <ReplaneNextProvider snapshot={snapshot}>
272
- {/* No connection prop = no live updates */}
198
+ <ReplaneProvider
199
+ snapshot={replaneSnapshot}
200
+ options={{
201
+ baseUrl: string;
202
+ sdkKey: string;
203
+ }}
204
+ >
273
205
  {children}
274
- </ReplaneNextProvider>
206
+ </ReplaneProvider>
275
207
  ```
276
208
 
277
- ### Pages Router Support
209
+ ### Hooks
278
210
 
279
- ```tsx
280
- // pages/_app.tsx
281
- import { ReplaneNextProvider } from "@replanejs/next";
282
- import type { AppProps } from "next/app";
211
+ #### `useConfig<T>(name: string): T`
283
212
 
284
- export default function App({ Component, pageProps }: AppProps) {
285
- return (
286
- <ReplaneNextProvider
287
- snapshot={pageProps.replaneSnapshot}
288
- connection={{
289
- baseUrl: process.env.NEXT_PUBLIC_REPLANE_BASE_URL!,
290
- sdkKey: process.env.NEXT_PUBLIC_REPLANE_SDK_KEY!,
291
- }}
292
- >
293
- <Component {...pageProps} />
294
- </ReplaneNextProvider>
295
- );
296
- }
297
- ```
213
+ Returns the value of a config by name. Re-renders when the config changes.
298
214
 
299
215
  ```tsx
300
- // pages/index.tsx
301
- import { getReplaneSnapshot } from "@replanejs/next/server";
302
- import type { GetServerSideProps } from "next";
216
+ const theme = useConfig<{ darkMode: boolean }>("theme");
217
+ ```
303
218
 
304
- export const getServerSideProps: GetServerSideProps = async () => {
305
- const replaneSnapshot = await getReplaneSnapshot({
306
- baseUrl: process.env.REPLANE_BASE_URL!,
307
- sdkKey: process.env.REPLANE_SDK_KEY!,
308
- });
219
+ #### `useReplane<T>(): ReplaneClient<T>`
309
220
 
310
- return {
311
- props: { replaneSnapshot },
312
- };
313
- };
221
+ Returns the Replane client instance for advanced usage.
222
+
223
+ ```tsx
224
+ const client = useReplane<AppConfigs>();
225
+ const snapshot = client.getSnapshot();
226
+ const theme = client.get("theme");
314
227
  ```
315
228
 
316
- ### Context-based Overrides
229
+ #### `createConfigHook<T>()`
317
230
 
318
- Pass user context for personalized config values:
231
+ Creates a typed version of `useConfig` for your config schema.
319
232
 
320
233
  ```tsx
321
- // Server-side: include context in snapshot
322
- const snapshot = await getReplaneSnapshot({
323
- baseUrl: process.env.REPLANE_BASE_URL!,
324
- sdkKey: process.env.REPLANE_SDK_KEY!,
325
- context: {
326
- userId: user.id,
327
- plan: user.subscription,
328
- country: user.country,
329
- },
330
- });
331
-
332
- // Client-side: override or extend context
333
- <ReplaneNextProvider
334
- snapshot={snapshot}
335
- context={{
336
- // Add client-specific context
337
- browser: navigator.userAgent,
338
- screenSize: window.innerWidth > 768 ? "desktop" : "mobile",
339
- }}
340
- >
341
- {children}
342
- </ReplaneNextProvider>
234
+ const useAppConfig = createConfigHook<AppConfigs>();
235
+ const theme = useAppConfig("theme"); // fully typed
343
236
  ```
344
237
 
345
- ### Required Configs
238
+ #### `createReplaneHook<T>()`
346
239
 
347
- Ensure specific configs are loaded before rendering:
240
+ Creates a typed version of `useReplane` for your config schema.
348
241
 
349
242
  ```tsx
350
- const snapshot = await getReplaneSnapshot({
351
- baseUrl: process.env.REPLANE_BASE_URL!,
352
- sdkKey: process.env.REPLANE_SDK_KEY!,
353
- required: ["critical-feature", "api-endpoint"],
354
- // Or with default values:
355
- required: {
356
- "critical-feature": true,
357
- "api-endpoint": "https://default.api.com",
358
- },
359
- });
243
+ const useAppReplane = createReplaneHook<AppConfigs>();
244
+ const client = useAppReplane(); // client.get("theme") is typed
360
245
  ```
361
246
 
362
- ### Error Handling with Fallbacks
247
+ ### Functions
363
248
 
364
- Provide fallback values for resilience:
249
+ #### `getReplaneSnapshot<T>(options): Promise<ReplaneSnapshot<T>>`
250
+
251
+ Fetches a snapshot of all configs. Use in `getServerSideProps`, `getStaticProps`, or `getInitialProps`.
365
252
 
366
253
  ```tsx
367
- const snapshot = await getReplaneSnapshot({
254
+ const snapshot = await getReplaneSnapshot<AppConfigs>({
368
255
  baseUrl: process.env.REPLANE_BASE_URL!,
369
256
  sdkKey: process.env.REPLANE_SDK_KEY!,
370
- fallbacks: {
371
- "feature-flag": false,
372
- "max-items": 10,
373
- "api-endpoint": "https://fallback.api.com",
374
- },
257
+ cacheTtlMs: 60_000, // optional, default 60 seconds
375
258
  });
376
259
  ```
377
260
 
378
- ## TypeScript
261
+ #### `clearSnapshotCache(): Promise<void>`
379
262
 
380
- Define your config types for full type safety:
263
+ Clears the internal client cache. Useful for testing.
381
264
 
382
265
  ```tsx
383
- // types/replane.ts
384
- export interface ReplaneConfigs {
385
- "feature-flag": boolean;
386
- "max-items": number;
387
- "api-endpoint": string;
388
- theme: {
389
- primaryColor: string;
390
- darkMode: boolean;
391
- };
392
- }
266
+ await clearSnapshotCache();
393
267
  ```
394
268
 
395
- ```tsx
396
- // Use with generics
397
- import type { ReplaneConfigs } from "./types/replane";
269
+ ## Environment Variables
398
270
 
399
- const snapshot = await getReplaneSnapshot<ReplaneConfigs>({
400
- baseUrl: process.env.REPLANE_BASE_URL!,
401
- sdkKey: process.env.REPLANE_SDK_KEY!,
402
- });
271
+ ```env
272
+ # Server-side only (for SSR/SSG)
273
+ REPLANE_BASE_URL=https://api.replane.io
274
+ REPLANE_SDK_KEY=your-sdk-key
403
275
 
404
- // In components
405
- const theme = useConfig<ReplaneConfigs["theme"]>("theme");
276
+ # Client-side (for live updates)
277
+ NEXT_PUBLIC_REPLANE_BASE_URL=https://api.replane.io
278
+ NEXT_PUBLIC_REPLANE_SDK_KEY=your-sdk-key
406
279
  ```
407
280
 
408
- ## Why SSR Hydration?
409
-
410
- The snapshot pattern minimizes latency by:
411
-
412
- 1. **Server Fetch**: Configs are fetched during SSR (no client-side request delay)
413
- 2. **Instant Hydration**: Client instantly has all config values (no loading states)
414
- 3. **Optional Live Updates**: Real-time connection established after hydration
415
-
416
- This means users see correct feature flags immediately without any loading states or flashes of incorrect content.
281
+ ## Examples
417
282
 
418
- ## Requirements
283
+ See the [examples](./examples) directory for complete working examples:
419
284
 
420
- - Next.js >= 13.0.0
421
- - React >= 18.0.0
422
- - Node.js >= 18.0.0
285
+ - **[next-app-router](./examples/next-app-router)** - App Router with ReplaneRoot
286
+ - **[next-pages-router](./examples/next-pages-router)** - Pages Router with getInitialProps
423
287
 
424
288
  ## License
425
289