Arcwayv0.3.0

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:

PropTypeDefaultDescription
pathPrefixstring''Prefix for all API paths
headersobjectDefault HTTP headers for all requests
wsUrlstringWebSocket 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:

ParameterTypeDescription
pathstring | nullAPI endpoint. Pass null to disable fetching.
queryobjectOptional query parameters
optionsobjectOptional SWR config plus { disable, ws }

Returns:

PropertyTypeDescription
dataanyResponse data
errorError | nullError if request failed
loadingbooleanTrue during initial load
isLoadingbooleanSame as loading
isValidatingbooleanTrue during any fetch (including revalidation)
mutatefunctionManually revalidate or update cached data
postfunctionPOST request, auto-revalidates after
putfunctionPUT request, auto-revalidates after
patchfunctionPATCH request, auto-revalidates after
delfunctionDELETE request, auto-revalidates after

Inline mutationspost, 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 null to 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:

ParameterTypeDescription
pathstringAPI endpoint
optionsobjectOptional { method } (default: 'POST')

Returns: { data, error, loading, isMutating, trigger, reset }

  • trigger(body?) — execute the mutation with optional request body
  • reset() — 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:

ParameterTypeDescription
querystring | nullGraphQL query string. Pass null to disable.
variablesobjectOptional GraphQL variables
optionsobjectOptional 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 }

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:

PropertyTypeDescription
pathnamestringCurrent URL path
paramsobjectRoute parameters
queryobjectURL query parameters
push(to, options?)functionNavigate to a new URL. to can be a string or { pathname, query }
replace(to, options?)functionNavigate with history replace. to can be a string or { pathname, query }
back()functionGo back in history
forward()functionGo forward in history
refresh()functionReload 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 parameters
  • setQuery(updates, options?) — merges updates into URL query string. Set value to null to remove it. Pass { replace: true } to replace all params.

Forms

Manage form state with plain React state and submit handlers in your components.

Returns:

PropertyTypeDescription
valuesobjectCurrent form values
errorsobjectField errors by name
touchedobjectWhich fields have been interacted with
isDirtybooleanWhether form has changed from initial values
isSubmittingbooleanWhether submission is in progress
setField(name, value)functionSet a single field value
setError(name, message)functionSet a field error manually
handleSubmit(e?)functionForm submission handler
reset(newValues?)functionReset to initial values (or new values)

Components

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';

On this page