React
Beignet has optional React integrations for server state, URL state, forms, and uploads. Each package is independent, so install only the pieces your app needs.
| Package | Use it for |
|---|---|
@beignet/react-query | Typed TanStack Query options, mutations, prefetching, and query keys |
@beignet/nuqs | URL-backed search, filters, tabs, sorting, and pagination |
@beignet/react-hook-form | Typed React Hook Form setup from a contract body schema |
@beignet/react-uploads | Typed upload state, progress, errors, and results from a Beignet upload client |
Adapter shape
Every adapter follows the same shape: create the package adapter once, then bind contracts or upload names from the returned helper.
const rq = createReactQuery(client);
const rhf = createReactHookForm();
const nq = createNuqs();
const reactUploads = createReactUploads({ uploads });Feature workflow
Keep shared adapter factories in client/ and product UI in the feature:
client/
auth-client.ts
forms.ts
index.ts
features/
todos/
client/
queries.ts
components/
todo-app.tsx
contracts.tsFeature-specific query options and hooks can live under
features/<feature>/client/ when colocating data-fetching workflows keeps the
component simpler. For tiny features, importing the contract directly from the
component is fine.
// features/todos/client/queries.ts
import { rq } from "@/client";
import { createTodo, listTodos } from "@/features/todos/contracts";
export function listTodosQueryOptions() {
return rq(listTodos).queryOptions({ query: {} });
}
export function createTodoMutationOptions() {
return rq(createTodo).mutationOptions();
}The feature component imports the feature client helper and uses shared client adapters:
"use client";
import { useMutation, useQuery } from "@tanstack/react-query";
import { rhf } from "@/client/forms";
import {
createTodoMutationOptions,
listTodosQueryOptions,
} from "@/features/todos/client/queries";
import { createTodo } from "@/features/todos/contracts";
const createTodoForm = rhf(createTodo);
export function TodoApp() {
const todosQuery = useQuery(listTodosQueryOptions());
const form = createTodoForm.useForm({ defaultValues: { title: "" } });
const createTodoMutation = useMutation(createTodoMutationOptions());
const onSubmit = form.handleSubmit((body) => {
createTodoMutation.mutate({ body });
});
return <form onSubmit={onSubmit}>{/* fields */}</form>;
}This is feature colocation, not server colocation. Components can import
contracts, feature client helpers, and frontend helpers, but they should not
import use cases, route groups, infra adapters, provider packages, server/,
or app-context.ts.
Server context and prefetching
When a Next.js layout or Server Component needs request-scoped Beignet state,
use an app-owned server-only helper such as lib/server-context.ts that wraps
server.createContextFromNext() in React cache(...). Reading ctx.auth,
ctx.tenant, or request metadata there is fine for redirects and shell state;
feature data and business workflows should still go through use cases.
When a Server Component needs to hydrate React Query data and the route is a
thin { contract, useCase } binding, keep the contract-derived query key from
rq(contract).queryOptions(...) and replace only the server queryFn with an
app-owned lib/server-react-query.ts helper that consumes the request context.
Use the normal HTTP rq(contract).queryOptions(...) path when the route handler
owns response mapping, headers, streaming, or other HTTP-layer behavior.
Install
bun add @beignet/react-query @tanstack/react-query
bun add @beignet/nuqs nuqs
bun add @beignet/react-hook-form react-hook-form @hookform/resolvers
bun add @beignet/react-uploads