React
Browser reads over HTTP. No db. Hooks stay a typed binding.
pnpm add @usegraft/sdk-reactnpm i @usegraft/sdk-reactbun add @usegraft/sdk-reactEvery other Graft SDK reads on a server. This one reads in the browser, for the places a loader cannot help: a search box, a client-side editor preview, a widget on a page the server rendered an hour ago.
react is a peer dependency, 18 or 19.
Create the handle
// src/graft.ts
import { createGraft, createGraftHooks } from "@usegraft/sdk-react";
import { collections } from "../graft.config";
export const graft = createGraft({
endpoint: "https://cms.example.com/api/content/v1",
collections,
});
export const { GraftProvider, useGraft, useContent, useContentList, useContentSearch } =
createGraftHooks(graft);
Types do not cross the wire. Your app imports its own collections at compile
time, exactly as a server adapter does. The endpoint supplies data at runtime.
The hooks come out of a factory rather than being importable directly, and that is what keeps the reads typed.
Read one document
import { useContent } from "../graft";
function Doc({ slug }: { slug: string }) {
const { data, error, loading } = useContent("docs", slug);
if (loading) return <Spinner />;
if (error) return <Problem error={error} />;
if (!data) return <NotFound />;
return <article>{data.data.title}</article>;
}
Each hook reports { data, error, loading, refresh }. Changing the slug
clears data rather than showing the previous document while the next one
loads.
endpoint points at a mount of Graft's read-only content API — what
graft serve exposes at /api/content/v1, or your own mount of
createContentApiHandler. See Content API.
Mount functions and MCP
There is no graftRoute. Mounting a request handler is a server's job. Use
the adapter for the framework that serves the page.
Cache and invalidation
There is none, by construction. No deduplication, no retry, no
stale-while-revalidate. graft.getContent is a plain async function, so
compose it with TanStack Query or SWR if you want those.
useQuery({
queryKey: ["docs", slug],
queryFn: () => graft.getContent("docs", slug),
});
The caveat you cannot infer
There is no db option. A database handle in a browser bundle is a database
URL in a browser bundle.
There is also no branch option. An endpoint is pinned to one branch on the
server. Passing branch throws CONFIG_INVALID. Point at the preview
deployment's own endpoint instead.
useEffect does not run during server rendering, so on the server these hooks
report their loading state and nothing else. Content that has to be in the
HTML belongs in a loader or a server adapter.
See SDK reference for the shared surface and the honest gaps.