React
Install the adapter alongside React 18.3 or newer:
pnpm add @omni-async/react
Query
import { useQuery } from "@omni-async/react";
function Search() {
const results = useQuery(
async (term: string) => {
const response = await fetch(`/api/search?q=${encodeURIComponent(term)}`);
if (!response.ok) throw new Error("Search failed");
return response.json() as Promise<string[]>;
},
{ initial: () => [] },
);
return (
<>
<button disabled={results.loading} onClick={() => void results.trigger("async")}>
Search
</button>
{results.error ? <p>Search failed</p> : null}
<ul>
{results.data.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</>
);
}
useQuery uses latest-request concurrency. Its initial function supplies initial data, and a
rejected request preserves the latest data while updating error.
Fetch on mount
const users = useFetch(
async (signal) => {
const response = await fetch("/api/users", { signal });
return response.json() as Promise<User[]>;
},
{ initial: () => [] },
);
useFetch starts on mount, aborts the previous fetch before a new one, and aborts during cleanup.
Server Functions
In a React app whose framework supports Server Functions, a Client Component can pass an imported
Server Function to useAction and call it after hydration:
"use client";
import { useAction } from "@omni-async/react";
import { saveUser } from "./actions"; // Exported from a "use server" module.
function SaveButton() {
const save = useAction(saveUser);
return (
<button disabled={save.loading} onClick={() => void save.trigger("Ada")}>
Save
</button>
);
}
Arguments and results must cross the server boundary in supported serializable forms. This hook
tracks the client call; React's useActionState provides previous-state handling, queued actions,
and form submission before hydration.