Skip to content

beyondoss/openapi-react

Repository files navigation

@beyond.dev/openapi-react

Fetch, cache, and mutate data from OpenAPI-typed services with full type safety.

Built on openapi-fetch. Every path, parameter, and response is inferred from your OpenAPI schema — no manual typing.

Install

npm install @beyond.dev/openapi-react openapi-fetch openapi-typescript-helpers

Quick Start

Generate types from your OpenAPI spec (see openapi-typescript), then create a client:

import { createClient } from "@beyond.dev/openapi-react";
import type { paths } from "./api.d.ts"; // generated by openapi-typescript

export const api = createClient<paths>({
  baseUrl: "https://api.example.com",
});

Fetch data in a component with useLoader (requires a <Suspense> boundary):

import { Suspense } from "react";
import { api } from "./api";

function UserList() {
  const { data } = api.useLoader({
    path: "GET /users",
    input: { query: { limit: 20 } },
  });

  return <ul>{data.items.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}

export default function App() {
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <UserList />
    </Suspense>
  );
}

Hooks

useLoader

Cached GET with Suspense support. Suspends while loading, throws on error.

const { data, status, fetchStatus, invalidate, refetch } = api.useLoader({
  path: "GET /users/{id}",
  input: { path: { id: "u_123" } },
  staleTime: 5000, // ms before re-fetching (default: 1000)
  disabled: false, // skip fetch entirely
  refetchOnMount: true, // re-fetch when component mounts (default: true)
  refetchOnFocus: true, // re-fetch on window focus (default: true)
  refetchOnReconnect: true, // re-fetch on network reconnect (default: true)
  refetchInterval: 30000, // poll every N ms, or (data) => ms | false
});
Field Type Description
data T Response data
status "success" | "error" | "disabled" Logical render state (see note below)
fetchStatus "fetching" | "refetching" | "success" | "error" | "uncached" Raw network state (see note below)
invalidate() () => void Mark stale, triggers background refetch
refetch() () => Promise<CachedResponse> Force immediate refetch

status vs fetchStatus

status is the logical render state — it collapses "refetching" into "success" (stale data stays visible during a background refresh) and is what you branch on in JSX. fetchStatus is the raw network state and exposes "refetching" separately, useful for showing a subtle background-refresh indicator without hiding existing data. When disabled: true, fetchStatus is "uncached".

// Branch on status for rendering decisions:
if (result.status === "error") return <ErrorMessage />;

// Use fetchStatus to layer a background-refresh hint over existing data:
{
  result.fetchStatus === "refetching" && <Spinner size="sm" />;
}
{
  result.data && <UserList users={result.data} />;
}

useInlineLoader

Same as useLoader but without Suspense. Returns a discriminated union instead.

const result = api.useInlineLoader({
  path: "GET /users/{id}",
  input: { path: { id: "u_123" } },
});

if (result.status === "fetching") return <Spinner />;
if (result.status === "error") return <Error data={result.error} />;
return <User data={result.data} />;

The "success" variant includes lastError — the previous error if a refetch succeeds after a failure.

useAction

Uncached mutations. Returns a send function and the current status.

const { send, status } = api.useAction({
  path: "POST /users",
  onSuccess(data, response) {/* redirect, update cache, etc. */},
  onError(error, response) {/* show toast, etc. */},
  onSettled(data, error, response) {/* runs on both success and error */},
});

// Call send with typed input
const user = await send({
  body: { name: "Alice", email: "alice@example.com" },
});
Field Type Description
send (input, requestInit?) => Promise<T> Execute the request
status "idle" | "fetching" | "success" | "error" Current mutation state

send throws ErrorResponse on failure. The onError callback receives the typed error data.

onSettled(data, error, response) runs after every send regardless of outcome — useful for cleanup that must always happen (closing a modal, resetting a form). data is defined on success; error is defined on failure.

Cache Operations

load

Fetch programmatically. Respects staleTime — returns cached data if fresh.

const cached = await api.load({
  path: "GET /users",
  staleTime: 0, // force refetch
});

hydrate

Pre-populate the cache without a network request.

Note: hydrate is a no-op when the cache already holds a successful entry for that key. To overwrite existing data, call purge first then hydrate.

api.hydrate({
  path: "GET /users/{id}",
  data: { id: "u_123", name: "Alice" },
  input: { path: { id: "u_123" } },
});

invalidate

Mark a cache entry stale. Components subscribed to that key will refetch in the background.

// Exact match
api.invalidate({ path: "GET /users/{id}", input: { path: { id: "u_123" } } });

// Pattern match — invalidate all /users entries
api.invalidate({
  match({ path }) {
    return path.startsWith("GET /users");
  },
});

refetch

Invalidate and immediately re-fetch.

await api.refetch({ path: "GET /users" });

// Pattern match
await api.refetch({
  match({ path }, mountCount) {
    return path === "GET /users" && mountCount > 0;
  },
});

The match callback receives the parsed cache key and the current mount count (refCount).

purge

Remove entries from the cache entirely.

api.purge({ path: "GET /users/{id}", input: { path: { id: "u_123" } } });

url

Build a typed URL string from a path and parameters.

const href = api.url({
  path: "GET /users/{id}",
  input: { path: { id: "u_123" }, query: { expand: "roles" } },
});
// "https://api.example.com/users/u_123?expand=roles"

Configuration

const api = createClient<paths>({
  baseUrl: "https://api.example.com",

  // How long cached data is considered fresh (ms). Default: 1000.
  staleTime: 5000,

  // How long to keep unused cache entries after all subscribers unmount (ms). Default: 300_000.
  cacheTime: 60_000,

  // Max retries on 5xx errors. Default: 3.
  retries: 2,

  // Custom retry predicate. Return false to abort, true/void to retry.
  shouldRetry(error, retryCount) {
    return error.response?.status !== 429;
  },

  // Transform all responses before caching.
  transform: camelize,

  // Default request options (credentials, mode, headers).
  requestInit: () => ({
    credentials: "include",
    headers: { "X-App-Version": "1.0" },
  }),

  // Custom query string serializer.
  querySerializer: createQuerySerializer({ array: { style: "form" } }),

  // Extend cache keys — useful for user-scoped caches.
  extendCacheKey: ({ path, input }) => ({ path, input, userId: getUser().id }),

  // Global callbacks.
  onEachSuccess(data) {},
  onEachError(error) {},

  debug: true, // Log requests and responses to the console.
});

Utilities

camelize

Recursively converts snake_case keys to camelCase.

import { camelize } from "@beyond.dev/openapi-react";

camelize({ user_id: "u_1", created_at: "2024-01-01" });
// => { userId: "u_1", createdAt: "2024-01-01" }

Use as a global transform to normalize API responses across all hooks:

const api = createClient<paths>({ baseUrl, transform: camelize });

snakenize

Shallow converts camelCase keys to snake_case (top-level keys only).

import { snakenize } from "@beyond.dev/openapi-react";

snakenize({ userId: "u_1", firstName: "Alice" });
// => { user_id: "u_1", first_name: "Alice" }

Use it when building request bodies that expect snake_case from camelCase inputs.

Error Handling

Failed requests throw ErrorResponse<T>, which carries the typed error payload.

import { ErrorResponse } from "@beyond.dev/openapi-react";

try {
  await api.load({ path: "GET /users/{id}", input: { path: { id: "x" } } });
} catch (err) {
  if (err instanceof ErrorResponse) {
    console.log(err.data); // typed error body
    console.log(err.response); // raw Response
  }
}

With useLoader (Suspense mode), errors propagate to the nearest error boundary. With useInlineLoader, they surface as status: "error" with result.error typed to your schema's error shape.

About

Suspenseful React hooks for openapi-fetch

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors