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.

PackageUse it for
@beignet/react-queryTyped TanStack Query options, mutations, prefetching, and query keys
@beignet/nuqsURL-backed search, filters, tabs, sorting, and pagination
@beignet/react-hook-formTyped React Hook Form setup from a contract body schema
@beignet/react-uploadsTyped 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.ts

Feature-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