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.