@archie/react-sdk
React hooks, provider, and components for the Archie BAAS platform.
Built on top of @archie/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.
Table of Contents
- Installation
- Quick Start
- ArchieProvider
- Hooks
- Components
- Re-exported Types
- Error Handling
- SSR Compatibility
- TypeScript
- License
Installation
npm install @archie/js-sdk @archie/react-sdk
# or
pnpm add @archie/js-sdk @archie/react-sdk
Peer dependencies: react >=18, @archie/js-sdk
Quick Start
import { createClient } from '@archie/js-sdk';
import { ArchieProvider, useAuth, useQuery, AuthGuard } from '@archie/react-sdk';
const archie = createClient({
projectId: 'your-project-uuid',
apiKey: 'anon your-api-key',
apiUrl: 'https://your-project.archiecore.com',
environment: 'master',
retries: 3, // automatic retry with backoff (inherited from js-sdk)
timeout: 15_000, // request timeout in ms
});
function App() {
return (
<ArchieProvider client={archie}>
<AuthGuard fallback={<LoginPage />} loadingComponent={<Spinner />}>
<Dashboard />
</AuthGuard>
</ArchieProvider>
);
}
function LoginPage() {
const { signIn } = useAuth();
const handleSubmit = async (email: string, password: string) => {
const { session, error } = await signIn({ email, password });
if (error) console.error(error.message);
};
return <form onSubmit={/* ... */}>/* ... */</form>;
}
function Dashboard() {
const { user, signOut } = useAuth();
const { data, isLoading } = useQuery<{ orders: Order[] }>('{ orders { id total status } }');
return (
<div>
<p>Welcome, {user?.email}</p>
<button onClick={signOut}>Sign Out</button>
{isLoading ? <Spinner /> : <OrderList orders={data?.orders ?? []} />}
</div>
);
}
ArchieProvider
Wraps your React app to provide the Archie client and auth session state via context.
import { createClient } from '@archie/js-sdk';
import { ArchieProvider } from '@archie/react-sdk';
const archie = createClient({ projectId: '...', apiKey: 'anon ...' });
function App() {
return (
<ArchieProvider client={archie}>
<MyApp />
</ArchieProvider>
);
}
Props:
| Prop | Type | Description |
|---|---|---|
client | ArchieClient | Client instance from createClient() |
children | ReactNode | Child components |
Internals:
- Creates React context with
ArchieClientinstance - Calls
client.auth.waitForInit()on mount to restore persisted session - Subscribes to
onAuthStateChangeand updates session in context - Provides
{ client, session, isLoading }to all child hooks
Hooks
useArchieClient
Returns the raw ArchieClient instance. Useful for advanced use cases outside the provided hooks.
const archie = useArchieClient();
// Direct access to any module
const jwks = await archie.auth.getJWKS();
Throws if used outside <ArchieProvider>.
useAuth
Full authentication state and operations. All async methods return { data/session, error } — they never throw.
const {
user, // User | null
session, // Session | null
isLoading, // boolean — true until initial session check completes
isAuthenticated, // boolean — shorthand for session !== null
signIn, // (params) => Promise<{ session, error }>
signUp, // (params) => Promise<{ data, error }>
exchangeCode, // ({ code }) => Promise<{ session, error }> — single-use code from a trusted server; session cannot be refreshed
signOut, // () => Promise<void>
confirmSignUp, // (params) => Promise<{ session, error }>
recoverPassword, // (params) => Promise<{ data, error }>
resetPassword, // (params) => Promise<{ data, error }>
refreshSession, // () => Promise<{ session, error }>
} = useAuth();
Example — Sign In:
function LoginForm() {
const { signIn, isLoading } = useAuth();
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
const { session, error } = await signIn({ email, password });
if (error) {
setError(error.message);
}
// session is automatically updated in context — no manual setState needed
};
return (
<form onSubmit={handleSubmit}>
{error && <p className="error">{error}</p>}
{/* inputs */}
<button disabled={isLoading}>Sign In</button>
</form>
);
}
Example — Sign Up flow:
const { signUp, confirmSignUp } = useAuth();
// Step 1: Register
const { data, error } = await signUp({ email, password });
// data = { userId: '...', message: 'Check your email' }
// Step 2: Confirm with code from email
const { session, error: confirmError } = await confirmSignUp({
email,
code: '123456',
});
Example — Password Recovery:
const { recoverPassword, resetPassword } = useAuth();
// Step 1: Request reset
await recoverPassword({ email });
// Step 2: Reset with code
await resetPassword({ email, code: '123456', newPassword: 'newPass!' });
All methods are memoized with useCallback — safe to pass as props without causing re-renders.
useQuery
Declarative GraphQL queries with caching, stale-while-revalidate, polling, and automatic deduplication.
const { data, error, isLoading, isRefetching, refetch } = useQuery<T>(
gql, // GraphQL query string
variables?, // Record<string, unknown>
options?, // UseQueryOptions
);
Options:
| Option | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Set false to skip fetch (conditional queries) |
refetchInterval | number | — | Poll interval in ms |
refetchOnWindowFocus | boolean | false | Refetch when window regains focus |
onSuccess | (data: T) => void | — | Called on successful fetch |
onError | (error: ArchieError) => void | — | Called on error |
Return:
| Field | Type | Description |
|---|---|---|
data | T | null | Query result |
error | ArchieError | null | Error if failed |
isLoading | boolean | True on initial load |
isRefetching | boolean | True on background refetch |
refetch | () => Promise<void> | Manually trigger refetch |
Behaviors:
- Stale-while-revalidate: Shows cached data immediately, refetches in background
- Deduplication: Multiple components with the same query+variables share a single network request
- Auto-refetch on auth change: When the user's token changes, all active queries refetch automatically
- Auto-refetch on invalidation: A mounted query re-runs as soon as its key is invalidated (e.g. by a
useMutationwithinvalidateQueries); concurrent consumers of the same key still share one request - Latest-response-wins: A response that was already in flight when its key got invalidated is discarded instead of repainting the view with pre-mutation data
- Errors never blank the view: A failed revalidation sets
errorbut keeps the data already on screen - Variable tracking: Deep comparison via
JSON.stringify— refetches when variables change - SSR safe: No
windowaccess whenrefetchOnWindowFocusis disabled
Examples:
// Basic query
const { data, isLoading } = useQuery<{ users: User[] }>('{ users { id email name } }');
// With variables
const { data } = useQuery<{ user: User }>('query($id: ID!) { user(id: $id) { id email } }', {
id: userId,
});
// Conditional query (wait for userId)
const { data } = useQuery<{ user: User }>(
'query($id: ID!) { user(id: $id) { id email } }',
{ id: userId },
{ enabled: !!userId },
);
// Polling every 5 seconds
const { data } = useQuery<{ stats: Stats }>('{ stats { activeUsers requests } }', undefined, {
refetchInterval: 5000,
});
useMutation
Imperative GraphQL mutations with cache invalidation.
const { mutate, mutateAsync, data, error, isLoading, reset } = useMutation<T>(
gql, // GraphQL mutation string
options?, // UseMutationOptions
);
Options:
| Option | Type | Description |
|---|---|---|
onSuccess | (data: T) => void | Called after successful mutation |
onError | (error: ArchieError) => void | Called on error |
onSettled | () => void | Called after success or error |
invalidateQueries | string[] | Query substrings to invalidate after success — every mounted useQuery whose key matches refetches |
Return:
| Field | Type | Description |
|---|---|---|
mutate | (vars?) => void | Fire-and-forget mutation |
mutateAsync | (vars?) => Promise<T | null> | Await the result |
data | T | null | Last successful result |
error | ArchieError | null | Last error |
isLoading | boolean | True while executing |
reset | () => void | Clear data/error/isLoading |
Example:
const { mutate, isLoading } = useMutation<{ createUser: User }>(
'mutation($input: CreateUserInput!) { createUser(input: $input) { id email } }',
{
invalidateQueries: ['users'],
onSuccess: (data) => toast.success(`Created ${data.createUser.email}`),
},
);
const handleCreate = () => {
mutate({ input: { email: '[email protected]', name: 'John' } });
};
Matching is by substring, so
invalidateQueries: ['id']would refetch every mounted query whose text containsid— each one a real network request. Prefer patterns distinctive enough to name the entity you mutated. Passing several patterns is cheap: the whole array is applied as one batch, so a query matched by more than one of them still refetches only once.
useSubscription
Real-time GraphQL subscriptions via WebSocket. Subscribes on mount, unsubscribes on unmount.
const { data, error, connectionState } = useSubscription<T>(
gql, // GraphQL subscription string
variables?, // Record<string, unknown>
options?, // UseSubscriptionOptions
);
Options:
| Option | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Set false to skip subscription |
onData | (data: T) => void | — | Called on each new value |
onError | (error: ArchieError) => void | — | Called on error |
Return:
| Field | Type | Description |
|---|---|---|
data | T | null | Latest subscription value |
error | ArchieError | null | Last error |
connectionState | ConnectionState | 'DISCONNECTED' | 'CONNECTING' | 'CONNECTED' | 'RECONNECTING' |
Example:
function LiveOrders() {
const { data, connectionState } = useSubscription<{ orderUpdated: Order }>(
'subscription { orderUpdated { id status total } }',
);
return (
<div>
<Badge>{connectionState}</Badge>
{data && <OrderCard order={data.orderUpdated} />}
</div>
);
}
Lifecycle: Reconnection is handled automatically by the underlying js-sdk realtime module (exponential backoff).
useFileUpload
File uploads with progress tracking.
const { upload, progress, isUploading, data, error, retry, abort, reset } = useFileUpload();
Return:
| Field | Type | Description |
|---|---|---|
upload | (file: File | Blob, opts?: FileUploadOptions) => Promise<FileUploadResult | null> | Start upload; resolves the result, or null on failure/abort |
progress | number | Real upload progress, 0..100 (jumps 0 to 100 without XMLHttpRequest) |
fraction | number | The same progress as 0..1 |
isUploading | boolean | True while uploading |
data | FileUploadResult | null | { fileId, url, filename, mimetype, size } on success (url is null if the lookup failed) |
error | ArchieError | null | Error if failed |
retry | () => Promise<FileUploadResult | null> | Re-run the last upload() with the same file and options |
abort | () => void | Abort the upload in flight (error.code is REQUEST_ABORTED) |
reset | () => void | Clear all state |
FileUploadOptions: { filename?, contentType?, providerType? } (filename and contentType default to the File's; a plain Blob without a type needs contentType). Also { onProgress?, signal? }. error.code is the stable upload code (FILE_TOO_LARGE, UNSUPPORTED_FILE_TYPE, STORAGE_NOT_ENABLED, UPLOAD_AUTH_REQUIRED, UPLOADS_DISABLED, STORAGE_QUOTA_EXCEEDED, REQUEST_ABORTED, TIMEOUT). Progress is real in browsers (XMLHttpRequest); in Node/SSR it jumps 0 to 1.
Example:
function FileUploader() {
const { upload, progress, isUploading, data, error, reset } = useFileUpload();
return (
<div>
<input
type="file"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) upload(file, { contentType: file.type, filename: file.name });
}}
/>
{isUploading && <ProgressBar value={progress} />}
{data && <p>Uploaded: {data.url ?? data.fileId}</p>}
{error && <p className="error">{error.message}</p>}
{data && <button onClick={reset}>Upload Another</button>}
</div>
);
}
useRest
Returns the REST module directly for imperative REST API calls.
const rest = useRest();
// rest.get<T>(path, options?)
// rest.post<T>(path, body?, options?)
// rest.put<T>(path, body?, options?)
// rest.patch<T>(path, body?, options?)
// rest.delete<T>(path, options?)
Example:
function ProductActions() {
const rest = useRest();
const handleExport = async () => {
const csv = await rest.get<string>('/api/products/export');
downloadCsv(csv);
};
return <button onClick={handleExport}>Export CSV</button>;
}
useRestQuery
Declarative hook for GET requests to custom REST endpoints.
const { data, error, isLoading, refetch } = useRestQuery<T>(path, options?);
Options extend RestRequestOptions ({ headers?, params?, signal? }) with enabled?: boolean.
Return:
| Field | Type | Description |
|---|---|---|
data | T | null | Response data |
error | ArchieError | null | Error if failed |
isLoading | boolean | True on initial load |
refetch | () => Promise<void> | Manually trigger refetch |
Example:
const { data, isLoading } = useRestQuery<Product[]>('/api/products');
const { data: product } = useRestQuery<Product>(`/api/products/${id}`, { enabled: !!id });
useRestMutation
Imperative hook for POST/PUT/PATCH/DELETE requests.
const { execute, data, error, isLoading, reset } = useRestMutation<T>(path, method?);
Params:
| Param | Type | Default | Description |
|---|---|---|---|
path | string | — | REST endpoint path |
method | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'POST' | HTTP method |
Return:
| Field | Type | Description |
|---|---|---|
execute | (body?, overrideOptions?: RestRequestOptions) => Promise<T | null> | Execute the request |
data | T | null | Last successful result |
error | ArchieError | null | Last error |
isLoading | boolean | True while executing |
reset | () => void | Clear data/error/isLoading |
The execute function accepts an optional second parameter overrideOptions (RestRequestOptions) to pass custom headers, params, or signal per request.
Example:
const { execute, isLoading } = useRestMutation<Product>('/api/products', 'POST');
const handleCreate = async () => {
const product = await execute({ name: 'Widget', price: 9.99 });
if (product) router.push(`/products/${product.id}`);
};
// With custom headers per request
await execute(payload, { headers: { 'x-idempotency-key': uuid() } });
useNotifications
Subscribes to live in-app notifications and manages read state. Opens a WebSocket channel on mount and closes it on unmount.
const { user } = useAuth();
const receiverId = user?.id ?? '';
const {
notifications,
latest,
unreadCount,
isLoading,
error,
connectionState,
markAsRead,
markAllAsRead,
refetch,
} = useNotifications(receiverId, { enabled: !!user, limit: 20 });
Options:
| Option | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Set false to skip loading and subscription (e.g. while user is null) |
unreadOnly | boolean | false | Initial load returns only unread notifications |
limit | number | — | Max items to load in the initial fetch |
onNotification | (n: NotificationWithSafeUrl) => void | — | Called for each new live notification (e.g. for toasts or sounds) |
Returns:
| Field | Type | Description |
|---|---|---|
notifications | Notification[] | History + live arrivals, most-recent first, deduplicated by id |
latest | Notification | null | Last notification received live (convenient for toasts) |
unreadCount | number | Live unread count — updates automatically |
isLoading | boolean | True during initial history load |
error | ArchieError | null | Error from the latest failed operation |
connectionState | ConnectionState | 'CONNECTING' | 'CONNECTED' | 'DISCONNECTED' | 'RECONNECTING' |
markAsRead | (id: string) => Promise<void> | Mark one notification read; updates local state optimistically |
markAllAsRead | () => Promise<void> | Mark all loaded notifications read; reconciles unreadCount with the server (normally 0) |
refetch | () => Promise<void> | Reload the notification history |
Infinite scroll
useNotifications pages backward through history without dropping or duplicating live items:
const { notifications, fetchMore, hasMore, isFetchingMore, error } = useNotifications(user.id, {
limit: 20, // page size for fetchMore()
});
// e.g. an IntersectionObserver on a sentinel at the bottom of the list.
// Gate on `!error` so a failed page doesn't drive a tight auto-retry loop
// (the hook also suppresses fetchMore internally after an error until refetch).
useEffect(() => {
if (hasMore && !isFetchingMore && !error) void fetchMore();
}, [hasMore, isFetchingMore, error, fetchMore]);
fetchMore()— loads the next older page and appends it (deduped, newest-first). No-op when!hasMore, a fetch is already in flight, the hook is disabled, or a previousfetchMoreerrored (callrefetch()— or changereceiverId/unreadOnly— to retry).hasMore— more older records exist for the active filter.isFetchingMore— afetchMore()call is in flight (drive a bottom "loading older" indicator).
Changing receiverId or unreadOnly resets pagination and re-seeds from the first page.
Example — notification bell with unread badge:
import { useAuth, useNotifications } from '@archie/react-sdk';
function NotificationBell() {
const { user } = useAuth();
const receiverId = user?.id ?? '';
const { unreadCount, connectionState } = useNotifications(receiverId, { enabled: !!user });
return (
<button aria-label="Notifications">
<BellIcon />
{unreadCount > 0 && <span className="badge">{unreadCount > 99 ? '99+' : unreadCount}</span>}
{connectionState !== 'CONNECTED' && <span className="dot reconnecting" />}
</button>
);
}
Example — notification list with mark-as-read:
function NotificationList() {
const { user } = useAuth();
const receiverId = user?.id ?? '';
const { notifications, isLoading, unreadCount, markAsRead, markAllAsRead } = useNotifications(
receiverId,
{ enabled: !!user, limit: 20 },
);
if (isLoading) return <Spinner />;
return (
<div>
<header>
<span>{unreadCount} unread</span>
<button onClick={() => markAllAsRead()} disabled={unreadCount === 0}>
Mark all read
</button>
</header>
<ul>
{notifications.map((n) => (
<li
key={n.id}
className={n.isRead ? 'read' : 'unread'}
onClick={() => !n.isRead && markAsRead(n.id)}
>
<p>{n.actionText ?? n.notificationKey}</p>
<time>{new Date(n.createdAt).toLocaleString()}</time>
</li>
))}
</ul>
</div>
);
}
Always call the hook unconditionally (React rules of hooks). When there is no authenticated user, pass
receiverId=''andenabled: false— the hook will not load or subscribe.
Security note. Never render the raw
n.actionUrlas anhref— it is untrusted server data (javascript:,data:,//evil.com,https://[email protected]). Rows returned byuseNotificationscarry a pre-sanitizedsafeActionUrlfield (string | null), or use theNotificationActioncomponent. Treatn.metadataandn.paramsas untrusted too. See the js-sdk README Security section for the exact rules ofsafeActionUrl.
safeActionUrl on each row. Every item in notifications (and latest) is the original notification plus safeActionUrl: string | null. actionUrl itself is left untouched, so this is purely additive.
{
notifications.map((n) => (
<li key={n.id}>
{n.safeActionUrl ? (
<a href={n.safeActionUrl} rel="noopener noreferrer">
Open
</a>
) : null}
</li>
));
}
useAIChat
Live chat over Archie AI (client.ai.stream, REST SSE). For one-shot tasks (summarize, classify, extract) prefer the GraphQL aiChat mutation with useMutation.
import { useAIChat } from '@archie/react-sdk';
function Assistant() {
const { messages, send, stop, status, usage, error } = useAIChat({
system: 'You help customers with their orders.',
tools: [
{
name: 'getOrders',
description: 'List the current user orders',
parameters: { type: 'object', properties: { limit: { type: 'number' } } },
execute: async ({ limit }) => fetchOrders(Number(limit ?? 5)),
},
],
metadata: { feature: 'order-assistant' },
});
// render messages; send('Where is my order?'); stop() aborts
}
messagesare in contract shape and update live while the answer streams.providerStatefromdoneis kept on the assistant message and resent on the next turn.- Tool loop: each
tool_callrunstool.execute(args, { toolCall, signal })(oronToolCall(call, { signal })), its result (strings as-is, other values as JSON; a throw becomesisError: true) is appended as atoolmessage and the chat continues automatically, up tomaxStepsmodel calls persend()(default 5). status:idle|submitted|streaming|error;usageaccumulates over the conversation;erroris anAIError(insufficient_credits,rate_limited, ...).stop(),reset(), a newsend()and unmount abort the request in flight; an interrupted turn keeps its text and drops unanswered tool calls, so the next turn is valid.- Options:
model,provider,system,tools,toolChoice,responseFormat,reasoning,maxTokens,temperature,metadata,providerOptions,initialMessages,maxSteps,onToolCall,onFinish,onError.
Components
AuthGuard
Conditional rendering based on authentication state.
<AuthGuard fallback={<LoginPage />} loadingComponent={<Spinner />} requiredRoles={['admin']}>
<ProtectedContent />
</AuthGuard>
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Content shown when authorized |
fallback | ReactNode | null | Content shown when NOT authenticated or unauthorized |
loadingComponent | ReactNode | null | Content shown while session loads |
requiredRoles | string[] | — | User must have at least one of these roles |
Behavior:
- While
isLoading→ renderloadingComponent - If not authenticated → render
fallback - If
requiredRolesspecified and user lacks all of them → renderfallback - Otherwise → render
children
Nested guards:
<AuthGuard fallback={<LoginPage />}>
<AppLayout>
<AuthGuard requiredRoles={['admin']} fallback={<Unauthorized />}>
<AdminPanel />
</AuthGuard>
</AppLayout>
</AuthGuard>
NotificationAction
Renders a notification's call-to-action safely.
import { NotificationAction } from '@archie/react-sdk';
<NotificationAction notification={n} className="btn" />
<NotificationAction notification={n}>View details</NotificationAction>
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
notification | { actionText?, actionUrl? } (a Notification) | — | Source of the label and link |
className | string | — | Applied to the <a> (or to the <span> when there is no link) |
children | ReactNode | actionText | Overrides the label |
Behavior:
- The URL goes through
safeActionUrl(https://or same-origin/pathonly). - Safe →
<a href rel="noopener noreferrer">; the label is a plain React text child (escaped, never HTML). - Unsafe or missing URL → the label as a
<span>with no link. - No label → renders nothing.
Re-exported Types
For convenience, @archie/react-sdk re-exports all essential types from @archie/js-sdk, so you don't need to import from both packages:
import {
// Client
createClient,
type ArchieClient,
type ArchieClientOptions,
// Auth types
type Session,
type User,
type AuthSignUpParams,
type AuthSignInParams,
type AuthConfirmParams,
type AuthRecoverParams,
type AuthResetPasswordParams,
type AuthEvent,
type AuthEventCallback,
// GraphQL types
type GraphQLResponse,
type GraphQLRequestOptions,
type GraphQLRawRequest,
// File types
type FileUploadOptions,
type FileUploadResult,
type CsvUploadOptions,
// Realtime types
type Subscription,
type SubscriptionCallbacks,
type RealtimeEvent,
type RealtimeChannel,
type ConnectionState,
type ConnectionStateCallback,
// REST types
type RestRequestOptions,
// Error classes
ArchieError,
AuthError,
GraphQLError,
NetworkError,
// Storage
type StorageAdapter,
BrowserLocalStorage,
MemoryStorage,
// Interfaces (SOLID)
type IAuthModule,
type IGraphQLModule,
type IFileModule,
type IRealtimeModule,
type IRestModule,
type IHttpClient,
type Logger,
type TokenAccessor,
} from '@archie/react-sdk';
// React-specific prop types
import type { ArchieProviderProps, AuthGuardProps } from '@archie/react-sdk';
Error Handling
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.
Error classes from @archie/js-sdk:
| Class | When |
|---|---|
AuthError | Authentication failures (sign in, sign up, token refresh) |
GraphQLError | GraphQL response errors |
NetworkError | Network failures, timeouts |
ArchieError | Base class for all Archie errors |
const { error } = useQuery<{ users: User[] }>('{ users { id } }');
if (error) {
if (error instanceof AuthError) {
// Token expired, redirect to login
} else if (error instanceof NetworkError) {
// Show offline indicator
} else {
// Show generic error
}
}
SSR Compatibility
All hooks are SSR safe:
- No
windowordocumentaccess on initial render refetchOnWindowFocuscheckstypeof windowbefore adding listenersArchieProviderinitializes session asynchronously (no blocking SSR)AuthGuardrendersloadingComponentduring server render when session is unknown
For frameworks like Next.js, create the client outside the component tree:
// lib/archie.ts
import { createClient } from '@archie/js-sdk';
export const archie = createClient({
projectId: process.env.NEXT_PUBLIC_ARCHIE_PROJECT_ID!,
apiKey: process.env.NEXT_PUBLIC_ARCHIE_ANON_KEY!,
});
// app/providers.tsx
('use client');
import { ArchieProvider } from '@archie/react-sdk';
import { archie } from '@/lib/archie';
export function Providers({ children }: { children: React.ReactNode }) {
return <ArchieProvider client={archie}>{children}</ArchieProvider>;
}
Advanced Client Options
@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:
import { createClient, consoleLogger } from '@archie/js-sdk';
const archie = createClient({
projectId: 'your-project-uuid',
// Retry & timeout
retries: 3, // retry transient errors (429, 500, 502, 503, 504, network)
retryDelay: 200, // initial backoff delay in ms (exponential with jitter)
timeout: 15_000, // abort requests after 15 seconds (0 = disabled)
// Custom fetch — for testing, proxies, or edge runtimes
fetch: customFetchImpl,
// Logger
logger: consoleLogger, // or a custom Logger implementation
});
| Option | Type | Default | Description |
|---|---|---|---|
timeout | number | 30000 | Request timeout in ms (0 = disabled) |
retries | number | 0 | Max retry attempts for transient errors |
retryDelay | number | 200 | Initial backoff delay in ms |
fetch | typeof fetch | globalThis.fetch | Custom fetch implementation |
Health Check
The underlying client also exposes ping():
const archie = useArchieClient();
const isUp = await archie.ping();
See the full @archie/js-sdk README for detailed documentation on retry behavior, backoff strategy, timeout semantics, and custom fetch usage.
TypeScript
All hooks are fully generic:
interface User {
id: string;
email: string;
name: string;
}
// Typed query response
const { data } = useQuery<{ users: User[] }>('{ users { id email name } }');
// ^? { users: User[] } | null
// Typed mutation response
const { mutateAsync } = useMutation<{ createUser: User }>('mutation ...');
const result = await mutateAsync({ input: { email: '[email protected]' } });
// ^? { createUser: User } | null
// Typed subscription
const { data } = useSubscription<{ orderUpdated: Order }>('subscription ...');
// ^? { orderUpdated: Order } | null
// Typed REST
const { data } = useRestQuery<Product[]>('/api/products');
// ^? Product[] | null