npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@beignet/react-form

v0.0.56

Published

TanStack Form integration for Beignet with Standard Schema support

Downloads

426

Readme

@beignet/react-form

Runtime: Beignet requires Node.js 22.12 or newer. Bun is optional.

TanStack Form integration for Beignet

[!CAUTION] Beignet is experimental alpha software. The 0.0.x package line is for early evaluation, and APIs may change between releases while the framework settles.

This package turns a contract body schema into typed TanStack Form options. TanStack Form accepts Standard Schema validators natively, so the contract's Zod, Valibot, or ArkType body schema becomes the form validator without a resolver package.

If your app already uses React Hook Form, use @beignet/react-hook-form instead. Both adapters bind the same contracts. To switch an existing app, follow the TanStack Form setup guide and retain its existing form helpers until their components have been migrated.

Installation

npm install @beignet/react-form @beignet/core @tanstack/react-form@^1.33.5 react

TanStack Form 1.33.5 or newer in the 1.x line is required so a failed submit can be retried after setting a server error.

TypeScript requirements

This package requires TypeScript 5.0 or higher for proper type inference.

Agent skills

This package ships a TanStack Intent skill for coding agents: @beignet/react-form#forms. Load it when adding contract-backed forms, TanStack Form setup, validateOn slots, serverErrorMap, React Query mutations, form.Field rendering, feature component boundaries, or field/server error mapping in a Beignet app.

Usage

Basic form

These examples use the Todos contracts from the Beignet starter. Create accepts title; completion belongs to the update contract. For a working form with verification steps, follow the form guide.

"use client";

import { createReactForm, fieldErrorMessages } from "@beignet/react-form";
import { createTodo } from "@/features/todos/contracts";

const rf = createReactForm();
const createTodoForm = rf(createTodo);

function CreateTodoForm() {
  const form = createTodoForm.useForm({
    defaultValues: { title: "" },
    onSubmit: ({ value }) => {
      // value is the schema input: { title: string }
      console.log("Creating todo:", value);
    },
  });

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        void form.handleSubmit();
      }}
    >
      <form.Field name="title">
        {(field) => (
          <>
            <input
              value={field.state.value}
              onBlur={field.handleBlur}
              onChange={(event) => field.handleChange(event.target.value)}
              placeholder="What needs to be done?"
            />
            {fieldErrorMessages(field.state.meta.errors).map((message) => (
              <p className="error" key={message}>
                {message}
              </p>
            ))}
          </>
        )}
      </form.Field>

      <form.Subscribe selector={(state) => state.isSubmitting}>
        {(isSubmitting) => (
          <button type="submit" disabled={isSubmitting}>
            Create Todo
          </button>
        )}
      </form.Subscribe>
    </form>
  );
}

With React Query mutation

"use client";

import {
  createReactForm,
  fieldErrorMessages,
  serverErrorMap,
} from "@beignet/react-form";
import { useMutation } from "@tanstack/react-query";
import { rq } from "@/client";
import { createTodo, listTodos } from "@/features/todos/contracts";

const rf = createReactForm();
const createTodoForm = rf(createTodo);

function CreateTodoForm() {
  const mutation = useMutation(
    rq(createTodo).mutationOptions({
      invalidates: () => [rq(listTodos).contractFilter()],
    }),
  );

  const form = createTodoForm.useForm({
    defaultValues: { title: "" },
    onSubmit: async ({ value, formApi }) => {
      try {
        await mutation.mutateAsync({ body: value });
        formApi.reset();
      } catch (error) {
        formApi.setErrorMap(serverErrorMap(error, "Could not create the todo."));
      }
    },
  });

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        void form.handleSubmit();
      }}
    >
      <form.Field name="title">
        {(field) => (
          <>
            <input
              value={field.state.value}
              onBlur={field.handleBlur}
              onChange={(event) => field.handleChange(event.target.value)}
              placeholder="Title"
              readOnly={mutation.isPending}
            />
            {fieldErrorMessages(field.state.meta.errors).map((message) => (
              <p key={message}>{message}</p>
            ))}
          </>
        )}
      </form.Field>

      <form.Subscribe selector={(state) => state.errorMap.onServer}>
        {(message) => (message ? <p>{message}</p> : null)}
      </form.Subscribe>

      <button type="submit" disabled={mutation.isPending}>
        {mutation.isPending ? "Creating..." : "Create"}
      </button>
    </form>
  );
}

TanStack Form only owns request body fields. Pass path params, query params, headers, and auth-derived values to the endpoint call or mutation variables. Submit failures come back as Beignet client errors, so map route-owned catalog errors, framework validation errors, network failures, and contract drift through serverErrorMap(...) and formApi.setErrorMap(...). TanStack Form clears the previous server error when the next submit validates cleanly, so a retry never shows stale copy.

Choosing when the schema runs

TanStack Form validates per slot. The adapter installs the body schema in one async slot chosen by validateOn, which defaults to "submit":

const form = createTodoForm.useForm({
  validateOn: "change", // "submit" | "change" | "blur" | "dynamic" | false
  defaultValues: { title: "" },
});

Use "dynamic" to validate on submit and revalidate on change after the first submission. It runs through TanStack Form's validationLogic, which the adapter defaults to revalidateLogic(); pass your own strategy to change the timing:

import { revalidateLogic } from "@tanstack/react-form";

const form = createTodoForm.useForm({
  validateOn: "dynamic",
  validationLogic: revalidateLogic({ mode: "blur" }),
  defaultValues: { title: "" },
});

Set validateOn: false to keep the form typing without the schema, for example in partial or multi-step flows where the use case still validates.

The schema uses onSubmitAsync, onChangeAsync, onBlurAsync, or onDynamicAsync, so synchronous schemas and async refinements both work. TanStack Form awaits validation before calling onSubmit; form values remain the schema input. Error-map keys still use the event name, such as form.state.errorMap.onSubmit.

Your own validators compose with the schema. A validator in the selected async slot replaces it; for example, validators.onSubmitAsync overrides the schema with the default validateOn: "submit". Synchronous validators such as onSubmit run first, and TanStack Form skips async validation if they fail unless you set asyncAlways: true. Validators in other slots remain intact.

Composing with useAppForm and withForm

formOptions(...) returns plain TanStack Form options, so it spreads into useForm from @tanstack/react-form, an app-level useAppForm created with createFormHook, or withForm:

import { useForm } from "@tanstack/react-form";

const form = useForm(
  createTodoForm.formOptions({
    defaultValues: { title: "" },
    validateOn: "blur",
  }),
);

Using contract config directly

You can pass either a contract builder or its config:

const { useForm } = rf(createTodo);

// Or using the contract config
const { useForm } = rf(createTodo.config);

API reference

createReactForm()

Creates a TanStack Form adapter factory.

const rf = createReactForm();

rf(contract)

Creates a TanStack Form adapter for a contract whose body schema input is an object. TypeScript rejects contracts with scalar or array body inputs because they do not define named form fields. The schema output may still transform to any value.

const adapter = rf(createTodo);

adapter.formOptions(options?)

Returns TanStack Form options with the contract body schema installed as a Standard Schema validator.

const options = adapter.formOptions({
  defaultValues: { title: "" },
  validateOn: "blur",
});

validateOn defaults to "submit"; it also accepts "change", "blur", "dynamic", or false. Pass other TanStack Form options such as validators, listeners, onSubmit, and onSubmitMeta in the same object.

adapter.useForm(options?)

Calls useForm from @tanstack/react-form with adapter.formOptions(options).

serverErrorMap(error, fallback, overrides?)

Maps a failed endpoint call or mutation error to the { onServer } shape that form.setErrorMap(...) accepts. Wraps contractErrorMessage from @beignet/core/client: non-contract errors return the fallback copy, client-side input validation failures return a generic "check the highlighted fields" message, and catalog codes can override copy per form.

formApi.setErrorMap(
  serverErrorMap(error, "Could not update profile.", {
    HANDLE_UNAVAILABLE: "That handle is already taken.",
  }),
);

fieldErrorMessages(errors)

Flattens a TanStack Form error list such as field.state.meta.errors into strings. Standard Schema validators produce issue objects, function validators usually return strings, and empty slots are undefined; all three are handled.

Type inference

Form values come from the contract body schema input. TanStack Form validates values but never transforms them, so defaultValues, field state, and the value passed to onSubmit all share the input type:

const createPayment = payments
  .post("/api/payments")
  .body(
    z.object({
      amount: z.string().transform(Number),
      note: z.string().optional(),
    }),
  )
  .responses({ 201: PaymentSchema });

const form = rf(createPayment).useForm({
  defaultValues: { amount: "" }, // input: string
  onSubmit: ({ value }) => {
    value.amount; // string (input)
  },
});

The typed client posts the schema input and the server transforms it, so the onSubmit value is always valid as the contract body for mutation.mutate({ body: value }), transforms included.

The schema input must be object-shaped. Keep lists and scalar values inside named fields rather than making the request body itself an array or scalar:

const replaceTags = todos
  .put("/api/todos/:id/tags")
  .body(z.object({ tags: z.array(z.string()) }))
  .responses({ 200: TodoSchema });

const form = rf(replaceTags).useForm({
  defaultValues: { tags: [] },
});

Field names are checked against the bound contract. For the starter's create contract, title is the only field; description would be a type error:

function TodoTitleField() {
  const form = rf(createTodo).useForm({ defaultValues: { title: "" } });

  return (
    <form.Field name="title">
      {(field) => (
        <label>
          Title
          <input
            value={field.state.value}
            onBlur={field.handleBlur}
            onChange={(event) => field.handleChange(event.target.value)}
          />
        </label>
      )}
    </form.Field>
  );
}

Error state

Field issues appear on the matching form.Field in field.state.meta.errors. Form-level results appear in form.state.errorMap under the slot the schema runs in, keyed by field path. Server errors live in form.state.errorMap.onServer as a string.

<form.Subscribe selector={(state) => state.errorMap.onServer}>
  {(message) => (message ? <p role="alert">{message}</p> : null)}
</form.Subscribe>

Related packages

License

MIT