@archie/react-sdk
v0.1.1
Published
Archie BAAS - React hooks, provider & components
Readme
@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-sdkPeer 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 }>
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
- 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 from cache after success |
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' } });
};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, reset } = useFileUpload();Return:
| Field | Type | Description |
| ------------- | ----------------------------------------------------------------- | ---------------------------- |
| upload | (file: File \| Blob, opts?: FileUploadOptions) => Promise<void> | Start upload |
| progress | number | 0–100 |
| isUploading | boolean | True while uploading |
| data | FileUploadResult \| null | { url, fileId } on success |
| error | ArchieError \| null | Error if failed |
| reset | () => void | Clear all state |
FileUploadOptions: { filename?, contentType?, providerType?, onProgress? }
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}</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() } });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>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