@archie/react-sdk 0.1.0

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 ADDED
@@ -0,0 +1,792 @@
1
+ # @archie/react-sdk
2
+
3
+ React hooks, provider, and components for the [Archie BAAS](https://archie.dev) platform.
4
+
5
+ Built on top of [`@archie/js-sdk`](../js-sdk), this package provides a fully reactive React integration with auth state management, declarative data fetching, real-time subscriptions, file uploads with progress tracking, and route guards.
6
+
7
+ ---
8
+
9
+ ## Table of Contents
10
+
11
+ - [Installation](#installation)
12
+ - [Quick Start](#quick-start)
13
+ - [ArchieProvider](#archieprovider)
14
+ - [Hooks](#hooks)
15
+ - [useArchieClient](#usearchieclient)
16
+ - [useAuth](#useauth)
17
+ - [useQuery](#usequery)
18
+ - [useMutation](#usemutation)
19
+ - [useSubscription](#usesubscription)
20
+ - [useFileUpload](#usefileupload)
21
+ - [useRest](#userest)
22
+ - [useRestQuery](#userestquery)
23
+ - [useRestMutation](#userestmutation)
24
+ - [Components](#components)
25
+ - [AuthGuard](#authguard)
26
+ - [Re-exported Types](#re-exported-types)
27
+ - [Error Handling](#error-handling)
28
+ - [SSR Compatibility](#ssr-compatibility)
29
+ - [TypeScript](#typescript)
30
+ - [License](#license)
31
+
32
+ ---
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ npm install @archie/js-sdk @archie/react-sdk
38
+ # or
39
+ pnpm add @archie/js-sdk @archie/react-sdk
40
+ ```
41
+
42
+ **Peer dependencies:** `react >=18`, `@archie/js-sdk`
43
+
44
+ ---
45
+
46
+ ## Quick Start
47
+
48
+ ```tsx
49
+ import { createClient } from '@archie/js-sdk';
50
+ import { ArchieProvider, useAuth, useQuery, AuthGuard } from '@archie/react-sdk';
51
+
52
+ const archie = createClient({
53
+ projectId: 'your-project-uuid',
54
+ apiKey: 'anon your-api-key',
55
+ apiUrl: 'https://your-project.archiecore.com',
56
+ environment: 'master',
57
+ retries: 3, // automatic retry with backoff (inherited from js-sdk)
58
+ timeout: 15_000, // request timeout in ms
59
+ });
60
+
61
+ function App() {
62
+ return (
63
+ <ArchieProvider client={archie}>
64
+ <AuthGuard fallback={<LoginPage />} loadingComponent={<Spinner />}>
65
+ <Dashboard />
66
+ </AuthGuard>
67
+ </ArchieProvider>
68
+ );
69
+ }
70
+
71
+ function LoginPage() {
72
+ const { signIn } = useAuth();
73
+
74
+ const handleSubmit = async (email: string, password: string) => {
75
+ const { session, error } = await signIn({ email, password });
76
+ if (error) console.error(error.message);
77
+ };
78
+
79
+ return <form onSubmit={/* ... */}>/* ... */</form>;
80
+ }
81
+
82
+ function Dashboard() {
83
+ const { user, signOut } = useAuth();
84
+ const { data, isLoading } = useQuery<{ orders: Order[] }>('{ orders { id total status } }');
85
+
86
+ return (
87
+ <div>
88
+ <p>Welcome, {user?.email}</p>
89
+ <button onClick={signOut}>Sign Out</button>
90
+ {isLoading ? <Spinner /> : <OrderList orders={data?.orders ?? []} />}
91
+ </div>
92
+ );
93
+ }
94
+ ```
95
+
96
+ ---
97
+
98
+ ## ArchieProvider
99
+
100
+ Wraps your React app to provide the Archie client and auth session state via context.
101
+
102
+ ```tsx
103
+ import { createClient } from '@archie/js-sdk';
104
+ import { ArchieProvider } from '@archie/react-sdk';
105
+
106
+ const archie = createClient({ projectId: '...', apiKey: 'anon ...' });
107
+
108
+ function App() {
109
+ return (
110
+ <ArchieProvider client={archie}>
111
+ <MyApp />
112
+ </ArchieProvider>
113
+ );
114
+ }
115
+ ```
116
+
117
+ **Props:**
118
+
119
+ | Prop | Type | Description |
120
+ | ---------- | -------------- | ------------------------------------- |
121
+ | `client` | `ArchieClient` | Client instance from `createClient()` |
122
+ | `children` | `ReactNode` | Child components |
123
+
124
+ **Internals:**
125
+
126
+ - Creates React context with `ArchieClient` instance
127
+ - Calls `client.auth.waitForInit()` on mount to restore persisted session
128
+ - Subscribes to `onAuthStateChange` and updates session in context
129
+ - Provides `{ client, session, isLoading }` to all child hooks
130
+
131
+ ---
132
+
133
+ ## Hooks
134
+
135
+ ### useArchieClient
136
+
137
+ Returns the raw `ArchieClient` instance. Useful for advanced use cases outside the provided hooks.
138
+
139
+ ```tsx
140
+ const archie = useArchieClient();
141
+
142
+ // Direct access to any module
143
+ const jwks = await archie.auth.getJWKS();
144
+ ```
145
+
146
+ **Throws** if used outside `<ArchieProvider>`.
147
+
148
+ ---
149
+
150
+ ### useAuth
151
+
152
+ Full authentication state and operations. All async methods return `{ data/session, error }` — they never throw.
153
+
154
+ ```tsx
155
+ const {
156
+ user, // User | null
157
+ session, // Session | null
158
+ isLoading, // boolean — true until initial session check completes
159
+ isAuthenticated, // boolean — shorthand for session !== null
160
+ signIn, // (params) => Promise<{ session, error }>
161
+ signUp, // (params) => Promise<{ data, error }>
162
+ signOut, // () => Promise<void>
163
+ confirmSignUp, // (params) => Promise<{ session, error }>
164
+ recoverPassword, // (params) => Promise<{ data, error }>
165
+ resetPassword, // (params) => Promise<{ data, error }>
166
+ refreshSession, // () => Promise<{ session, error }>
167
+ } = useAuth();
168
+ ```
169
+
170
+ **Example — Sign In:**
171
+
172
+ ```tsx
173
+ function LoginForm() {
174
+ const { signIn, isLoading } = useAuth();
175
+ const [error, setError] = useState<string | null>(null);
176
+
177
+ const handleSubmit = async (e: FormEvent) => {
178
+ e.preventDefault();
179
+ const { session, error } = await signIn({ email, password });
180
+ if (error) {
181
+ setError(error.message);
182
+ }
183
+ // session is automatically updated in context — no manual setState needed
184
+ };
185
+
186
+ return (
187
+ <form onSubmit={handleSubmit}>
188
+ {error && <p className="error">{error}</p>}
189
+ {/* inputs */}
190
+ <button disabled={isLoading}>Sign In</button>
191
+ </form>
192
+ );
193
+ }
194
+ ```
195
+
196
+ **Example — Sign Up flow:**
197
+
198
+ ```tsx
199
+ const { signUp, confirmSignUp } = useAuth();
200
+
201
+ // Step 1: Register
202
+ const { data, error } = await signUp({ email, password });
203
+ // data = { userId: '...', message: 'Check your email' }
204
+
205
+ // Step 2: Confirm with code from email
206
+ const { session, error: confirmError } = await confirmSignUp({
207
+ email,
208
+ code: '123456',
209
+ });
210
+ ```
211
+
212
+ **Example — Password Recovery:**
213
+
214
+ ```tsx
215
+ const { recoverPassword, resetPassword } = useAuth();
216
+
217
+ // Step 1: Request reset
218
+ await recoverPassword({ email });
219
+
220
+ // Step 2: Reset with code
221
+ await resetPassword({ email, code: '123456', newPassword: 'newPass!' });
222
+ ```
223
+
224
+ All methods are memoized with `useCallback` — safe to pass as props without causing re-renders.
225
+
226
+ ---
227
+
228
+ ### useQuery
229
+
230
+ Declarative GraphQL queries with caching, stale-while-revalidate, polling, and automatic deduplication.
231
+
232
+ ```tsx
233
+ const { data, error, isLoading, isRefetching, refetch } = useQuery<T>(
234
+ gql, // GraphQL query string
235
+ variables?, // Record<string, unknown>
236
+ options?, // UseQueryOptions
237
+ );
238
+ ```
239
+
240
+ **Options:**
241
+
242
+ | Option | Type | Default | Description |
243
+ | ---------------------- | ------------------------------ | ------- | ----------------------------------------------- |
244
+ | `enabled` | `boolean` | `true` | Set `false` to skip fetch (conditional queries) |
245
+ | `refetchInterval` | `number` | — | Poll interval in ms |
246
+ | `refetchOnWindowFocus` | `boolean` | `false` | Refetch when window regains focus |
247
+ | `onSuccess` | `(data: T) => void` | — | Called on successful fetch |
248
+ | `onError` | `(error: ArchieError) => void` | — | Called on error |
249
+
250
+ **Return:**
251
+
252
+ | Field | Type | Description |
253
+ | -------------- | --------------------- | -------------------------- |
254
+ | `data` | `T \| null` | Query result |
255
+ | `error` | `ArchieError \| null` | Error if failed |
256
+ | `isLoading` | `boolean` | True on initial load |
257
+ | `isRefetching` | `boolean` | True on background refetch |
258
+ | `refetch` | `() => Promise<void>` | Manually trigger refetch |
259
+
260
+ **Behaviors:**
261
+
262
+ - **Stale-while-revalidate**: Shows cached data immediately, refetches in background
263
+ - **Deduplication**: Multiple components with the same query+variables share a single network request
264
+ - **Auto-refetch on auth change**: When the user's token changes, all active queries refetch automatically
265
+ - **Variable tracking**: Deep comparison via `JSON.stringify` — refetches when variables change
266
+ - **SSR safe**: No `window` access when `refetchOnWindowFocus` is disabled
267
+
268
+ **Examples:**
269
+
270
+ ```tsx
271
+ // Basic query
272
+ const { data, isLoading } = useQuery<{ users: User[] }>('{ users { id email name } }');
273
+
274
+ // With variables
275
+ const { data } = useQuery<{ user: User }>('query($id: ID!) { user(id: $id) { id email } }', {
276
+ id: userId,
277
+ });
278
+
279
+ // Conditional query (wait for userId)
280
+ const { data } = useQuery<{ user: User }>(
281
+ 'query($id: ID!) { user(id: $id) { id email } }',
282
+ { id: userId },
283
+ { enabled: !!userId },
284
+ );
285
+
286
+ // Polling every 5 seconds
287
+ const { data } = useQuery<{ stats: Stats }>('{ stats { activeUsers requests } }', undefined, {
288
+ refetchInterval: 5000,
289
+ });
290
+ ```
291
+
292
+ ---
293
+
294
+ ### useMutation
295
+
296
+ Imperative GraphQL mutations with cache invalidation.
297
+
298
+ ```tsx
299
+ const { mutate, mutateAsync, data, error, isLoading, reset } = useMutation<T>(
300
+ gql, // GraphQL mutation string
301
+ options?, // UseMutationOptions
302
+ );
303
+ ```
304
+
305
+ **Options:**
306
+
307
+ | Option | Type | Description |
308
+ | ------------------- | ------------------------------ | ------------------------------------------------------- |
309
+ | `onSuccess` | `(data: T) => void` | Called after successful mutation |
310
+ | `onError` | `(error: ArchieError) => void` | Called on error |
311
+ | `onSettled` | `() => void` | Called after success or error |
312
+ | `invalidateQueries` | `string[]` | Query substrings to invalidate from cache after success |
313
+
314
+ **Return:**
315
+
316
+ | Field | Type | Description |
317
+ | ------------- | ------------------------------- | -------------------------- |
318
+ | `mutate` | `(vars?) => void` | Fire-and-forget mutation |
319
+ | `mutateAsync` | `(vars?) => Promise<T \| null>` | Await the result |
320
+ | `data` | `T \| null` | Last successful result |
321
+ | `error` | `ArchieError \| null` | Last error |
322
+ | `isLoading` | `boolean` | True while executing |
323
+ | `reset` | `() => void` | Clear data/error/isLoading |
324
+
325
+ **Example:**
326
+
327
+ ```tsx
328
+ const { mutate, isLoading } = useMutation<{ createUser: User }>(
329
+ 'mutation($input: CreateUserInput!) { createUser(input: $input) { id email } }',
330
+ {
331
+ invalidateQueries: ['users'],
332
+ onSuccess: (data) => toast.success(`Created ${data.createUser.email}`),
333
+ },
334
+ );
335
+
336
+ const handleCreate = () => {
337
+ mutate({ input: { email: 'new@user.com', name: 'John' } });
338
+ };
339
+ ```
340
+
341
+ ---
342
+
343
+ ### useSubscription
344
+
345
+ Real-time GraphQL subscriptions via WebSocket. Subscribes on mount, unsubscribes on unmount.
346
+
347
+ ```tsx
348
+ const { data, error, connectionState } = useSubscription<T>(
349
+ gql, // GraphQL subscription string
350
+ variables?, // Record<string, unknown>
351
+ options?, // UseSubscriptionOptions
352
+ );
353
+ ```
354
+
355
+ **Options:**
356
+
357
+ | Option | Type | Default | Description |
358
+ | --------- | ------------------------------ | ------- | -------------------------------- |
359
+ | `enabled` | `boolean` | `true` | Set `false` to skip subscription |
360
+ | `onData` | `(data: T) => void` | — | Called on each new value |
361
+ | `onError` | `(error: ArchieError) => void` | — | Called on error |
362
+
363
+ **Return:**
364
+
365
+ | Field | Type | Description |
366
+ | ----------------- | --------------------- | ----------------------------------------------------------------- |
367
+ | `data` | `T \| null` | Latest subscription value |
368
+ | `error` | `ArchieError \| null` | Last error |
369
+ | `connectionState` | `ConnectionState` | `'DISCONNECTED' \| 'CONNECTING' \| 'CONNECTED' \| 'RECONNECTING'` |
370
+
371
+ **Example:**
372
+
373
+ ```tsx
374
+ function LiveOrders() {
375
+ const { data, connectionState } = useSubscription<{ orderUpdated: Order }>(
376
+ 'subscription { orderUpdated { id status total } }',
377
+ );
378
+
379
+ return (
380
+ <div>
381
+ <Badge>{connectionState}</Badge>
382
+ {data && <OrderCard order={data.orderUpdated} />}
383
+ </div>
384
+ );
385
+ }
386
+ ```
387
+
388
+ **Lifecycle:** Reconnection is handled automatically by the underlying `js-sdk` realtime module (exponential backoff).
389
+
390
+ ---
391
+
392
+ ### useFileUpload
393
+
394
+ File uploads with progress tracking.
395
+
396
+ ```tsx
397
+ const { upload, progress, isUploading, data, error, reset } = useFileUpload();
398
+ ```
399
+
400
+ **Return:**
401
+
402
+ | Field | Type | Description |
403
+ | ------------- | ----------------------------------------------------------------- | ---------------------------- |
404
+ | `upload` | `(file: File \| Blob, opts?: FileUploadOptions) => Promise<void>` | Start upload |
405
+ | `progress` | `number` | 0–100 |
406
+ | `isUploading` | `boolean` | True while uploading |
407
+ | `data` | `FileUploadResult \| null` | `{ url, fileId }` on success |
408
+ | `error` | `ArchieError \| null` | Error if failed |
409
+ | `reset` | `() => void` | Clear all state |
410
+
411
+ `FileUploadOptions`: `{ filename?, contentType?, providerType?, onProgress? }`
412
+
413
+ **Example:**
414
+
415
+ ```tsx
416
+ function FileUploader() {
417
+ const { upload, progress, isUploading, data, error, reset } = useFileUpload();
418
+
419
+ return (
420
+ <div>
421
+ <input
422
+ type="file"
423
+ onChange={(e) => {
424
+ const file = e.target.files?.[0];
425
+ if (file) upload(file, { contentType: file.type, filename: file.name });
426
+ }}
427
+ />
428
+ {isUploading && <ProgressBar value={progress} />}
429
+ {data && <p>Uploaded: {data.url}</p>}
430
+ {error && <p className="error">{error.message}</p>}
431
+ {data && <button onClick={reset}>Upload Another</button>}
432
+ </div>
433
+ );
434
+ }
435
+ ```
436
+
437
+ ---
438
+
439
+ ### useRest
440
+
441
+ Returns the REST module directly for imperative REST API calls.
442
+
443
+ ```tsx
444
+ const rest = useRest();
445
+
446
+ // rest.get<T>(path, options?)
447
+ // rest.post<T>(path, body?, options?)
448
+ // rest.put<T>(path, body?, options?)
449
+ // rest.patch<T>(path, body?, options?)
450
+ // rest.delete<T>(path, options?)
451
+ ```
452
+
453
+ **Example:**
454
+
455
+ ```tsx
456
+ function ProductActions() {
457
+ const rest = useRest();
458
+
459
+ const handleExport = async () => {
460
+ const csv = await rest.get<string>('/api/products/export');
461
+ downloadCsv(csv);
462
+ };
463
+
464
+ return <button onClick={handleExport}>Export CSV</button>;
465
+ }
466
+ ```
467
+
468
+ ---
469
+
470
+ ### useRestQuery
471
+
472
+ Declarative hook for GET requests to custom REST endpoints.
473
+
474
+ ```tsx
475
+ const { data, error, isLoading, refetch } = useRestQuery<T>(path, options?);
476
+ ```
477
+
478
+ **Options** extend `RestRequestOptions` (`{ headers?, params?, signal? }`) with `enabled?: boolean`.
479
+
480
+ **Return:**
481
+
482
+ | Field | Type | Description |
483
+ | ----------- | --------------------- | ------------------------ |
484
+ | `data` | `T \| null` | Response data |
485
+ | `error` | `ArchieError \| null` | Error if failed |
486
+ | `isLoading` | `boolean` | True on initial load |
487
+ | `refetch` | `() => Promise<void>` | Manually trigger refetch |
488
+
489
+ **Example:**
490
+
491
+ ```tsx
492
+ const { data, isLoading } = useRestQuery<Product[]>('/api/products');
493
+ const { data: product } = useRestQuery<Product>(`/api/products/${id}`, { enabled: !!id });
494
+ ```
495
+
496
+ ---
497
+
498
+ ### useRestMutation
499
+
500
+ Imperative hook for POST/PUT/PATCH/DELETE requests.
501
+
502
+ ```tsx
503
+ const { execute, data, error, isLoading, reset } = useRestMutation<T>(path, method?);
504
+ ```
505
+
506
+ **Params:**
507
+
508
+ | Param | Type | Default | Description |
509
+ | -------- | ---------------------------------------- | -------- | ------------------ |
510
+ | `path` | `string` | — | REST endpoint path |
511
+ | `method` | `'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'` | `'POST'` | HTTP method |
512
+
513
+ **Return:**
514
+
515
+ | Field | Type | Description |
516
+ | ----------- | --------------------------------------------------------------------- | -------------------------- |
517
+ | `execute` | `(body?, overrideOptions?: RestRequestOptions) => Promise<T \| null>` | Execute the request |
518
+ | `data` | `T \| null` | Last successful result |
519
+ | `error` | `ArchieError \| null` | Last error |
520
+ | `isLoading` | `boolean` | True while executing |
521
+ | `reset` | `() => void` | Clear data/error/isLoading |
522
+
523
+ The `execute` function accepts an optional second parameter `overrideOptions` (`RestRequestOptions`) to pass custom `headers`, `params`, or `signal` per request.
524
+
525
+ **Example:**
526
+
527
+ ```tsx
528
+ const { execute, isLoading } = useRestMutation<Product>('/api/products', 'POST');
529
+
530
+ const handleCreate = async () => {
531
+ const product = await execute({ name: 'Widget', price: 9.99 });
532
+ if (product) router.push(`/products/${product.id}`);
533
+ };
534
+
535
+ // With custom headers per request
536
+ await execute(payload, { headers: { 'x-idempotency-key': uuid() } });
537
+ ```
538
+
539
+ ---
540
+
541
+ ## Components
542
+
543
+ ### AuthGuard
544
+
545
+ Conditional rendering based on authentication state.
546
+
547
+ ```tsx
548
+ <AuthGuard fallback={<LoginPage />} loadingComponent={<Spinner />} requiredRoles={['admin']}>
549
+ <ProtectedContent />
550
+ </AuthGuard>
551
+ ```
552
+
553
+ **Props:**
554
+
555
+ | Prop | Type | Default | Description |
556
+ | ------------------ | ----------- | ------- | ---------------------------------------------------- |
557
+ | `children` | `ReactNode` | — | Content shown when authorized |
558
+ | `fallback` | `ReactNode` | `null` | Content shown when NOT authenticated or unauthorized |
559
+ | `loadingComponent` | `ReactNode` | `null` | Content shown while session loads |
560
+ | `requiredRoles` | `string[]` | — | User must have at least one of these roles |
561
+
562
+ **Behavior:**
563
+
564
+ 1. While `isLoading` → render `loadingComponent`
565
+ 2. If not authenticated → render `fallback`
566
+ 3. If `requiredRoles` specified and user lacks all of them → render `fallback`
567
+ 4. Otherwise → render `children`
568
+
569
+ **Nested guards:**
570
+
571
+ ```tsx
572
+ <AuthGuard fallback={<LoginPage />}>
573
+ <AppLayout>
574
+ <AuthGuard requiredRoles={['admin']} fallback={<Unauthorized />}>
575
+ <AdminPanel />
576
+ </AuthGuard>
577
+ </AppLayout>
578
+ </AuthGuard>
579
+ ```
580
+
581
+ ---
582
+
583
+ ## Re-exported Types
584
+
585
+ For convenience, `@archie/react-sdk` re-exports all essential types from `@archie/js-sdk`, so you don't need to import from both packages:
586
+
587
+ ```tsx
588
+ import {
589
+ // Client
590
+ createClient,
591
+ type ArchieClient,
592
+ type ArchieClientOptions,
593
+
594
+ // Auth types
595
+ type Session,
596
+ type User,
597
+ type AuthSignUpParams,
598
+ type AuthSignInParams,
599
+ type AuthConfirmParams,
600
+ type AuthRecoverParams,
601
+ type AuthResetPasswordParams,
602
+ type AuthEvent,
603
+ type AuthEventCallback,
604
+
605
+ // GraphQL types
606
+ type GraphQLResponse,
607
+ type GraphQLRequestOptions,
608
+ type GraphQLRawRequest,
609
+
610
+ // File types
611
+ type FileUploadOptions,
612
+ type FileUploadResult,
613
+ type CsvUploadOptions,
614
+
615
+ // Realtime types
616
+ type Subscription,
617
+ type SubscriptionCallbacks,
618
+ type RealtimeEvent,
619
+ type RealtimeChannel,
620
+ type ConnectionState,
621
+ type ConnectionStateCallback,
622
+
623
+ // REST types
624
+ type RestRequestOptions,
625
+
626
+ // Error classes
627
+ ArchieError,
628
+ AuthError,
629
+ GraphQLError,
630
+ NetworkError,
631
+
632
+ // Storage
633
+ type StorageAdapter,
634
+ BrowserLocalStorage,
635
+ MemoryStorage,
636
+
637
+ // Interfaces (SOLID)
638
+ type IAuthModule,
639
+ type IGraphQLModule,
640
+ type IFileModule,
641
+ type IRealtimeModule,
642
+ type IRestModule,
643
+ type IHttpClient,
644
+ type Logger,
645
+ type TokenAccessor,
646
+ } from '@archie/react-sdk';
647
+
648
+ // React-specific prop types
649
+ import type { ArchieProviderProps, AuthGuardProps } from '@archie/react-sdk';
650
+ ```
651
+
652
+ ---
653
+
654
+ ## Error Handling
655
+
656
+ All `useAuth` methods return `{ data/session, error }` — they never throw. For `useQuery` and `useMutation`, errors are exposed via the `error` field in the return object.
657
+
658
+ Error classes from `@archie/js-sdk`:
659
+
660
+ | Class | When |
661
+ | -------------- | --------------------------------------------------------- |
662
+ | `AuthError` | Authentication failures (sign in, sign up, token refresh) |
663
+ | `GraphQLError` | GraphQL response errors |
664
+ | `NetworkError` | Network failures, timeouts |
665
+ | `ArchieError` | Base class for all Archie errors |
666
+
667
+ ```tsx
668
+ const { error } = useQuery<{ users: User[] }>('{ users { id } }');
669
+
670
+ if (error) {
671
+ if (error instanceof AuthError) {
672
+ // Token expired, redirect to login
673
+ } else if (error instanceof NetworkError) {
674
+ // Show offline indicator
675
+ } else {
676
+ // Show generic error
677
+ }
678
+ }
679
+ ```
680
+
681
+ ---
682
+
683
+ ## SSR Compatibility
684
+
685
+ All hooks are SSR safe:
686
+
687
+ - No `window` or `document` access on initial render
688
+ - `refetchOnWindowFocus` checks `typeof window` before adding listeners
689
+ - `ArchieProvider` initializes session asynchronously (no blocking SSR)
690
+ - `AuthGuard` renders `loadingComponent` during server render when session is unknown
691
+
692
+ For frameworks like Next.js, create the client outside the component tree:
693
+
694
+ ```tsx
695
+ // lib/archie.ts
696
+ import { createClient } from '@archie/js-sdk';
697
+
698
+ export const archie = createClient({
699
+ projectId: process.env.NEXT_PUBLIC_ARCHIE_PROJECT_ID!,
700
+ apiKey: process.env.NEXT_PUBLIC_ARCHIE_ANON_KEY!,
701
+ });
702
+
703
+ // app/providers.tsx
704
+ ('use client');
705
+ import { ArchieProvider } from '@archie/react-sdk';
706
+ import { archie } from '@/lib/archie';
707
+
708
+ export function Providers({ children }: { children: React.ReactNode }) {
709
+ return <ArchieProvider client={archie}>{children}</ArchieProvider>;
710
+ }
711
+ ```
712
+
713
+ ---
714
+
715
+ ## Advanced Client Options
716
+
717
+ `@archie/react-sdk` inherits all advanced options from `@archie/js-sdk` via `createClient()`. These are configured once at client creation and propagate automatically to all hooks:
718
+
719
+ ```tsx
720
+ import { createClient, consoleLogger } from '@archie/js-sdk';
721
+
722
+ const archie = createClient({
723
+ projectId: 'your-project-uuid',
724
+
725
+ // Retry & timeout
726
+ retries: 3, // retry transient errors (429, 500, 502, 503, 504, network)
727
+ retryDelay: 200, // initial backoff delay in ms (exponential with jitter)
728
+ timeout: 15_000, // abort requests after 15 seconds (0 = disabled)
729
+
730
+ // Custom fetch — for testing, proxies, or edge runtimes
731
+ fetch: customFetchImpl,
732
+
733
+ // Logger
734
+ logger: consoleLogger, // or a custom Logger implementation
735
+ });
736
+ ```
737
+
738
+ | Option | Type | Default | Description |
739
+ | ------------ | -------------- | ------------------ | --------------------------------------- |
740
+ | `timeout` | `number` | `30000` | Request timeout in ms (0 = disabled) |
741
+ | `retries` | `number` | `0` | Max retry attempts for transient errors |
742
+ | `retryDelay` | `number` | `200` | Initial backoff delay in ms |
743
+ | `fetch` | `typeof fetch` | `globalThis.fetch` | Custom fetch implementation |
744
+
745
+ ### Health Check
746
+
747
+ The underlying client also exposes `ping()`:
748
+
749
+ ```tsx
750
+ const archie = useArchieClient();
751
+
752
+ const isUp = await archie.ping();
753
+ ```
754
+
755
+ See the full [`@archie/js-sdk` README](../js-sdk/README.md) for detailed documentation on retry behavior, backoff strategy, timeout semantics, and custom fetch usage.
756
+
757
+ ---
758
+
759
+ ## TypeScript
760
+
761
+ All hooks are fully generic:
762
+
763
+ ```tsx
764
+ interface User {
765
+ id: string;
766
+ email: string;
767
+ name: string;
768
+ }
769
+
770
+ // Typed query response
771
+ const { data } = useQuery<{ users: User[] }>('{ users { id email name } }');
772
+ // ^? { users: User[] } | null
773
+
774
+ // Typed mutation response
775
+ const { mutateAsync } = useMutation<{ createUser: User }>('mutation ...');
776
+ const result = await mutateAsync({ input: { email: 'a@b.com' } });
777
+ // ^? { createUser: User } | null
778
+
779
+ // Typed subscription
780
+ const { data } = useSubscription<{ orderUpdated: Order }>('subscription ...');
781
+ // ^? { orderUpdated: Order } | null
782
+
783
+ // Typed REST
784
+ const { data } = useRestQuery<Product[]>('/api/products');
785
+ // ^? Product[] | null
786
+ ```
787
+
788
+ ---
789
+
790
+ ## License
791
+
792
+ [MIT](../../LICENSE)