@replanejs/react 0.7.2 → 0.7.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/README.md CHANGED
@@ -51,23 +51,26 @@ function MyComponent() {
51
51
 
52
52
  ### ReplaneProvider
53
53
 
54
- Provider component that makes the Replane client available to your component tree. Supports three usage patterns:
54
+ Provider component that makes the Replane client available to your component tree. Supports four usage patterns:
55
55
 
56
56
  #### 1. With options (recommended)
57
57
 
58
- The provider creates and manages the client internally:
58
+ The provider creates and manages the client internally. Use an Error Boundary to handle initialization errors:
59
59
 
60
60
  ```tsx
61
- <ReplaneProvider
62
- options={{
63
- baseUrl: 'https://your-replane-server.com',
64
- sdkKey: 'your-sdk-key',
65
- }}
66
- loader={<LoadingSpinner />}
67
- onError={(error) => console.error('Failed to initialize:', error)}
68
- >
69
- <App />
70
- </ReplaneProvider>
61
+ import { ErrorBoundary } from 'react-error-boundary';
62
+
63
+ <ErrorBoundary fallback={<div>Failed to load configuration</div>}>
64
+ <ReplaneProvider
65
+ options={{
66
+ baseUrl: 'https://your-replane-server.com',
67
+ sdkKey: 'your-sdk-key',
68
+ }}
69
+ loader={<LoadingSpinner />}
70
+ >
71
+ <App />
72
+ </ReplaneProvider>
73
+ </ErrorBoundary>
71
74
  ```
72
75
 
73
76
  #### 2. With pre-created client
@@ -92,19 +95,48 @@ const client = await createReplaneClient({
92
95
  Integrates with React Suspense for loading states:
93
96
 
94
97
  ```tsx
95
- <Suspense fallback={<LoadingSpinner />}>
96
- <ReplaneProvider
97
- options={{
98
+ <ErrorBoundary fallback={<div>Failed to load configuration</div>}>
99
+ <Suspense fallback={<LoadingSpinner />}>
100
+ <ReplaneProvider
101
+ options={{
102
+ baseUrl: 'https://your-replane-server.com',
103
+ sdkKey: 'your-sdk-key',
104
+ }}
105
+ suspense
106
+ >
107
+ <App />
108
+ </ReplaneProvider>
109
+ </Suspense>
110
+ </ErrorBoundary>
111
+ ```
112
+
113
+ #### 4. With snapshot (for SSR/hydration)
114
+
115
+ Restore a client from a snapshot obtained on the server. This is synchronous and useful for SSR scenarios:
116
+
117
+ ```tsx
118
+ // On the server
119
+ const serverClient = await createReplaneClient({ baseUrl: '...', sdkKey: '...' });
120
+ const snapshot = serverClient.getSnapshot();
121
+ // Pass snapshot to client via props, context, or serialized HTML
122
+
123
+ // On the client
124
+ <ReplaneProvider
125
+ restoreOptions={{
126
+ snapshot,
127
+ // Optional: connect for live updates
128
+ connection: {
98
129
  baseUrl: 'https://your-replane-server.com',
99
130
  sdkKey: 'your-sdk-key',
100
- }}
101
- suspense
102
- >
103
- <App />
104
- </ReplaneProvider>
105
- </Suspense>
131
+ },
132
+ }}
133
+ >
134
+ <App />
135
+ </ReplaneProvider>
106
136
  ```
107
137
 
138
+ The restored client is immediately available with no loading state. If `connection` is provided, it will establish a connection for real-time updates in the background.
139
+
108
140
  ### useConfig
109
141
 
110
142
  Hook to retrieve a configuration value. Automatically subscribes to updates and re-renders when the value changes.
@@ -128,15 +160,15 @@ function MyComponent() {
128
160
 
129
161
  ### useReplane
130
162
 
131
- Hook to access the underlying Replane client directly:
163
+ Hook to access the underlying Replane client directly. Returns the client instance:
132
164
 
133
165
  ```tsx
134
166
  function MyComponent() {
135
- const { client } = useReplane();
167
+ const replane = useReplane();
136
168
 
137
169
  const handleClick = () => {
138
- // Access client methods directly
139
- const value = client.get('some-config');
170
+ // Access replane methods directly
171
+ const value = replane.get('some-config');
140
172
  console.log(value);
141
173
  };
142
174
 
@@ -144,6 +176,77 @@ function MyComponent() {
144
176
  }
145
177
  ```
146
178
 
179
+ ### createReplaneHook
180
+
181
+ Factory function to create a typed version of `useReplane`. Returns a hook that provides the typed client directly:
182
+
183
+ ```tsx
184
+ import { createReplaneHook } from '@replanejs/react';
185
+
186
+ // Define your config types
187
+ interface AppConfigs {
188
+ theme: { darkMode: boolean; primaryColor: string };
189
+ features: { beta: boolean; analytics: boolean };
190
+ maxItems: number;
191
+ }
192
+
193
+ // Create a typed hook
194
+ const useAppReplane = createReplaneHook<AppConfigs>();
195
+
196
+ function MyComponent() {
197
+ const replane = useAppReplane();
198
+
199
+ // replane.get is now typed - autocomplete works!
200
+ const theme = replane.get('theme');
201
+ // ^? { darkMode: boolean; primaryColor: string }
202
+
203
+ return <div>Dark mode: {theme.darkMode ? 'on' : 'off'}</div>;
204
+ }
205
+ ```
206
+
207
+ ### createConfigHook
208
+
209
+ Factory function to create a typed version of `useConfig`. This provides autocomplete for config names and type inference for values:
210
+
211
+ ```tsx
212
+ import { createConfigHook } from '@replanejs/react';
213
+
214
+ // Define your config types
215
+ interface AppConfigs {
216
+ theme: { darkMode: boolean; primaryColor: string };
217
+ features: { beta: boolean; analytics: boolean };
218
+ maxItems: number;
219
+ }
220
+
221
+ // Create a typed hook
222
+ const useAppConfig = createConfigHook<AppConfigs>();
223
+
224
+ function MyComponent() {
225
+ // Autocomplete for config names, automatic type inference
226
+ const theme = useAppConfig('theme');
227
+ // ^? { darkMode: boolean; primaryColor: string }
228
+
229
+ const features = useAppConfig('features');
230
+ // ^? { beta: boolean; analytics: boolean }
231
+
232
+ const maxItems = useAppConfig('maxItems');
233
+ // ^? number
234
+
235
+ // With context override
236
+ const premiumFeatures = useAppConfig('features', {
237
+ context: { userId: '123', plan: 'premium' },
238
+ });
239
+
240
+ return (
241
+ <div>
242
+ <p>Dark mode: {theme.darkMode ? 'on' : 'off'}</p>
243
+ <p>Beta enabled: {features.beta ? 'yes' : 'no'}</p>
244
+ <p>Max items: {maxItems}</p>
245
+ </div>
246
+ );
247
+ }
248
+ ```
249
+
147
250
  ### clearSuspenseCache
148
251
 
149
252
  Utility function to clear the suspense cache. Useful for testing or forcing re-initialization:
@@ -163,21 +266,95 @@ clearSuspenseCache();
163
266
 
164
267
  ## TypeScript
165
268
 
166
- The SDK is fully typed. You can provide a type parameter to get type-safe configuration values:
269
+ The SDK is fully typed. For the best TypeScript experience, use the hook factory functions:
167
270
 
168
271
  ```tsx
169
- interface MyConfig {
170
- theme: 'light' | 'dark';
171
- maxItems: number;
172
- features: {
173
- analytics: boolean;
174
- notifications: boolean;
272
+ // Define all your config types in one interface
273
+ interface AppConfigs {
274
+ 'theme-config': {
275
+ darkMode: boolean;
276
+ primaryColor: string;
277
+ };
278
+ 'feature-flags': {
279
+ newUI: boolean;
280
+ beta: boolean;
175
281
  };
282
+ 'max-items': number;
283
+ 'welcome-message': string;
176
284
  }
177
285
 
178
- // Type-safe hooks
179
- const { client } = useReplane<MyConfig>();
180
- const theme = useConfig<MyConfig['theme']>('theme');
286
+ // Create typed hooks once
287
+ const useAppReplane = createReplaneHook<AppConfigs>();
288
+ const useAppConfig = createConfigHook<AppConfigs>();
289
+
290
+ // Use throughout your app with full type safety
291
+ function Settings() {
292
+ const theme = useAppConfig('theme-config');
293
+ // ^? { darkMode: boolean; primaryColor: string }
294
+
295
+ const replane = useAppReplane();
296
+ const snapshot = replane.getSnapshot();
297
+ // ^? { configs: ConfigSnapshot<AppConfigs>[] }
298
+
299
+ return (
300
+ <div style={{ color: theme.primaryColor }}>
301
+ Dark mode: {theme.darkMode ? 'enabled' : 'disabled'}
302
+ </div>
303
+ );
304
+ }
305
+ ```
306
+
307
+ ## Error Handling
308
+
309
+ The provider throws errors during rendering so they can be caught by React Error Boundaries:
310
+
311
+ ```tsx
312
+ import { Component, ReactNode } from 'react';
313
+
314
+ class ErrorBoundary extends Component<
315
+ { children: ReactNode; fallback: ReactNode },
316
+ { hasError: boolean }
317
+ > {
318
+ state = { hasError: false };
319
+
320
+ static getDerivedStateFromError() {
321
+ return { hasError: true };
322
+ }
323
+
324
+ render() {
325
+ if (this.state.hasError) {
326
+ return this.props.fallback;
327
+ }
328
+ return this.props.children;
329
+ }
330
+ }
331
+
332
+ // Usage
333
+ <ErrorBoundary fallback={<div>Configuration failed to load</div>}>
334
+ <ReplaneProvider options={options} loader={<Loading />}>
335
+ <App />
336
+ </ReplaneProvider>
337
+ </ErrorBoundary>
338
+ ```
339
+
340
+ Or use a library like `react-error-boundary`:
341
+
342
+ ```tsx
343
+ import { ErrorBoundary } from 'react-error-boundary';
344
+
345
+ <ErrorBoundary
346
+ fallbackRender={({ error, resetErrorBoundary }) => (
347
+ <div>
348
+ <p>Error: {error.message}</p>
349
+ <button onClick={resetErrorBoundary}>Retry</button>
350
+ </div>
351
+ )}
352
+ onReset={() => clearSuspenseCache()}
353
+ >
354
+ <ReplaneProvider options={options} loader={<Loading />}>
355
+ <App />
356
+ </ReplaneProvider>
357
+ </ErrorBoundary>
181
358
  ```
182
359
 
183
360
  ## License
package/dist/index.cjs CHANGED
@@ -38,7 +38,7 @@ function getCacheKey(options) {
38
38
  * Hook to manage ReplaneClient creation internally.
39
39
  * Handles loading state and cleanup.
40
40
  */
41
- function useReplaneClient(options, onError) {
41
+ function useReplaneClientInternal(options) {
42
42
  const [state, setState] = (0, react.useState)({
43
43
  status: "loading",
44
44
  client: null,
@@ -63,13 +63,12 @@ function useReplaneClient(options, onError) {
63
63
  });
64
64
  } catch (err) {
65
65
  if (cancelled) return;
66
- const error = err instanceof Error ? err : new Error(String(err));
66
+ const error = err instanceof Error ? err : new Error(String(err), { cause: err });
67
67
  setState({
68
68
  status: "error",
69
69
  client: null,
70
70
  error
71
71
  });
72
- onError?.(error);
73
72
  }
74
73
  }
75
74
  initClient();
@@ -124,6 +123,12 @@ function clearSuspenseCache(options) {
124
123
  function hasClient(props) {
125
124
  return "client" in props && props.client !== void 0;
126
125
  }
126
+ /**
127
+ * Type guard to check if props contain restore options.
128
+ */
129
+ function hasRestoreOptions(props) {
130
+ return "restoreOptions" in props && props.restoreOptions !== void 0;
131
+ }
127
132
 
128
133
  //#endregion
129
134
  //#region src/provider.tsx
@@ -139,11 +144,12 @@ function ReplaneProviderWithClient({ client, children }) {
139
144
  }
140
145
  /**
141
146
  * Internal provider component for options-based client creation (non-suspense).
147
+ * Throws errors during rendering so they can be caught by Error Boundaries.
142
148
  */
143
- function ReplaneProviderWithOptions({ options, children, loader, onError }) {
144
- const state = useReplaneClient(options, onError);
149
+ function ReplaneProviderWithOptions({ options, children, loader }) {
150
+ const state = useReplaneClientInternal(options);
145
151
  if (state.status === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: loader ?? null });
146
- if (state.status === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: loader ?? null });
152
+ if (state.status === "error") throw state.error;
147
153
  const value = { client: state.client };
148
154
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReplaneContext.Provider, {
149
155
  value,
@@ -162,9 +168,21 @@ function ReplaneProviderWithSuspense({ options, children }) {
162
168
  });
163
169
  }
164
170
  /**
171
+ * Internal provider component for restoring client from snapshot.
172
+ * Uses restoreReplaneClient which is synchronous.
173
+ */
174
+ function ReplaneProviderWithSnapshot({ restoreOptions, children }) {
175
+ const client = (0, react.useMemo)(() => (0, __replanejs_sdk.restoreReplaneClient)(restoreOptions), [restoreOptions]);
176
+ const value = (0, react.useMemo)(() => ({ client }), [client]);
177
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReplaneContext.Provider, {
178
+ value,
179
+ children
180
+ });
181
+ }
182
+ /**
165
183
  * Provider component that makes a ReplaneClient available to the component tree.
166
184
  *
167
- * Can be used in two ways:
185
+ * Can be used in four ways:
168
186
  *
169
187
  * 1. With a pre-created client:
170
188
  * ```tsx
@@ -176,28 +194,52 @@ function ReplaneProviderWithSuspense({ options, children }) {
176
194
  *
177
195
  * 2. With options (client managed internally):
178
196
  * ```tsx
179
- * <ReplaneProvider
180
- * options={{ baseUrl: '...', sdkKey: '...' }}
181
- * loader={<LoadingSpinner />}
182
- * >
183
- * <App />
184
- * </ReplaneProvider>
185
- * ```
186
- *
187
- * 3. With Suspense:
188
- * ```tsx
189
- * <Suspense fallback={<LoadingSpinner />}>
197
+ * <ErrorBoundary fallback={<ErrorMessage />}>
190
198
  * <ReplaneProvider
191
199
  * options={{ baseUrl: '...', sdkKey: '...' }}
192
- * suspense
200
+ * loader={<LoadingSpinner />}
193
201
  * >
194
202
  * <App />
195
203
  * </ReplaneProvider>
196
- * </Suspense>
204
+ * </ErrorBoundary>
205
+ * ```
206
+ *
207
+ * 3. With Suspense:
208
+ * ```tsx
209
+ * <ErrorBoundary fallback={<ErrorMessage />}>
210
+ * <Suspense fallback={<LoadingSpinner />}>
211
+ * <ReplaneProvider
212
+ * options={{ baseUrl: '...', sdkKey: '...' }}
213
+ * suspense
214
+ * >
215
+ * <App />
216
+ * </ReplaneProvider>
217
+ * </Suspense>
218
+ * </ErrorBoundary>
219
+ * ```
220
+ *
221
+ * 4. With a snapshot (for SSR/hydration):
222
+ * ```tsx
223
+ * // On the server, get a snapshot from the client
224
+ * const snapshot = serverClient.getSnapshot();
225
+ *
226
+ * // On the client, restore from the snapshot
227
+ * <ReplaneProvider
228
+ * restoreOptions={{
229
+ * snapshot,
230
+ * connection: { baseUrl: '...', sdkKey: '...' } // optional, for live updates
231
+ * }}
232
+ * >
233
+ * <App />
234
+ * </ReplaneProvider>
197
235
  * ```
236
+ *
237
+ * Errors during client initialization are thrown during rendering,
238
+ * allowing them to be caught by React Error Boundaries.
198
239
  */
199
240
  function ReplaneProvider(props) {
200
241
  if (hasClient(props)) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReplaneProviderWithClient, { ...props });
242
+ if (hasRestoreOptions(props)) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReplaneProviderWithSnapshot, { ...props });
201
243
  if (props.suspense) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReplaneProviderWithSuspense, { ...props });
202
244
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReplaneProviderWithOptions, { ...props });
203
245
  }
@@ -207,18 +249,66 @@ function ReplaneProvider(props) {
207
249
  function useReplane() {
208
250
  const context = (0, react.useContext)(ReplaneContext);
209
251
  if (!context) throw new Error("useReplane must be used within a ReplaneProvider");
210
- return context;
252
+ return context.client;
211
253
  }
212
254
  function useConfig(name, options) {
213
- const { client } = useReplane();
255
+ const client = useReplane();
214
256
  const value = (0, react.useSyncExternalStore)((onStoreChange) => {
215
257
  return client.subscribe(name, onStoreChange);
216
258
  }, () => client.get(name, options), () => client.get(name, options));
217
259
  return value;
218
260
  }
261
+ /**
262
+ * Creates a typed version of useReplane hook.
263
+ *
264
+ * @example
265
+ * ```tsx
266
+ * interface AppConfigs {
267
+ * theme: { darkMode: boolean };
268
+ * features: { beta: boolean };
269
+ * }
270
+ *
271
+ * const useAppReplane = createReplaneHook<AppConfigs>();
272
+ *
273
+ * function MyComponent() {
274
+ * const replane = useAppReplane();
275
+ * // replane.get("theme") returns { darkMode: boolean }
276
+ * }
277
+ * ```
278
+ */
279
+ function createReplaneHook() {
280
+ return function useTypedReplane() {
281
+ return useReplane();
282
+ };
283
+ }
284
+ /**
285
+ * Creates a typed version of useConfig hook.
286
+ *
287
+ * @example
288
+ * ```tsx
289
+ * interface AppConfigs {
290
+ * theme: { darkMode: boolean };
291
+ * features: { beta: boolean };
292
+ * }
293
+ *
294
+ * const useAppConfig = createConfigHook<AppConfigs>();
295
+ *
296
+ * function MyComponent() {
297
+ * const theme = useAppConfig("theme");
298
+ * // theme is typed as { darkMode: boolean }
299
+ * }
300
+ * ```
301
+ */
302
+ function createConfigHook() {
303
+ return function useTypedConfig(name, options) {
304
+ return useConfig(String(name), options);
305
+ };
306
+ }
219
307
 
220
308
  //#endregion
221
309
  exports.ReplaneProvider = ReplaneProvider;
222
310
  exports.clearSuspenseCache = clearSuspenseCache;
311
+ exports.createConfigHook = createConfigHook;
312
+ exports.createReplaneHook = createReplaneHook;
223
313
  exports.useConfig = useConfig;
224
314
  exports.useReplane = useReplane;
package/dist/index.d.cts CHANGED
@@ -1,15 +1,13 @@
1
1
  import * as react_jsx_runtime0 from "react/jsx-runtime";
2
- import { ReplaneClient, ReplaneClientOptions } from "@replanejs/sdk";
2
+ import { GetConfigOptions, ReplaneClient, ReplaneClientOptions, RestoreReplaneClientOptions } from "@replanejs/sdk";
3
3
  import { ReactNode } from "react";
4
4
 
5
5
  //#region src/types.d.ts
6
- interface ReplaneContextValue<T extends object = any> {
7
- client: ReplaneClient<T>;
8
- }
6
+ type UntypedReplaneConfig = Record<string, unknown>;
9
7
  /**
10
8
  * Props for ReplaneProvider when using a pre-created client.
11
9
  */
12
- interface ReplaneProviderWithClientProps<T extends object = any> {
10
+ interface ReplaneProviderWithClientProps<T extends object = UntypedReplaneConfig> {
13
11
  /** Pre-created ReplaneClient instance */
14
12
  client: ReplaneClient<T>;
15
13
  children: ReactNode;
@@ -17,7 +15,7 @@ interface ReplaneProviderWithClientProps<T extends object = any> {
17
15
  /**
18
16
  * Props for ReplaneProvider when letting it manage the client internally.
19
17
  */
20
- interface ReplaneProviderWithOptionsProps<T extends object = any> {
18
+ interface ReplaneProviderWithOptionsProps<T extends object = UntypedReplaneConfig> {
21
19
  /** Options to create the ReplaneClient */
22
20
  options: ReplaneClientOptions<T>;
23
21
  children: ReactNode;
@@ -32,21 +30,27 @@ interface ReplaneProviderWithOptionsProps<T extends object = any> {
32
30
  * @default false
33
31
  */
34
32
  suspense?: boolean;
35
- /**
36
- * Callback when client initialization fails.
37
- */
38
- onError?: (error: Error) => void;
39
33
  }
40
- type ReplaneProviderProps<T extends object = any> = ReplaneProviderWithClientProps<T> | ReplaneProviderWithOptionsProps<T>;
34
+ /**
35
+ * Props for ReplaneProvider when restoring from a snapshot.
36
+ * Uses restoreReplaneClient from the SDK for synchronous client creation.
37
+ */
38
+ interface ReplaneProviderWithSnapshotProps<T extends object = UntypedReplaneConfig> {
39
+ /** Options to restore the ReplaneClient from a snapshot */
40
+ restoreOptions: RestoreReplaneClientOptions<T>;
41
+ children: ReactNode;
42
+ }
43
+ type ReplaneProviderProps<T extends object = UntypedReplaneConfig> = ReplaneProviderWithClientProps<T> | ReplaneProviderWithOptionsProps<T> | ReplaneProviderWithSnapshotProps<T>;
41
44
  /**
42
45
  * Type guard to check if props contain a pre-created client.
43
46
  */
47
+
44
48
  //#endregion
45
49
  //#region src/provider.d.ts
46
50
  /**
47
51
  * Provider component that makes a ReplaneClient available to the component tree.
48
52
  *
49
- * Can be used in two ways:
53
+ * Can be used in four ways:
50
54
  *
51
55
  * 1. With a pre-created client:
52
56
  * ```tsx
@@ -58,36 +62,94 @@ type ReplaneProviderProps<T extends object = any> = ReplaneProviderWithClientPro
58
62
  *
59
63
  * 2. With options (client managed internally):
60
64
  * ```tsx
61
- * <ReplaneProvider
62
- * options={{ baseUrl: '...', sdkKey: '...' }}
63
- * loader={<LoadingSpinner />}
64
- * >
65
- * <App />
66
- * </ReplaneProvider>
67
- * ```
68
- *
69
- * 3. With Suspense:
70
- * ```tsx
71
- * <Suspense fallback={<LoadingSpinner />}>
65
+ * <ErrorBoundary fallback={<ErrorMessage />}>
72
66
  * <ReplaneProvider
73
67
  * options={{ baseUrl: '...', sdkKey: '...' }}
74
- * suspense
68
+ * loader={<LoadingSpinner />}
75
69
  * >
76
70
  * <App />
77
71
  * </ReplaneProvider>
78
- * </Suspense>
72
+ * </ErrorBoundary>
79
73
  * ```
74
+ *
75
+ * 3. With Suspense:
76
+ * ```tsx
77
+ * <ErrorBoundary fallback={<ErrorMessage />}>
78
+ * <Suspense fallback={<LoadingSpinner />}>
79
+ * <ReplaneProvider
80
+ * options={{ baseUrl: '...', sdkKey: '...' }}
81
+ * suspense
82
+ * >
83
+ * <App />
84
+ * </ReplaneProvider>
85
+ * </Suspense>
86
+ * </ErrorBoundary>
87
+ * ```
88
+ *
89
+ * 4. With a snapshot (for SSR/hydration):
90
+ * ```tsx
91
+ * // On the server, get a snapshot from the client
92
+ * const snapshot = serverClient.getSnapshot();
93
+ *
94
+ * // On the client, restore from the snapshot
95
+ * <ReplaneProvider
96
+ * restoreOptions={{
97
+ * snapshot,
98
+ * connection: { baseUrl: '...', sdkKey: '...' } // optional, for live updates
99
+ * }}
100
+ * >
101
+ * <App />
102
+ * </ReplaneProvider>
103
+ * ```
104
+ *
105
+ * Errors during client initialization are thrown during rendering,
106
+ * allowing them to be caught by React Error Boundaries.
80
107
  */
81
108
  declare function ReplaneProvider<T extends object>(props: ReplaneProviderProps<T>): react_jsx_runtime0.JSX.Element;
82
109
  //# sourceMappingURL=provider.d.ts.map
83
110
  //#endregion
84
111
  //#region src/hooks.d.ts
85
- declare function useReplane<T extends object = Record<string, unknown>>(): ReplaneContextValue<T>;
86
- declare function useConfig<T>(name: string, options?: {
87
- context?: Record<string, string | number | boolean | null>;
88
- }): T;
112
+ declare function useReplane<T extends object = UntypedReplaneConfig>(): ReplaneClient<T>;
113
+ declare function useConfig<T>(name: string, options?: GetConfigOptions): T;
114
+ /**
115
+ * Creates a typed version of useReplane hook.
116
+ *
117
+ * @example
118
+ * ```tsx
119
+ * interface AppConfigs {
120
+ * theme: { darkMode: boolean };
121
+ * features: { beta: boolean };
122
+ * }
123
+ *
124
+ * const useAppReplane = createReplaneHook<AppConfigs>();
125
+ *
126
+ * function MyComponent() {
127
+ * const replane = useAppReplane();
128
+ * // replane.get("theme") returns { darkMode: boolean }
129
+ * }
130
+ * ```
131
+ */
132
+ declare function createReplaneHook<TConfigs extends object>(): () => ReplaneClient<TConfigs>;
133
+ /**
134
+ * Creates a typed version of useConfig hook.
135
+ *
136
+ * @example
137
+ * ```tsx
138
+ * interface AppConfigs {
139
+ * theme: { darkMode: boolean };
140
+ * features: { beta: boolean };
141
+ * }
142
+ *
143
+ * const useAppConfig = createConfigHook<AppConfigs>();
144
+ *
145
+ * function MyComponent() {
146
+ * const theme = useAppConfig("theme");
147
+ * // theme is typed as { darkMode: boolean }
148
+ * }
149
+ * ```
150
+ */
151
+ declare function createConfigHook<TConfigs extends object>(): <K extends keyof TConfigs>(name: K, options?: GetConfigOptions) => TConfigs[K];
89
152
  //# sourceMappingURL=hooks.d.ts.map
90
-
91
153
  //#endregion
92
154
  //#region src/useReplaneClient.d.ts
93
155
  /**
@@ -96,5 +158,5 @@ declare function useConfig<T>(name: string, options?: {
96
158
  */
97
159
  declare function clearSuspenseCache<T extends object>(options?: ReplaneClientOptions<T>): void;
98
160
  //#endregion
99
- export { type ReplaneContextValue, ReplaneProvider, type ReplaneProviderProps, type ReplaneProviderWithClientProps, type ReplaneProviderWithOptionsProps, clearSuspenseCache, useConfig, useReplane };
161
+ export { ReplaneProvider, type ReplaneProviderProps, type ReplaneProviderWithClientProps, type ReplaneProviderWithOptionsProps, type ReplaneProviderWithSnapshotProps, clearSuspenseCache, createConfigHook, createReplaneHook, useConfig, useReplane };
100
162
  //# sourceMappingURL=index.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../src/types.ts","../src/provider.tsx","../src/hooks.ts","../src/useReplaneClient.ts"],"sourcesContent":[],"mappings":";;;;;UAIiB;UACP,cAAc;;AADxB;;;AACU,UAOO,8BAPP,CAAA,UAAA,MAAA,GAAA,GAAA,CAAA,CAAA;EAAa;EAON,MAAA,EAEP,aAFO,CAEO,CAFP,CAAA;EAA8B,QAAA,EAGnC,SAHmC;;;;AAG1B;AAOJ,UAAA,+BAA+B,CAAA,UAAA,MAAA,GAAA,GAAA,CAAA,CAAA;EAAA;EAAA,OAEhB,EAArB,oBAAqB,CAAA,CAAA,CAAA;EAAC,QAAtB,EACC,SADD;EAAoB;;;AAgBN;EAIb,MAAA,CAAA,EAdD,SAcC;EAAoB;;;;;EAEG,QAAA,CAAA,EAAA,OAAA;;;;ECgDnB,OAAA,CAAA,EAAA,CAAA,KAAA,EDtDI,KCsDW,EAAA,GAAA,IAAA;;AAA+C,KDlDlE,oBCkDkE,CAAA,UAAA,MAAA,GAAA,GAAA,CAAA,GDjD1E,8BCiD0E,CDjD3C,CCiD2C,CAAA,GDhD1E,+BCgD0E,CDhD1C,CCgD0C,CAAA;;;AAAE;;;;;;;AD1FhF;;;;AACuB;AAOvB;;;;;AAGqB;AAOrB;;;;;;;AAkByB;AAIzB;;;;;;AAEmC;;;;ACgDnC;;AAA8E,iBAA9D,eAA8D,CAAA,UAAA,MAAA,CAAA,CAAA,KAAA,EAArB,oBAAqB,CAAA,CAAA,CAAA,CAAA,EAAE,kBAAA,CAAA,GAAA,CAAA,OAAF;;;;iBC1F9D,8BAA8B,4BAA4B,oBAAoB;iBAQ9E;YAEQ;IACrB;AFXH;;;;AAwCA;;;;AAEoC,iBGgFpB,kBHhFoB,CAAA,UAAA,MAAA,CAAA,CAAA,OAAA,CAAA,EGgF2B,oBHhF3B,CGgFgD,CHhFhD,CAAA,CAAA,EAAA,IAAA"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/types.ts","../src/provider.tsx","../src/hooks.ts","../src/useReplaneClient.ts"],"sourcesContent":[],"mappings":";;;;;KAOY,oBAAA,GAAuB;AASnC;;;AAEwB,UAFP,8BAEO,CAAA,UAAA,MAAA,GAF2C,oBAE3C,CAAA,CAAA;EAAC;EAAF,MACX,EADF,aACE,CADY,CACZ,CAAA;EAAS,QAAA,EAAT,SAAS;AAMrB;;;;AAEW,UAFM,+BAEN,CAAA,UAAA,MAAA,GAFyD,oBAEzD,CAAA,CAAA;EAAoB;EACV,OAKV,EANA,oBAMA,CANqB,CAMrB,CAAA;EAAS,QAAA,EALR,SAKQ;EAaH;;;;EAE8B,MAA7B,CAAA,EAfP,SAeO;EAA2B;AACxB;AAGrB;;;EAAwE,QACrC,CAAA,EAAA,OAAA;;;;;;AAEC,UATnB,gCASmB,CAAA,UAAA,MAAA,GATiC,oBASjC,CAAA,CAAA;;kBAPlB,4BAA4B;YAClC;ACkFZ;AAA+B,KD/EnB,oBC+EmB,CAAA,UAAA,MAAA,GD/EqB,oBC+ErB,CAAA,GD9E3B,8BC8E2B,CD9EI,CC8EJ,CAAA,GD7E3B,+BC6E2B,CD7EK,CC6EL,CAAA,GD5E3B,gCC4E2B,CD5EM,CC4EN,CAAA;;;;;;;;;;;AD5H/B;AASA;;;;;;AAGqB;AAMrB;;;;;;;AAQoB;AAapB;;;;;;AAGqB;AAGrB;;;;;;;;;AAGoC;;;;AC4EpC;;;;;AAAgF;;;;AC9HhF;;;;;AAAoF;AAQpF;;;AAAwE,iBDsHxD,eCtHwD,CAAA,UAAA,MAAA,CAAA,CAAA,KAAA,EDsHf,oBCtHe,CDsHM,CCtHN,CAAA,CAAA,EDsHQ,kBAAA,CAAA,GAAA,CAAA,OCtHR;AAAC;;;iBARzD,8BAA8B,yBAAyB,cAAc;iBAQrE,qCAAqC,mBAAmB;;AFNxE;AASA;;;;;;AAGqB;AAMrB;;;;;;;AAQoB;AAapB;AAAiD,iBEDjC,iBFCiC,CAAA,iBAAA,MAAA,CAAA,CAAA,CAAA,EAAA,GAAA,GEAZ,aFAY,CEAE,QFAF,CAAA;;;;;AAG5B;AAGrB;;;;;;;;;AAGoC;;;;AC4EpB,iBC9DA,gBD8De,CAAA,iBAAA,MAAA,CAAA,CAAA,CAAA,EAAA,CAAA,UAAA,MC7DkB,QD6DlB,CAAA,CAAA,IAAA,EC5DrB,CD4DqB,EAAA,OAAA,CAAA,EC3DjB,gBD2DiB,EAAA,GC1D1B,QD0D0B,CC1DjB,CD0DiB,CAAA;;;;;;;ADlFV;AAGT,iBGwEI,kBHxEgB,CAAA,UAAA,MAAA,CAAA,CAAA,OAAA,CAAA,EGwE+B,oBHxE/B,CGwEoD,CHxEpD,CAAA,CAAA,EAAA,IAAA"}
package/dist/index.d.ts CHANGED
@@ -1,15 +1,13 @@
1
1
  import { ReactNode } from "react";
2
- import { ReplaneClient, ReplaneClientOptions } from "@replanejs/sdk";
2
+ import { GetConfigOptions, ReplaneClient, ReplaneClientOptions, RestoreReplaneClientOptions } from "@replanejs/sdk";
3
3
  import * as react_jsx_runtime0 from "react/jsx-runtime";
4
4
 
5
5
  //#region src/types.d.ts
6
- interface ReplaneContextValue<T extends object = any> {
7
- client: ReplaneClient<T>;
8
- }
6
+ type UntypedReplaneConfig = Record<string, unknown>;
9
7
  /**
10
8
  * Props for ReplaneProvider when using a pre-created client.
11
9
  */
12
- interface ReplaneProviderWithClientProps<T extends object = any> {
10
+ interface ReplaneProviderWithClientProps<T extends object = UntypedReplaneConfig> {
13
11
  /** Pre-created ReplaneClient instance */
14
12
  client: ReplaneClient<T>;
15
13
  children: ReactNode;
@@ -17,7 +15,7 @@ interface ReplaneProviderWithClientProps<T extends object = any> {
17
15
  /**
18
16
  * Props for ReplaneProvider when letting it manage the client internally.
19
17
  */
20
- interface ReplaneProviderWithOptionsProps<T extends object = any> {
18
+ interface ReplaneProviderWithOptionsProps<T extends object = UntypedReplaneConfig> {
21
19
  /** Options to create the ReplaneClient */
22
20
  options: ReplaneClientOptions<T>;
23
21
  children: ReactNode;
@@ -32,21 +30,27 @@ interface ReplaneProviderWithOptionsProps<T extends object = any> {
32
30
  * @default false
33
31
  */
34
32
  suspense?: boolean;
35
- /**
36
- * Callback when client initialization fails.
37
- */
38
- onError?: (error: Error) => void;
39
33
  }
40
- type ReplaneProviderProps<T extends object = any> = ReplaneProviderWithClientProps<T> | ReplaneProviderWithOptionsProps<T>;
34
+ /**
35
+ * Props for ReplaneProvider when restoring from a snapshot.
36
+ * Uses restoreReplaneClient from the SDK for synchronous client creation.
37
+ */
38
+ interface ReplaneProviderWithSnapshotProps<T extends object = UntypedReplaneConfig> {
39
+ /** Options to restore the ReplaneClient from a snapshot */
40
+ restoreOptions: RestoreReplaneClientOptions<T>;
41
+ children: ReactNode;
42
+ }
43
+ type ReplaneProviderProps<T extends object = UntypedReplaneConfig> = ReplaneProviderWithClientProps<T> | ReplaneProviderWithOptionsProps<T> | ReplaneProviderWithSnapshotProps<T>;
41
44
  /**
42
45
  * Type guard to check if props contain a pre-created client.
43
46
  */
47
+
44
48
  //#endregion
45
49
  //#region src/provider.d.ts
46
50
  /**
47
51
  * Provider component that makes a ReplaneClient available to the component tree.
48
52
  *
49
- * Can be used in two ways:
53
+ * Can be used in four ways:
50
54
  *
51
55
  * 1. With a pre-created client:
52
56
  * ```tsx
@@ -58,36 +62,94 @@ type ReplaneProviderProps<T extends object = any> = ReplaneProviderWithClientPro
58
62
  *
59
63
  * 2. With options (client managed internally):
60
64
  * ```tsx
61
- * <ReplaneProvider
62
- * options={{ baseUrl: '...', sdkKey: '...' }}
63
- * loader={<LoadingSpinner />}
64
- * >
65
- * <App />
66
- * </ReplaneProvider>
67
- * ```
68
- *
69
- * 3. With Suspense:
70
- * ```tsx
71
- * <Suspense fallback={<LoadingSpinner />}>
65
+ * <ErrorBoundary fallback={<ErrorMessage />}>
72
66
  * <ReplaneProvider
73
67
  * options={{ baseUrl: '...', sdkKey: '...' }}
74
- * suspense
68
+ * loader={<LoadingSpinner />}
75
69
  * >
76
70
  * <App />
77
71
  * </ReplaneProvider>
78
- * </Suspense>
72
+ * </ErrorBoundary>
79
73
  * ```
74
+ *
75
+ * 3. With Suspense:
76
+ * ```tsx
77
+ * <ErrorBoundary fallback={<ErrorMessage />}>
78
+ * <Suspense fallback={<LoadingSpinner />}>
79
+ * <ReplaneProvider
80
+ * options={{ baseUrl: '...', sdkKey: '...' }}
81
+ * suspense
82
+ * >
83
+ * <App />
84
+ * </ReplaneProvider>
85
+ * </Suspense>
86
+ * </ErrorBoundary>
87
+ * ```
88
+ *
89
+ * 4. With a snapshot (for SSR/hydration):
90
+ * ```tsx
91
+ * // On the server, get a snapshot from the client
92
+ * const snapshot = serverClient.getSnapshot();
93
+ *
94
+ * // On the client, restore from the snapshot
95
+ * <ReplaneProvider
96
+ * restoreOptions={{
97
+ * snapshot,
98
+ * connection: { baseUrl: '...', sdkKey: '...' } // optional, for live updates
99
+ * }}
100
+ * >
101
+ * <App />
102
+ * </ReplaneProvider>
103
+ * ```
104
+ *
105
+ * Errors during client initialization are thrown during rendering,
106
+ * allowing them to be caught by React Error Boundaries.
80
107
  */
81
108
  declare function ReplaneProvider<T extends object>(props: ReplaneProviderProps<T>): react_jsx_runtime0.JSX.Element;
82
109
  //# sourceMappingURL=provider.d.ts.map
83
110
  //#endregion
84
111
  //#region src/hooks.d.ts
85
- declare function useReplane<T extends object = Record<string, unknown>>(): ReplaneContextValue<T>;
86
- declare function useConfig<T>(name: string, options?: {
87
- context?: Record<string, string | number | boolean | null>;
88
- }): T;
112
+ declare function useReplane<T extends object = UntypedReplaneConfig>(): ReplaneClient<T>;
113
+ declare function useConfig<T>(name: string, options?: GetConfigOptions): T;
114
+ /**
115
+ * Creates a typed version of useReplane hook.
116
+ *
117
+ * @example
118
+ * ```tsx
119
+ * interface AppConfigs {
120
+ * theme: { darkMode: boolean };
121
+ * features: { beta: boolean };
122
+ * }
123
+ *
124
+ * const useAppReplane = createReplaneHook<AppConfigs>();
125
+ *
126
+ * function MyComponent() {
127
+ * const replane = useAppReplane();
128
+ * // replane.get("theme") returns { darkMode: boolean }
129
+ * }
130
+ * ```
131
+ */
132
+ declare function createReplaneHook<TConfigs extends object>(): () => ReplaneClient<TConfigs>;
133
+ /**
134
+ * Creates a typed version of useConfig hook.
135
+ *
136
+ * @example
137
+ * ```tsx
138
+ * interface AppConfigs {
139
+ * theme: { darkMode: boolean };
140
+ * features: { beta: boolean };
141
+ * }
142
+ *
143
+ * const useAppConfig = createConfigHook<AppConfigs>();
144
+ *
145
+ * function MyComponent() {
146
+ * const theme = useAppConfig("theme");
147
+ * // theme is typed as { darkMode: boolean }
148
+ * }
149
+ * ```
150
+ */
151
+ declare function createConfigHook<TConfigs extends object>(): <K extends keyof TConfigs>(name: K, options?: GetConfigOptions) => TConfigs[K];
89
152
  //# sourceMappingURL=hooks.d.ts.map
90
-
91
153
  //#endregion
92
154
  //#region src/useReplaneClient.d.ts
93
155
  /**
@@ -96,5 +158,5 @@ declare function useConfig<T>(name: string, options?: {
96
158
  */
97
159
  declare function clearSuspenseCache<T extends object>(options?: ReplaneClientOptions<T>): void;
98
160
  //#endregion
99
- export { type ReplaneContextValue, ReplaneProvider, type ReplaneProviderProps, type ReplaneProviderWithClientProps, type ReplaneProviderWithOptionsProps, clearSuspenseCache, useConfig, useReplane };
161
+ export { ReplaneProvider, type ReplaneProviderProps, type ReplaneProviderWithClientProps, type ReplaneProviderWithOptionsProps, type ReplaneProviderWithSnapshotProps, clearSuspenseCache, createConfigHook, createReplaneHook, useConfig, useReplane };
100
162
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/provider.tsx","../src/hooks.ts","../src/useReplaneClient.ts"],"sourcesContent":[],"mappings":";;;;;UAIiB;UACP,cAAc;;AADxB;;;AACU,UAOO,8BAPP,CAAA,UAAA,MAAA,GAAA,GAAA,CAAA,CAAA;EAAa;EAON,MAAA,EAEP,aAFO,CAEO,CAFP,CAAA;EAA8B,QAAA,EAGnC,SAHmC;;;;AAG1B;AAOJ,UAAA,+BAA+B,CAAA,UAAA,MAAA,GAAA,GAAA,CAAA,CAAA;EAAA;EAAA,OAEhB,EAArB,oBAAqB,CAAA,CAAA,CAAA;EAAC,QAAtB,EACC,SADD;EAAoB;;;AAgBN;EAIb,MAAA,CAAA,EAdD,SAcC;EAAoB;;;;;EAEG,QAAA,CAAA,EAAA,OAAA;;;;ECgDnB,OAAA,CAAA,EAAA,CAAA,KAAA,EDtDI,KCsDW,EAAA,GAAA,IAAA;;AAA+C,KDlDlE,oBCkDkE,CAAA,UAAA,MAAA,GAAA,GAAA,CAAA,GDjD1E,8BCiD0E,CDjD3C,CCiD2C,CAAA,GDhD1E,+BCgD0E,CDhD1C,CCgD0C,CAAA;;;AAAE;;;;;;;AD1FhF;;;;AACuB;AAOvB;;;;;AAGqB;AAOrB;;;;;;;AAkByB;AAIzB;;;;;;AAEmC;;;;ACgDnC;;AAA8E,iBAA9D,eAA8D,CAAA,UAAA,MAAA,CAAA,CAAA,KAAA,EAArB,oBAAqB,CAAA,CAAA,CAAA,CAAA,EAAE,kBAAA,CAAA,GAAA,CAAA,OAAF;;;;iBC1F9D,8BAA8B,4BAA4B,oBAAoB;iBAQ9E;YAEQ;IACrB;AFXH;;;;AAwCA;;;;AAEoC,iBGgFpB,kBHhFoB,CAAA,UAAA,MAAA,CAAA,CAAA,OAAA,CAAA,EGgF2B,oBHhF3B,CGgFgD,CHhFhD,CAAA,CAAA,EAAA,IAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/provider.tsx","../src/hooks.ts","../src/useReplaneClient.ts"],"sourcesContent":[],"mappings":";;;;;KAOY,oBAAA,GAAuB;AASnC;;;AAEwB,UAFP,8BAEO,CAAA,UAAA,MAAA,GAF2C,oBAE3C,CAAA,CAAA;EAAC;EAAF,MACX,EADF,aACE,CADY,CACZ,CAAA;EAAS,QAAA,EAAT,SAAS;AAMrB;;;;AAEW,UAFM,+BAEN,CAAA,UAAA,MAAA,GAFyD,oBAEzD,CAAA,CAAA;EAAoB;EACV,OAKV,EANA,oBAMA,CANqB,CAMrB,CAAA;EAAS,QAAA,EALR,SAKQ;EAaH;;;;EAE8B,MAA7B,CAAA,EAfP,SAeO;EAA2B;AACxB;AAGrB;;;EAAwE,QACrC,CAAA,EAAA,OAAA;;;;;;AAEC,UATnB,gCASmB,CAAA,UAAA,MAAA,GATiC,oBASjC,CAAA,CAAA;;kBAPlB,4BAA4B;YAClC;ACkFZ;AAA+B,KD/EnB,oBC+EmB,CAAA,UAAA,MAAA,GD/EqB,oBC+ErB,CAAA,GD9E3B,8BC8E2B,CD9EI,CC8EJ,CAAA,GD7E3B,+BC6E2B,CD7EK,CC6EL,CAAA,GD5E3B,gCC4E2B,CD5EM,CC4EN,CAAA;;;;;;;;;;;AD5H/B;AASA;;;;;;AAGqB;AAMrB;;;;;;;AAQoB;AAapB;;;;;;AAGqB;AAGrB;;;;;;;;;AAGoC;;;;AC4EpC;;;;;AAAgF;;;;AC9HhF;;;;;AAAoF;AAQpF;;;AAAwE,iBDsHxD,eCtHwD,CAAA,UAAA,MAAA,CAAA,CAAA,KAAA,EDsHf,oBCtHe,CDsHM,CCtHN,CAAA,CAAA,EDsHQ,kBAAA,CAAA,GAAA,CAAA,OCtHR;AAAC;;;iBARzD,8BAA8B,yBAAyB,cAAc;iBAQrE,qCAAqC,mBAAmB;;AFNxE;AASA;;;;;;AAGqB;AAMrB;;;;;;;AAQoB;AAapB;AAAiD,iBEDjC,iBFCiC,CAAA,iBAAA,MAAA,CAAA,CAAA,CAAA,EAAA,GAAA,GEAZ,aFAY,CEAE,QFAF,CAAA;;;;;AAG5B;AAGrB;;;;;;;;;AAGoC;;;;AC4EpB,iBC9DA,gBD8De,CAAA,iBAAA,MAAA,CAAA,CAAA,CAAA,EAAA,CAAA,UAAA,MC7DkB,QD6DlB,CAAA,CAAA,IAAA,EC5DrB,CD4DqB,EAAA,OAAA,CAAA,EC3DjB,gBD2DiB,EAAA,GC1D1B,QD0D0B,CC1DjB,CD0DiB,CAAA;;;;;;;ADlFV;AAGT,iBGwEI,kBHxEgB,CAAA,UAAA,MAAA,CAAA,CAAA,OAAA,CAAA,EGwE+B,oBHxE/B,CGwEoD,CHxEpD,CAAA,CAAA,EAAA,IAAA"}
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createContext, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
2
- import { createReplaneClient } from "@replanejs/sdk";
2
+ import { createReplaneClient, restoreReplaneClient } from "@replanejs/sdk";
3
3
  import { Fragment, jsx } from "react/jsx-runtime";
4
4
 
5
5
  //#region src/context.ts
@@ -15,7 +15,7 @@ function getCacheKey(options) {
15
15
  * Hook to manage ReplaneClient creation internally.
16
16
  * Handles loading state and cleanup.
17
17
  */
18
- function useReplaneClient(options, onError) {
18
+ function useReplaneClientInternal(options) {
19
19
  const [state, setState] = useState({
20
20
  status: "loading",
21
21
  client: null,
@@ -40,13 +40,12 @@ function useReplaneClient(options, onError) {
40
40
  });
41
41
  } catch (err) {
42
42
  if (cancelled) return;
43
- const error = err instanceof Error ? err : new Error(String(err));
43
+ const error = err instanceof Error ? err : new Error(String(err), { cause: err });
44
44
  setState({
45
45
  status: "error",
46
46
  client: null,
47
47
  error
48
48
  });
49
- onError?.(error);
50
49
  }
51
50
  }
52
51
  initClient();
@@ -101,6 +100,12 @@ function clearSuspenseCache(options) {
101
100
  function hasClient(props) {
102
101
  return "client" in props && props.client !== void 0;
103
102
  }
103
+ /**
104
+ * Type guard to check if props contain restore options.
105
+ */
106
+ function hasRestoreOptions(props) {
107
+ return "restoreOptions" in props && props.restoreOptions !== void 0;
108
+ }
104
109
 
105
110
  //#endregion
106
111
  //#region src/provider.tsx
@@ -116,11 +121,12 @@ function ReplaneProviderWithClient({ client, children }) {
116
121
  }
117
122
  /**
118
123
  * Internal provider component for options-based client creation (non-suspense).
124
+ * Throws errors during rendering so they can be caught by Error Boundaries.
119
125
  */
120
- function ReplaneProviderWithOptions({ options, children, loader, onError }) {
121
- const state = useReplaneClient(options, onError);
126
+ function ReplaneProviderWithOptions({ options, children, loader }) {
127
+ const state = useReplaneClientInternal(options);
122
128
  if (state.status === "loading") return /* @__PURE__ */ jsx(Fragment, { children: loader ?? null });
123
- if (state.status === "error") return /* @__PURE__ */ jsx(Fragment, { children: loader ?? null });
129
+ if (state.status === "error") throw state.error;
124
130
  const value = { client: state.client };
125
131
  return /* @__PURE__ */ jsx(ReplaneContext.Provider, {
126
132
  value,
@@ -139,9 +145,21 @@ function ReplaneProviderWithSuspense({ options, children }) {
139
145
  });
140
146
  }
141
147
  /**
148
+ * Internal provider component for restoring client from snapshot.
149
+ * Uses restoreReplaneClient which is synchronous.
150
+ */
151
+ function ReplaneProviderWithSnapshot({ restoreOptions, children }) {
152
+ const client = useMemo(() => restoreReplaneClient(restoreOptions), [restoreOptions]);
153
+ const value = useMemo(() => ({ client }), [client]);
154
+ return /* @__PURE__ */ jsx(ReplaneContext.Provider, {
155
+ value,
156
+ children
157
+ });
158
+ }
159
+ /**
142
160
  * Provider component that makes a ReplaneClient available to the component tree.
143
161
  *
144
- * Can be used in two ways:
162
+ * Can be used in four ways:
145
163
  *
146
164
  * 1. With a pre-created client:
147
165
  * ```tsx
@@ -153,28 +171,52 @@ function ReplaneProviderWithSuspense({ options, children }) {
153
171
  *
154
172
  * 2. With options (client managed internally):
155
173
  * ```tsx
156
- * <ReplaneProvider
157
- * options={{ baseUrl: '...', sdkKey: '...' }}
158
- * loader={<LoadingSpinner />}
159
- * >
160
- * <App />
161
- * </ReplaneProvider>
162
- * ```
163
- *
164
- * 3. With Suspense:
165
- * ```tsx
166
- * <Suspense fallback={<LoadingSpinner />}>
174
+ * <ErrorBoundary fallback={<ErrorMessage />}>
167
175
  * <ReplaneProvider
168
176
  * options={{ baseUrl: '...', sdkKey: '...' }}
169
- * suspense
177
+ * loader={<LoadingSpinner />}
170
178
  * >
171
179
  * <App />
172
180
  * </ReplaneProvider>
173
- * </Suspense>
181
+ * </ErrorBoundary>
182
+ * ```
183
+ *
184
+ * 3. With Suspense:
185
+ * ```tsx
186
+ * <ErrorBoundary fallback={<ErrorMessage />}>
187
+ * <Suspense fallback={<LoadingSpinner />}>
188
+ * <ReplaneProvider
189
+ * options={{ baseUrl: '...', sdkKey: '...' }}
190
+ * suspense
191
+ * >
192
+ * <App />
193
+ * </ReplaneProvider>
194
+ * </Suspense>
195
+ * </ErrorBoundary>
196
+ * ```
197
+ *
198
+ * 4. With a snapshot (for SSR/hydration):
199
+ * ```tsx
200
+ * // On the server, get a snapshot from the client
201
+ * const snapshot = serverClient.getSnapshot();
202
+ *
203
+ * // On the client, restore from the snapshot
204
+ * <ReplaneProvider
205
+ * restoreOptions={{
206
+ * snapshot,
207
+ * connection: { baseUrl: '...', sdkKey: '...' } // optional, for live updates
208
+ * }}
209
+ * >
210
+ * <App />
211
+ * </ReplaneProvider>
174
212
  * ```
213
+ *
214
+ * Errors during client initialization are thrown during rendering,
215
+ * allowing them to be caught by React Error Boundaries.
175
216
  */
176
217
  function ReplaneProvider(props) {
177
218
  if (hasClient(props)) return /* @__PURE__ */ jsx(ReplaneProviderWithClient, { ...props });
219
+ if (hasRestoreOptions(props)) return /* @__PURE__ */ jsx(ReplaneProviderWithSnapshot, { ...props });
178
220
  if (props.suspense) return /* @__PURE__ */ jsx(ReplaneProviderWithSuspense, { ...props });
179
221
  return /* @__PURE__ */ jsx(ReplaneProviderWithOptions, { ...props });
180
222
  }
@@ -184,16 +226,62 @@ function ReplaneProvider(props) {
184
226
  function useReplane() {
185
227
  const context = useContext(ReplaneContext);
186
228
  if (!context) throw new Error("useReplane must be used within a ReplaneProvider");
187
- return context;
229
+ return context.client;
188
230
  }
189
231
  function useConfig(name, options) {
190
- const { client } = useReplane();
232
+ const client = useReplane();
191
233
  const value = useSyncExternalStore((onStoreChange) => {
192
234
  return client.subscribe(name, onStoreChange);
193
235
  }, () => client.get(name, options), () => client.get(name, options));
194
236
  return value;
195
237
  }
238
+ /**
239
+ * Creates a typed version of useReplane hook.
240
+ *
241
+ * @example
242
+ * ```tsx
243
+ * interface AppConfigs {
244
+ * theme: { darkMode: boolean };
245
+ * features: { beta: boolean };
246
+ * }
247
+ *
248
+ * const useAppReplane = createReplaneHook<AppConfigs>();
249
+ *
250
+ * function MyComponent() {
251
+ * const replane = useAppReplane();
252
+ * // replane.get("theme") returns { darkMode: boolean }
253
+ * }
254
+ * ```
255
+ */
256
+ function createReplaneHook() {
257
+ return function useTypedReplane() {
258
+ return useReplane();
259
+ };
260
+ }
261
+ /**
262
+ * Creates a typed version of useConfig hook.
263
+ *
264
+ * @example
265
+ * ```tsx
266
+ * interface AppConfigs {
267
+ * theme: { darkMode: boolean };
268
+ * features: { beta: boolean };
269
+ * }
270
+ *
271
+ * const useAppConfig = createConfigHook<AppConfigs>();
272
+ *
273
+ * function MyComponent() {
274
+ * const theme = useAppConfig("theme");
275
+ * // theme is typed as { darkMode: boolean }
276
+ * }
277
+ * ```
278
+ */
279
+ function createConfigHook() {
280
+ return function useTypedConfig(name, options) {
281
+ return useConfig(String(name), options);
282
+ };
283
+ }
196
284
 
197
285
  //#endregion
198
- export { ReplaneProvider, clearSuspenseCache, useConfig, useReplane };
286
+ export { ReplaneProvider, clearSuspenseCache, createConfigHook, createReplaneHook, useConfig, useReplane };
199
287
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["options: ReplaneClientOptions<T>","onError?: (error: Error) => void","options?: ReplaneClientOptions<T>","props: ReplaneProviderProps<T>","value: ReplaneContextValue<T>","props: ReplaneProviderProps<T>","name: string","options?: { context?: Record<string, string | number | boolean | null> }"],"sources":["../src/context.ts","../src/useReplaneClient.ts","../src/types.ts","../src/provider.tsx","../src/hooks.ts"],"sourcesContent":["import { createContext } from \"react\";\nimport type { ReplaneContextValue } from \"./types\";\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const ReplaneContext = createContext<ReplaneContextValue<any> | null>(null);\n","import { useEffect, useRef, useState } from \"react\";\nimport { createReplaneClient } from \"@replanejs/sdk\";\nimport type { ReplaneClient, ReplaneClientOptions } from \"@replanejs/sdk\";\n\ntype ClientState<T extends object> =\n | { status: \"loading\"; client: null; error: null }\n | { status: \"ready\"; client: ReplaneClient<T>; error: null }\n | { status: \"error\"; client: null; error: Error };\n\n// Cache for suspense promise tracking\nconst suspenseCache = new Map<\n string,\n {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n promise: Promise<ReplaneClient<any>>;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n result?: ReplaneClient<any>;\n error?: Error;\n }\n>();\n\nfunction getCacheKey<T extends object>(options: ReplaneClientOptions<T>): string {\n return `${options.baseUrl}:${options.sdkKey}`;\n}\n\n/**\n * Hook to manage ReplaneClient creation internally.\n * Handles loading state and cleanup.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function useReplaneClient<T extends object = any>(\n options: ReplaneClientOptions<T>,\n onError?: (error: Error) => void\n): ClientState<T> {\n const [state, setState] = useState<ClientState<T>>({\n status: \"loading\",\n client: null,\n error: null,\n });\n const clientRef = useRef<ReplaneClient<T> | null>(null);\n const optionsRef = useRef(options);\n\n useEffect(() => {\n let cancelled = false;\n\n async function initClient() {\n try {\n const client = await createReplaneClient<T>(optionsRef.current);\n if (cancelled) {\n client.close();\n return;\n }\n clientRef.current = client;\n setState({ status: \"ready\", client, error: null });\n } catch (err) {\n if (cancelled) return;\n const error = err instanceof Error ? err : new Error(String(err));\n setState({ status: \"error\", client: null, error });\n onError?.(error);\n }\n }\n\n initClient();\n\n return () => {\n cancelled = true;\n if (clientRef.current) {\n clientRef.current.close();\n clientRef.current = null;\n }\n };\n // We intentionally only run this effect once on mount\n // Options changes would require remounting the provider\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return state;\n}\n\n/**\n * Hook for Suspense-based client creation.\n * Throws a promise while loading, throws error on failure.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function useReplaneClientSuspense<T extends object = any>(\n options: ReplaneClientOptions<T>\n): ReplaneClient<T> {\n const cacheKey = getCacheKey(options);\n const cached = suspenseCache.get(cacheKey);\n\n if (cached) {\n if (cached.error) {\n throw cached.error;\n }\n if (cached.result) {\n return cached.result as ReplaneClient<T>;\n }\n // Still loading, throw the promise\n throw cached.promise;\n }\n\n // First time - create the promise\n const promise = createReplaneClient<T>(options)\n .then((client) => {\n const entry = suspenseCache.get(cacheKey);\n if (entry) {\n entry.result = client;\n }\n return client;\n })\n .catch((err) => {\n const entry = suspenseCache.get(cacheKey);\n if (entry) {\n entry.error = err instanceof Error ? err : new Error(String(err));\n }\n throw err;\n });\n\n suspenseCache.set(cacheKey, { promise });\n throw promise;\n}\n\n/**\n * Clear the suspense cache for a specific options configuration.\n * Useful for testing or when you need to force re-initialization.\n */\nexport function clearSuspenseCache<T extends object>(options?: ReplaneClientOptions<T>): void {\n if (options) {\n suspenseCache.delete(getCacheKey(options));\n } else {\n suspenseCache.clear();\n }\n}\n","import type { ReplaneClient, ReplaneClientOptions } from \"@replanejs/sdk\";\nimport type { ReactNode } from \"react\";\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport interface ReplaneContextValue<T extends object = any> {\n client: ReplaneClient<T>;\n}\n\n/**\n * Props for ReplaneProvider when using a pre-created client.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport interface ReplaneProviderWithClientProps<T extends object = any> {\n /** Pre-created ReplaneClient instance */\n client: ReplaneClient<T>;\n children: ReactNode;\n}\n\n/**\n * Props for ReplaneProvider when letting it manage the client internally.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport interface ReplaneProviderWithOptionsProps<T extends object = any> {\n /** Options to create the ReplaneClient */\n options: ReplaneClientOptions<T>;\n children: ReactNode;\n /**\n * Optional loading component to show while the client is initializing.\n * If not provided and suspense is false/undefined, children will not render until ready.\n */\n loader?: ReactNode;\n /**\n * If true, uses React Suspense for loading state.\n * The provider will throw a promise that Suspense can catch.\n * @default false\n */\n suspense?: boolean;\n /**\n * Callback when client initialization fails.\n */\n onError?: (error: Error) => void;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type ReplaneProviderProps<T extends object = any> =\n | ReplaneProviderWithClientProps<T>\n | ReplaneProviderWithOptionsProps<T>;\n\n/**\n * Type guard to check if props contain a pre-created client.\n */\nexport function hasClient<T extends object>(\n props: ReplaneProviderProps<T>\n): props is ReplaneProviderWithClientProps<T> {\n return \"client\" in props && props.client !== undefined;\n}\n","import { useMemo } from \"react\";\nimport { ReplaneContext } from \"./context\";\nimport { useReplaneClient, useReplaneClientSuspense } from \"./useReplaneClient\";\nimport type {\n ReplaneProviderProps,\n ReplaneProviderWithClientProps,\n ReplaneProviderWithOptionsProps,\n ReplaneContextValue,\n} from \"./types\";\nimport { hasClient } from \"./types\";\n\n/**\n * Internal provider component for pre-created client.\n */\nfunction ReplaneProviderWithClient<T extends object>({\n client,\n children,\n}: ReplaneProviderWithClientProps<T>) {\n const value = useMemo<ReplaneContextValue<T>>(() => ({ client }), [client]);\n return <ReplaneContext.Provider value={value}>{children}</ReplaneContext.Provider>;\n}\n\n/**\n * Internal provider component for options-based client creation (non-suspense).\n */\nfunction ReplaneProviderWithOptions<T extends object>({\n options,\n children,\n loader,\n onError,\n}: ReplaneProviderWithOptionsProps<T>) {\n const state = useReplaneClient<T>(options, onError);\n\n if (state.status === \"loading\") {\n return <>{loader ?? null}</>;\n }\n\n if (state.status === \"error\") {\n // Error was already reported via onError callback\n // Return loader or null to prevent rendering children without a client\n return <>{loader ?? null}</>;\n }\n\n const value: ReplaneContextValue<T> = { client: state.client };\n return <ReplaneContext.Provider value={value}>{children}</ReplaneContext.Provider>;\n}\n\n/**\n * Internal provider component for options-based client creation with Suspense.\n */\nfunction ReplaneProviderWithSuspense<T extends object>({\n options,\n children,\n}: ReplaneProviderWithOptionsProps<T>) {\n const client = useReplaneClientSuspense<T>(options);\n const value = useMemo<ReplaneContextValue<T>>(() => ({ client }), [client]);\n return <ReplaneContext.Provider value={value}>{children}</ReplaneContext.Provider>;\n}\n\n/**\n * Provider component that makes a ReplaneClient available to the component tree.\n *\n * Can be used in two ways:\n *\n * 1. With a pre-created client:\n * ```tsx\n * const client = await createReplaneClient({ ... });\n * <ReplaneProvider client={client}>\n * <App />\n * </ReplaneProvider>\n * ```\n *\n * 2. With options (client managed internally):\n * ```tsx\n * <ReplaneProvider\n * options={{ baseUrl: '...', sdkKey: '...' }}\n * loader={<LoadingSpinner />}\n * >\n * <App />\n * </ReplaneProvider>\n * ```\n *\n * 3. With Suspense:\n * ```tsx\n * <Suspense fallback={<LoadingSpinner />}>\n * <ReplaneProvider\n * options={{ baseUrl: '...', sdkKey: '...' }}\n * suspense\n * >\n * <App />\n * </ReplaneProvider>\n * </Suspense>\n * ```\n */\nexport function ReplaneProvider<T extends object>(props: ReplaneProviderProps<T>) {\n if (hasClient(props)) {\n return <ReplaneProviderWithClient {...props} />;\n }\n\n if (props.suspense) {\n return <ReplaneProviderWithSuspense {...props} />;\n }\n\n return <ReplaneProviderWithOptions {...props} />;\n}\n","import { useContext, useSyncExternalStore } from \"react\";\nimport { ReplaneContext } from \"./context\";\nimport type { ReplaneContextValue } from \"./types\";\n\nexport function useReplane<T extends object = Record<string, unknown>>(): ReplaneContextValue<T> {\n const context = useContext(ReplaneContext);\n if (!context) {\n throw new Error(\"useReplane must be used within a ReplaneProvider\");\n }\n return context as ReplaneContextValue<T>;\n}\n\nexport function useConfig<T>(\n name: string,\n options?: { context?: Record<string, string | number | boolean | null> }\n): T {\n const { client } = useReplane();\n\n const value = useSyncExternalStore(\n (onStoreChange) => {\n return client.subscribe(name, onStoreChange);\n },\n () => client.get(name, options) as T,\n () => client.get(name, options) as T\n );\n\n return value;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["options: ReplaneClientOptions<T>","options?: ReplaneClientOptions<T>","props: ReplaneProviderProps<T>","value: ReplaneContextValue<T>","props: ReplaneProviderProps<T>","name: string","options?: GetConfigOptions","name: K"],"sources":["../src/context.ts","../src/useReplaneClient.ts","../src/types.ts","../src/provider.tsx","../src/hooks.ts"],"sourcesContent":["import { createContext } from \"react\";\nimport type { ReplaneContextValue } from \"./types\";\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const ReplaneContext = createContext<ReplaneContextValue<any> | null>(null);\n","import { useEffect, useRef, useState } from \"react\";\nimport { createReplaneClient } from \"@replanejs/sdk\";\nimport type { ReplaneClient, ReplaneClientOptions } from \"@replanejs/sdk\";\n\ntype ClientState<T extends object> =\n | { status: \"loading\"; client: null; error: null }\n | { status: \"ready\"; client: ReplaneClient<T>; error: null }\n | { status: \"error\"; client: null; error: Error };\n\n// Cache for suspense promise tracking\nconst suspenseCache = new Map<\n string,\n {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n promise: Promise<ReplaneClient<any>>;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n result?: ReplaneClient<any>;\n error?: Error;\n }\n>();\n\nfunction getCacheKey<T extends object>(options: ReplaneClientOptions<T>): string {\n return `${options.baseUrl}:${options.sdkKey}`;\n}\n\ntype ErrorConstructor = new (message: string, options?: { cause?: unknown }) => Error;\n\n/**\n * Hook to manage ReplaneClient creation internally.\n * Handles loading state and cleanup.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function useReplaneClientInternal<T extends object = any>(\n options: ReplaneClientOptions<T>\n): ClientState<T> {\n const [state, setState] = useState<ClientState<T>>({\n status: \"loading\",\n client: null,\n error: null,\n });\n const clientRef = useRef<ReplaneClient<T> | null>(null);\n const optionsRef = useRef(options);\n\n useEffect(() => {\n let cancelled = false;\n\n async function initClient() {\n try {\n const client = await createReplaneClient<T>(optionsRef.current);\n if (cancelled) {\n client.close();\n return;\n }\n clientRef.current = client;\n setState({ status: \"ready\", client, error: null });\n } catch (err) {\n if (cancelled) return;\n const error =\n err instanceof Error ? err : new (Error as ErrorConstructor)(String(err), { cause: err });\n setState({ status: \"error\", client: null, error });\n }\n }\n\n initClient();\n\n return () => {\n cancelled = true;\n if (clientRef.current) {\n clientRef.current.close();\n clientRef.current = null;\n }\n };\n }, []);\n\n return state;\n}\n\n/**\n * Hook for Suspense-based client creation.\n * Throws a promise while loading, throws error on failure.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function useReplaneClientSuspense<T extends object = any>(\n options: ReplaneClientOptions<T>\n): ReplaneClient<T> {\n const cacheKey = getCacheKey(options);\n const cached = suspenseCache.get(cacheKey);\n\n if (cached) {\n if (cached.error) {\n throw cached.error;\n }\n if (cached.result) {\n return cached.result as ReplaneClient<T>;\n }\n // Still loading, throw the promise\n throw cached.promise;\n }\n\n // First time - create the promise\n const promise = createReplaneClient<T>(options)\n .then((client) => {\n const entry = suspenseCache.get(cacheKey);\n if (entry) {\n entry.result = client;\n }\n return client;\n })\n .catch((err) => {\n const entry = suspenseCache.get(cacheKey);\n if (entry) {\n entry.error = err instanceof Error ? err : new Error(String(err));\n }\n throw err;\n });\n\n suspenseCache.set(cacheKey, { promise });\n throw promise;\n}\n\n/**\n * Clear the suspense cache for a specific options configuration.\n * Useful for testing or when you need to force re-initialization.\n */\nexport function clearSuspenseCache<T extends object>(options?: ReplaneClientOptions<T>): void {\n if (options) {\n suspenseCache.delete(getCacheKey(options));\n } else {\n suspenseCache.clear();\n }\n}\n","import type {\n ReplaneClient,\n ReplaneClientOptions,\n RestoreReplaneClientOptions,\n} from \"@replanejs/sdk\";\nimport type { ReactNode } from \"react\";\n\nexport type UntypedReplaneConfig = Record<string, unknown>;\n\nexport interface ReplaneContextValue<T extends object = UntypedReplaneConfig> {\n client: ReplaneClient<T>;\n}\n\n/**\n * Props for ReplaneProvider when using a pre-created client.\n */\nexport interface ReplaneProviderWithClientProps<T extends object = UntypedReplaneConfig> {\n /** Pre-created ReplaneClient instance */\n client: ReplaneClient<T>;\n children: ReactNode;\n}\n\n/**\n * Props for ReplaneProvider when letting it manage the client internally.\n */\nexport interface ReplaneProviderWithOptionsProps<T extends object = UntypedReplaneConfig> {\n /** Options to create the ReplaneClient */\n options: ReplaneClientOptions<T>;\n children: ReactNode;\n /**\n * Optional loading component to show while the client is initializing.\n * If not provided and suspense is false/undefined, children will not render until ready.\n */\n loader?: ReactNode;\n /**\n * If true, uses React Suspense for loading state.\n * The provider will throw a promise that Suspense can catch.\n * @default false\n */\n suspense?: boolean;\n}\n\n/**\n * Props for ReplaneProvider when restoring from a snapshot.\n * Uses restoreReplaneClient from the SDK for synchronous client creation.\n */\nexport interface ReplaneProviderWithSnapshotProps<T extends object = UntypedReplaneConfig> {\n /** Options to restore the ReplaneClient from a snapshot */\n restoreOptions: RestoreReplaneClientOptions<T>;\n children: ReactNode;\n}\n\nexport type ReplaneProviderProps<T extends object = UntypedReplaneConfig> =\n | ReplaneProviderWithClientProps<T>\n | ReplaneProviderWithOptionsProps<T>\n | ReplaneProviderWithSnapshotProps<T>;\n\n/**\n * Type guard to check if props contain a pre-created client.\n */\nexport function hasClient<T extends object>(\n props: ReplaneProviderProps<T>\n): props is ReplaneProviderWithClientProps<T> {\n return \"client\" in props && props.client !== undefined;\n}\n\n/**\n * Type guard to check if props contain restore options.\n */\nexport function hasRestoreOptions<T extends object>(\n props: ReplaneProviderProps<T>\n): props is ReplaneProviderWithSnapshotProps<T> {\n return \"restoreOptions\" in props && props.restoreOptions !== undefined;\n}\n","import { useMemo } from \"react\";\nimport { restoreReplaneClient } from \"@replanejs/sdk\";\nimport { ReplaneContext } from \"./context\";\nimport { useReplaneClientInternal, useReplaneClientSuspense } from \"./useReplaneClient\";\nimport type {\n ReplaneProviderProps,\n ReplaneProviderWithClientProps,\n ReplaneProviderWithOptionsProps,\n ReplaneProviderWithSnapshotProps,\n ReplaneContextValue,\n} from \"./types\";\nimport { hasClient, hasRestoreOptions } from \"./types\";\n\n/**\n * Internal provider component for pre-created client.\n */\nfunction ReplaneProviderWithClient<T extends object>({\n client,\n children,\n}: ReplaneProviderWithClientProps<T>) {\n const value = useMemo<ReplaneContextValue<T>>(() => ({ client }), [client]);\n return <ReplaneContext.Provider value={value}>{children}</ReplaneContext.Provider>;\n}\n\n/**\n * Internal provider component for options-based client creation (non-suspense).\n * Throws errors during rendering so they can be caught by Error Boundaries.\n */\nfunction ReplaneProviderWithOptions<T extends object>({\n options,\n children,\n loader,\n}: ReplaneProviderWithOptionsProps<T>) {\n const state = useReplaneClientInternal<T>(options);\n\n if (state.status === \"loading\") {\n return <>{loader ?? null}</>;\n }\n\n if (state.status === \"error\") {\n // Throw error during render so it can be caught by Error Boundary\n throw state.error;\n }\n\n const value: ReplaneContextValue<T> = { client: state.client };\n return <ReplaneContext.Provider value={value}>{children}</ReplaneContext.Provider>;\n}\n\n/**\n * Internal provider component for options-based client creation with Suspense.\n */\nfunction ReplaneProviderWithSuspense<T extends object>({\n options,\n children,\n}: ReplaneProviderWithOptionsProps<T>) {\n const client = useReplaneClientSuspense<T>(options);\n const value = useMemo<ReplaneContextValue<T>>(() => ({ client }), [client]);\n return <ReplaneContext.Provider value={value}>{children}</ReplaneContext.Provider>;\n}\n\n/**\n * Internal provider component for restoring client from snapshot.\n * Uses restoreReplaneClient which is synchronous.\n */\nfunction ReplaneProviderWithSnapshot<T extends object>({\n restoreOptions,\n children,\n}: ReplaneProviderWithSnapshotProps<T>) {\n const client = useMemo(() => restoreReplaneClient<T>(restoreOptions), [restoreOptions]);\n const value = useMemo<ReplaneContextValue<T>>(() => ({ client }), [client]);\n return <ReplaneContext.Provider value={value}>{children}</ReplaneContext.Provider>;\n}\n\n/**\n * Provider component that makes a ReplaneClient available to the component tree.\n *\n * Can be used in four ways:\n *\n * 1. With a pre-created client:\n * ```tsx\n * const client = await createReplaneClient({ ... });\n * <ReplaneProvider client={client}>\n * <App />\n * </ReplaneProvider>\n * ```\n *\n * 2. With options (client managed internally):\n * ```tsx\n * <ErrorBoundary fallback={<ErrorMessage />}>\n * <ReplaneProvider\n * options={{ baseUrl: '...', sdkKey: '...' }}\n * loader={<LoadingSpinner />}\n * >\n * <App />\n * </ReplaneProvider>\n * </ErrorBoundary>\n * ```\n *\n * 3. With Suspense:\n * ```tsx\n * <ErrorBoundary fallback={<ErrorMessage />}>\n * <Suspense fallback={<LoadingSpinner />}>\n * <ReplaneProvider\n * options={{ baseUrl: '...', sdkKey: '...' }}\n * suspense\n * >\n * <App />\n * </ReplaneProvider>\n * </Suspense>\n * </ErrorBoundary>\n * ```\n *\n * 4. With a snapshot (for SSR/hydration):\n * ```tsx\n * // On the server, get a snapshot from the client\n * const snapshot = serverClient.getSnapshot();\n *\n * // On the client, restore from the snapshot\n * <ReplaneProvider\n * restoreOptions={{\n * snapshot,\n * connection: { baseUrl: '...', sdkKey: '...' } // optional, for live updates\n * }}\n * >\n * <App />\n * </ReplaneProvider>\n * ```\n *\n * Errors during client initialization are thrown during rendering,\n * allowing them to be caught by React Error Boundaries.\n */\nexport function ReplaneProvider<T extends object>(props: ReplaneProviderProps<T>) {\n if (hasClient(props)) {\n return <ReplaneProviderWithClient {...props} />;\n }\n\n if (hasRestoreOptions(props)) {\n return <ReplaneProviderWithSnapshot {...props} />;\n }\n\n if (props.suspense) {\n return <ReplaneProviderWithSuspense {...props} />;\n }\n\n return <ReplaneProviderWithOptions {...props} />;\n}\n","import { useContext, useSyncExternalStore } from \"react\";\nimport { ReplaneContext } from \"./context\";\nimport type { UntypedReplaneConfig } from \"./types\";\nimport type { ReplaneClient, GetConfigOptions } from \"@replanejs/sdk\";\n\nexport function useReplane<T extends object = UntypedReplaneConfig>(): ReplaneClient<T> {\n const context = useContext(ReplaneContext);\n if (!context) {\n throw new Error(\"useReplane must be used within a ReplaneProvider\");\n }\n return context.client as ReplaneClient<T>;\n}\n\nexport function useConfig<T>(name: string, options?: GetConfigOptions): T {\n const client = useReplane();\n\n const value = useSyncExternalStore(\n (onStoreChange) => {\n return client.subscribe(name, onStoreChange);\n },\n () => client.get(name, options) as T,\n () => client.get(name, options) as T\n );\n\n return value;\n}\n\n/**\n * Creates a typed version of useReplane hook.\n *\n * @example\n * ```tsx\n * interface AppConfigs {\n * theme: { darkMode: boolean };\n * features: { beta: boolean };\n * }\n *\n * const useAppReplane = createReplaneHook<AppConfigs>();\n *\n * function MyComponent() {\n * const replane = useAppReplane();\n * // replane.get(\"theme\") returns { darkMode: boolean }\n * }\n * ```\n */\nexport function createReplaneHook<TConfigs extends object>() {\n return function useTypedReplane(): ReplaneClient<TConfigs> {\n return useReplane<TConfigs>();\n };\n}\n\n/**\n * Creates a typed version of useConfig hook.\n *\n * @example\n * ```tsx\n * interface AppConfigs {\n * theme: { darkMode: boolean };\n * features: { beta: boolean };\n * }\n *\n * const useAppConfig = createConfigHook<AppConfigs>();\n *\n * function MyComponent() {\n * const theme = useAppConfig(\"theme\");\n * // theme is typed as { darkMode: boolean }\n * }\n * ```\n */\nexport function createConfigHook<TConfigs extends object>() {\n return function useTypedConfig<K extends keyof TConfigs>(\n name: K,\n options?: GetConfigOptions\n ): TConfigs[K] {\n return useConfig<TConfigs[K]>(String(name), options);\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@replanejs/react",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "description": "React SDK for Replane - feature flags and remote configuration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -40,7 +40,7 @@
40
40
  "react": ">=18.0.0"
41
41
  },
42
42
  "dependencies": {
43
- "@replanejs/sdk": "^0.7.2"
43
+ "@replanejs/sdk": "^0.7.4"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@testing-library/jest-dom": "^6.9.1",