Skip to content

The fetch client returns standard promises, so it fits naturally into whatever async pattern your framework uses.

tsx
// React: pages/users/[id]/index.tsx
import { useLoaderData } from "react-router";
import fetchClients from "_/fetch";

const { GET } = fetchClients["users/[id]"];

// React Router passes { params, request } into the loader; read params the
// native way and pass them to the client as an array
export const loader = ({ params }) => GET([params.id]);

export default function UserProfile() {
  const user = useLoaderData();
  return <div>{user.name}</div>;
}

Types flow through these abstractions - loaders, resources, hooks, and components automatically know the response shape from your API definition.

KosmoJS only owns the envelope - the typed fetch client and how requests cross the wire. Everything above it is the framework's own model: Solid's query/createAsync, React Router's loader and useLoaderData, Vue's useLoaderData, Svelte's useLoaderData, MDX's useLoaderData - each reads through its native patterns, with no proprietary abstraction layered on top.

The client is just a typed function that takes the params array you build and returns a promise; where and how you call it is entirely the framework's.

These Integrations Work Under SSR Too ​

The client returns standard promises, so nothing above changes when the page is server-rendered.

What matters is when the fetch fires: loaders and preloaded resources run during the SSR render, so they take the in-process path and their result is reused on hydration rather than refetched.

A fetch in useEffect / onMounted does not run during SSR - it fetches in the browser after hydration, like a plain CSR app.

Isomorphic clients ›

Suspense Is Your Responsibility ​

Solid's createAsync (like createResource) suspends: it reports its pending state to the nearest <Suspense> boundary and propagates errors to the nearest <ErrorBoundary>.

KosmoJS does not provide either for you - the seeded App boilerplate renders its children directly, deliberately not wrapping the app in <Suspense>, because one app-wide boundary is an anti-pattern: any pending fetch anywhere collapses the whole page to a single fallback and unrelated async work shares one loading state.

Scope the boundary to the data component or a sensible subtree yourself:

SolidJS
tsx
import { Suspense } from "solid-js";
import { createAsync, useParams } from "@solidjs/router";

export default function UserProfile() {
  const params = useParams();
  const user = createAsync(() => getUser(params.id));

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <div>{user()?.name}</div>
    </Suspense>
  );
}

React's loader/useLoaderData resolves before render and does not suspend, so it needs no boundary unless you reach for React.lazy or a promise-throwing use().

The same holds for Vue, Svelte, and MDX loaders - they resolve before render, so only Solid's createAsync needs a boundary in the common case.

Wrapping the whole app in one boundary does work if you accept the tradeoff - it is your call, not a default KosmoJS makes for you.

See Data Preloading for the full breakdown.

Released under the MIT License.