React Package

Quick Start

Get started with headless Goodie Forms React.

Install Dependencies

  1. Install Goodie Forms
pnpm i @goodie-forms/core @goodie-forms/react
  1. Install a Validation Library (totally optional) See supported libs
pnpm i zod # Or any other validation lib

Create your Validation Schema

const LoginFormSchema = z.object({
  email: z.email(),
  password: z.string().nonempty(),
});

Create form with useForm hook

const form = useForm(
  {
    validationSchema: LoginFormSchema,
  },
  {
    validateMode: "onChange",
    revalidateMode: "onChange",
  },
);

Render Fields with FieldRenderer component

<FieldRenderer
  form={form}
  path={form.path.of("email")}
  defaultValue={""}
  render={({ fieldProps, field, form }) => (
    <div>
      <label htmlFor="email">E-mail</label>

      <input
        {...fieldProps}
        id="email"
        type="email"
        disabled={form.controller.isSubmitting}
      />

      {field.issues && <span>{field.issues.at(0)?.message}</span>}
    </div>
  )}
/>;

Create submission handler

<form
  onSubmit={form.controller.createSubmitHandler(
    async (data) => {
      console.log("Logging in with", data);
      await api.login(data);
    },
    async (issues) => {
      console.log("Form has issues:", issues);
    },
  )}
>
  ...
</form>;

Combine them all

import { useForm, FieldRenderer } from "@goodie-forms/react";
import z from "zod";

const LoginFormSchema = z.object({
  email: z.email(),
  password: z.string().nonempty(),
});

export function App() {
  const form = useForm(
    {
      validationSchema: LoginFormSchema,
    },
    {
      validateMode: "onChange",
      revalidateMode: "onChange",
    },
  );

  const handleSubmit = form.controller.createSubmitHandler(
    async (data) => {
      console.log("Logging in with", data);
      await api.login(data);
    },
    async (issues) => {
      console.log("Form has issues:", issues);
    },
  );

  return (
    <main>
      <form onSubmit={handleSubmit}>
        <FieldRenderer
          form={form}
          path={form.path.of("email")}
          defaultValue={""}
          render={({ fieldProps, field, form }) => (
            <div>
              <label htmlFor="email">E-mail</label>
              <input
                {...fieldProps}
                id="email"
                type="email"
                disabled={form.controller.isSubmitting}
              />
              {field.issues && <span>{field.issues.at(0)?.message}</span>}
            </div>
          )}
        />

        <FieldRenderer
          form={form}
          path={form.path.of("password")}
          defaultValue={""}
          render={({ fieldProps, field, form }) => (
            <div>
              <label htmlFor="password">Password</label>
              <input
                {...fieldProps}
                id="password"
                type="password"
                disabled={form.controller.isSubmitting}
              />
              {field.issues && <span>{field.issues.at(0)?.message}</span>}
            </div>
          )}
        />

        <button type="submit">Login</button>
      </form>
    </main>
  );
}
Copyright © 2026