Client Reference
React hooks, components, and utilities for building frontends with Arcway APIs
Arcway ships a client library (arcway/lib/client) with React hooks, components, and utilities for building interactive frontends that connect to Arcway APIs.
Setup
Wrap your app in the Provider to configure API communication:
import { Provider } from 'arcway/lib/client';
function App() {
return (
<Provider pathPrefix="/api" wsUrl="ws://localhost:3000">
<MyApp />
</Provider>
);
}Provider props:
| Prop | Type | Default | Description |
|---|---|---|---|
pathPrefix | string | '' | Prefix for all API paths |
headers | object | — | Default HTTP headers for all requests |
wsUrl | string | — | WebSocket URL for real-time updates |
Data Fetching
useApi
The primary data fetching hook. Fetches data from an API endpoint and returns reactive state with built-in SWR caching:
import { useApi } from 'arcway/lib/client';
function UserList() {
const { data, loading, error } = useApi('/users');
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{data.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}Parameters:
| Parameter | Type | Description |
|---|---|---|
path | string | null | API endpoint. Pass null to disable fetching. |
query | object | Optional query parameters |
options | object | Optional SWR config plus { disable, ws } |
Returns:
| Property | Type | Description |
|---|---|---|
data | any | Response data |
error | Error | null | Error if request failed |
loading | boolean | True during initial load |
isLoading | boolean | Same as loading |
isValidating | boolean | True during any fetch (including revalidation) |
mutate | function | Manually revalidate or update cached data |
post | function | POST request, auto-revalidates after |
put | function | PUT request, auto-revalidates after |
patch | function | PATCH request, auto-revalidates after |
del | function | DELETE request, auto-revalidates after |
Inline mutations — post, put, patch, del let you mutate without a separate hook:
function UserActions() {
const { data: users, post, del } = useApi('/users');
const addUser = async () => {
await post({ name: 'Alice' });
// data auto-revalidates after mutation
};
const removeUser = async (id) => {
await del({ id });
};
}Conditional fetching:
// Only fetch when userId is available
const { data } = useApi(userId ? `/users/${userId}` : null);
// Disable with option
const { data } = useApi('/users', undefined, { disable: true });WebSocket integration:
When an endpoint has a ws() handler on the server, useApi automatically subscribes to real-time updates. Disable with { ws: false }:
const { data } = useApi('/stats', undefined, { ws: false });useApiPaginated
For paginated endpoints where each page is fetched separately and appended client-side:
import { useApiPaginated } from 'arcway/lib/client';
function CommitList({ workspaceId }) {
const { data, size, setSize, loading } = useApiPaginated(
`/workspaces/${workspaceId}/git/commits`,
(pageIndex, previousPage) => {
if (pageIndex > 0 && previousPage?.hasMore === false) return null;
return { limit: 50, skip: pageIndex * 50 };
},
);
const pages = data || [];
const commits = pages.flatMap((page) => page.commits || []);
return (
<>
{commits.map((commit) => (
<div key={commit.sha}>{commit.subject}</div>
))}
<button disabled={loading} onClick={() => setSize(size + 1)}>
Load more
</button>
</>
);
}getQuery(pageIndex, previousPageData) controls pagination:
- return a query object to fetch that page
- return
nullto stop pagination
It returns the normal SWR Infinite fields like data, size, setSize, mutate, plus Arcway's loading, post, put, patch, and del helpers.
useMutation
For write operations that don't need to read data first:
import { useMutation } from 'arcway/lib/client';
function CreateUser() {
const { trigger, loading, error, data } = useMutation('/users');
const handleSubmit = async (formData) => {
const user = await trigger(formData);
// user is the response data
};
return <form onSubmit={...}>...</form>;
}Parameters:
| Parameter | Type | Description |
|---|---|---|
path | string | API endpoint |
options | object | Optional { method } (default: 'POST') |
Returns: { data, error, loading, isMutating, trigger, reset }
trigger(body?)— execute the mutation with optional request bodyreset()— clear data and error state
Method and body handling:
// POST (default) — Content-Type: application/json set automatically
const { trigger } = useMutation('/users');
await trigger({ name: 'Alice' });
// DELETE without body — no Content-Type header sent
const { trigger: del } = useMutation('/users/1', { method: 'DELETE' });
await del();
// DELETE with body — Content-Type: application/json set
await del({ reason: 'Inactive' });useMutation only sets Content-Type: application/json when a body is provided. This avoids Safari/WebKit issues with bodyless requests that include Content-Type.
useGraphQL
Fetch data with GraphQL queries:
import { useGraphQL } from 'arcway/lib/client';
function Users() {
const { data, loading, error } = useGraphQL(
`query { users { id name } }`,
);
if (loading) return <p>Loading...</p>;
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}Parameters:
| Parameter | Type | Description |
|---|---|---|
query | string | null | GraphQL query string. Pass null to disable. |
variables | object | Optional GraphQL variables |
options | object | Optional SWR config |
Returns: { data, error, isLoading, isValidating, mutate }
With variables:
const { data } = useGraphQL(
`query GetUser($id: ID!) { user(id: $id) { name email } }`,
{ id: userId },
);useGraphQLMutation
import { useGraphQLMutation } from 'arcway/lib/client';
function CreateUser() {
const { trigger, loading, error } = useGraphQLMutation(`
mutation CreateUser($name: String!, $email: String!) {
createUser(name: $name, email: $email) { id name }
}
`);
const handleSubmit = async () => {
const result = await trigger({ name: 'Alice', email: 'alice@example.com' });
};
}Returns: { data, error, isMutating, trigger, reset }
Navigation
Link
Client-side navigation link with prefetching:
import { Link } from 'arcway/lib/client';
<Link href="/about">About</Link>
<Link href="/page" prefetch="hover">Prefetch on hover (default)</Link>
<Link href="/page" prefetch="viewport">Prefetch when visible</Link>
<Link href="/page" prefetch="none">No prefetching</Link>
<Link href="/page" replace>Replace history entry</Link>useRouter
Programmatic navigation:
import { useRouter } from 'arcway/lib/client';
function MyComponent() {
const router = useRouter();
router.push('/new-page'); // Navigate
router.push({ pathname: '/inbox', query: { filter: 'unread' } }); // Navigate with query
router.replace('/new-page'); // Navigate without adding history entry
router.back(); // History back
router.forward(); // History forward
router.refresh(); // Reload current page
}Router properties:
| Property | Type | Description |
|---|---|---|
pathname | string | Current URL path |
params | object | Route parameters |
query | object | URL query parameters |
push(to, options?) | function | Navigate to a new URL. to can be a string or { pathname, query } |
replace(to, options?) | function | Navigate with history replace. to can be a string or { pathname, query } |
back() | function | Go back in history |
forward() | function | Go forward in history |
refresh() | function | Reload the current page |
useRouter() exposes pathname, params, and query directly:
import { useRouter } from 'arcway/lib/client';
const router = useRouter();
const pathname = router.pathname;
const { slug } = router.params;
const page = router.query.page || '1';useQuery
Manages URL query parameters with a setter (not for data fetching — use useApi for that):
import { useQuery } from 'arcway/lib/client';
function FilteredList() {
const { query, setQuery } = useQuery();
return (
<div>
<p>Current page: {query.page || '1'}</p>
<button onClick={() => setQuery({ page: '2' })}>Page 2</button>
<button onClick={() => setQuery({ page: null })}>Clear page param</button>
</div>
);
}Returns:
query— object of current URL query parameterssetQuery(updates, options?)— merges updates into URL query string. Set value tonullto remove it. Pass{ replace: true }to replace all params.
Forms
Manage form state with plain React state and submit handlers in your components.
Returns:
| Property | Type | Description |
|---|---|---|
values | object | Current form values |
errors | object | Field errors by name |
touched | object | Which fields have been interacted with |
isDirty | boolean | Whether form has changed from initial values |
isSubmitting | boolean | Whether submission is in progress |
setField(name, value) | function | Set a single field value |
setError(name, message) | function | Set a field error manually |
handleSubmit(e?) | function | Form submission handler |
reset(newValues?) | function | Reset to initial values (or new values) |
Components
Head
SSR-safe head/meta tag management:
import { Head } from 'arcway/lib/client';
export default function MyPage() {
return (
<>
<Head>
<title>My Page</title>
<meta name="description" content="Page description" />
</Head>
<div>Content</div>
</>
);
}Props: title, description, and any child <meta> or <link> elements. Works during both SSR and client-side rendering.
Environment
useEnv
Access public environment variables on the client. Variables must be prefixed with SOLO_PUBLIC_ in your .env file:
import { useEnv } from 'arcway/lib/client';
function MyComponent() {
const apiUrl = useEnv('PUBLIC_API_URL');
// reads SOLO_PUBLIC_API_URL from the environment
}The env() function is also available for non-React contexts:
import { env } from 'arcway/lib/client';
const apiUrl = env('PUBLIC_API_URL');Utility Hooks
useDebounce
Debounce a value by a delay:
import { useDebounce } from 'arcway/lib/client';
function Search() {
const [search, setSearch] = useState('');
const debouncedSearch = useDebounce(search, 300);
// Only fetches when user stops typing for 300ms
const { data } = useApi(debouncedSearch ? `/search?q=${debouncedSearch}` : null);
return <input value={search} onChange={(e) => setSearch(e.target.value)} />;
}useInterval
Run a callback on a recurring interval:
import { useInterval } from 'arcway/lib/client';
// Poll every 5 seconds
useInterval(() => fetchUpdates(), 5000);
// Pass null to disable
useInterval(() => fetchUpdates(), isActive ? 5000 : null);useLocalStorage
Persist state in localStorage with automatic JSON serialization:
import { useLocalStorage } from 'arcway/lib/client';
const [theme, setTheme] = useLocalStorage('theme', 'light');
// Supports function updater
setTheme((prev) => prev === 'light' ? 'dark' : 'light');useClickOutside
Detect clicks outside an element (useful for dropdowns, modals):
import { useClickOutside } from 'arcway/lib/client';
import { useRef } from 'react';
function Dropdown() {
const ref = useRef(null);
const [open, setOpen] = useState(false);
useClickOutside(ref, () => setOpen(false));
return (
<div ref={ref}>
<button onClick={() => setOpen(!open)}>Toggle</button>
{open && <div className="dropdown-menu">...</div>}
</div>
);
}Low-Level Utilities
soloFetch
Fetch helper for use outside React. Returns parsed JSON, throws ApiError on failure:
import { soloFetch } from 'arcway/lib/client';
const users = await soloFetch('/users');
const user = await soloFetch('/users/1');graphqlFetch
GraphQL fetch helper. Returns data, throws GraphQLError on failure:
import { graphqlFetch } from 'arcway/lib/client';
const result = await graphqlFetch(
'/graphql',
'query { users { id name } }',
{ /* variables */ },
{ /* headers */ },
);Error Classes
import { ApiError, GraphQLError } from 'arcway/lib/client';
try {
await soloFetch('/users');
} catch (err) {
if (err instanceof ApiError) {
err.status; // HTTP status code (e.g., 404)
err.code; // Error code string (e.g., 'NOT_FOUND')
err.message; // Error message
err.details; // Optional additional details
}
}
try {
await graphqlFetch('/graphql', 'query { ... }');
} catch (err) {
if (err instanceof GraphQLError) {
err.errors; // Array of GraphQL error objects
err.message; // First error message
}
}All Exports
Everything available from arcway/lib/client:
import {
// Provider
Provider, ApiProvider,
useApiContext,
// Data fetching
useApi,
useMutation,
useGraphQL,
useGraphQLMutation,
// Navigation
Router, Link,
useRouter, useQuery,
// Components
Head,
// Environment
useEnv, env,
// Utilities
useDebounce, useInterval, useLocalStorage, useClickOutside,
// Low-level
soloFetch, graphqlFetch,
ApiError, GraphQLError,
WsManager, useWsManager,
} from 'arcway/lib/client';