@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 +792 -0
- package/dist/index.d.ts +246 -0
- package/dist/index.js +483 -0
- package/dist/index.js.map +1 -0
- package/package.json +72 -0
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)
|