react-z-form

The fastest React form library — Lightweight, high-performance form state management powered by Zustand.
A modern alternative to Formik and React Hook Form. Zero re-renders, React 18 concurrent mode safe, TypeScript ready.
Why react-z-form?
| Re-renders | Minimal | Moderate | High |
| Bundle Size | ~3KB | ~9KB | ~13KB |
| React 18 Safe | Yes | Partial | No |
| Field Subscriptions | Yes | Yes | No |
| Zod/Yup Support | Yes | Yes | Yup only |
| TypeScript | Full | Full | Partial |
| Learning Curve | Easy | Medium | Medium |
Features
- Zero Re-renders — Fine-grained subscriptions mean only changed fields re-render
- React 18 Ready — Built with
useSyncExternalStore for concurrent mode
- Tiny Bundle — ~3KB minified + gzipped
- TypeScript First — Complete type definitions included
- Zod Integration — Built-in schema validation adapter
- No Dependencies on Redux — Powered by Zustand
- Works with Any UI — MUI, Chakra UI, Ant Design, Tailwind, or custom components
Installation
npm install react-z-form zustand immer
or
yarn add react-z-form zustand immer
or
pnpm add react-z-form zustand immer
Quick Start
import { FormProvider, Field, useForm } from "react-z-form";
function LoginForm() {
return (
<FormProvider form="login" initialValues={{ email: "", password: "" }}>
<FormContent />
</FormProvider>
);
}
function FormContent() {
const { submitWith, isSubmitting, isValid } = useForm();
const handleSubmit = async (values) => {
await api.login(values);
};
return (
<form onSubmit={(e) => { e.preventDefault(); submitWith(handleSubmit); }}>
<Field name="email" validate={(v) => (!v ? "Email is required" : undefined)}>
{({ input, meta }) => (
<div>
<input {...input} type="email" placeholder="Email" />
{meta.touched && meta.error && <span>{meta.error}</span>}
</div>
)}
</Field>
<Field name="password" validate={(v) => (!v ? "Password is required" : undefined)}>
{({ input, meta }) => (
<div>
<input {...input} type="password" placeholder="Password" />
{meta.touched && meta.error && <span>{meta.error}</span>}
</div>
)}
</Field>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Logging in..." : "Login"}
</button>
</form>
);
}
API Reference
Components
FormProvider
Wraps your form and provides context to all child components.
<FormProvider
form="myForm"
initialValues={{ name: "", email: "" }}
keepOnUnmount={false}
>
{children}
</FormProvider>
form | string | Yes | Unique form identifier |
initialValues | object | No | Initial form values |
keepOnUnmount | boolean | No | Keep form state after unmount |
Field
Render prop component for individual form fields.
<Field
name="email"
validate={(value) => !value ? "Required" : undefined}
validateOn="blur"
>
{({ input, meta }) => (
<>
<input {...input} />
{meta.touched && meta.error && <span>{meta.error}</span>}
</>
)}
</Field>
name | string | Yes | Field name |
initialValue | any | No | Initial value for this field |
validate | function | No | (value, allValues) => error | undefined |
validateOn | "change" | "blur" | "submit" | "all" | No | When to validate (default: "all") |
Render Props:
input.value | any | Current field value |
input.onChange | function | Change handler |
input.onBlur | function | Blur handler |
meta.touched | boolean | Field has been blurred |
meta.dirty | boolean | Value differs from initial |
meta.error | string | undefined | Validation error |
FormSpy
Watch the entire form state reactively.
<FormSpy>
{(state) => (
<pre>{JSON.stringify(state, null, 2)}</pre>
)}
</FormSpy>
Hooks
useForm(formName?)
Main hook for form control and state.
const {
values,
errors,
touched,
dirty,
isSubmitting,
submitCount,
isValid,
isDirty,
isTouched,
reset,
setErrors,
setFieldValue,
setFieldTouched,
setValues,
setFieldError,
submitWith,
} = useForm("myForm");
values | object | All form values |
errors | object | All validation errors |
touched | object | Touched state per field |
dirty | object | Dirty state per field |
isSubmitting | boolean | Form is submitting |
submitCount | number | Number of submit attempts |
isValid | boolean | No validation errors |
isDirty | boolean | Any field is dirty |
isTouched | boolean | Any field is touched |
reset(values?) | Reset form to initial or new values |
setFieldValue(field, value) | Set a single field value |
setFieldTouched(field, touched?) | Set field touched state |
setValues(values) | Set multiple values at once |
setFieldError(field, error) | Set a single field error |
setErrors(errors) | Set multiple errors at once |
submitWith(callback) | Handle async form submission |
useField(name, formName?)
Hook version of the Field component.
const { input, meta } = useField("email");
return <input {...input} />;
useFormState(formName?)
Subscribe to the entire form state.
const { values, errors, isSubmitting } = useFormState("myForm");
Validation
Inline Validation
<Field
name="email"
validate={(value) => {
if (!value) return "Email is required";
if (!/\S+@\S+\.\S+/.test(value)) return "Invalid email format";
return undefined;
}}
>
{({ input, meta }) => <input {...input} />}
</Field>
Zod Schema Validation
import { z } from "zod";
import { FormProvider, Field, useForm, zodField, zodForm } from "react-z-form";
const emailSchema = z.string().email("Invalid email address");
<Field name="email" validate={zodField(emailSchema)}>
{({ input, meta }) => <input {...input} />}
</Field>
const formSchema = z.object({
email: z.string().email("Invalid email"),
password: z.string().min(8, "Password must be at least 8 characters"),
});
function FormContent() {
const { submitWith, setErrors, values } = useForm();
const handleSubmit = async (values) => {
const errors = zodForm(formSchema)(values);
if (Object.keys(errors).length > 0) {
setErrors(errors);
return;
}
await api.submit(values);
};
return (
<form onSubmit={(e) => { e.preventDefault(); submitWith(handleSubmit); }}>
{/* fields */}
</form>
);
}
Validation Modes
Control when validation runs:
<Field name="email" validate={validate} validateOn="blur">
<Field name="email" validate={validate} validateOn="change">
// Validate on submit only (manual)
<Field name="email" validate={validate} validateOn="submit">
// Validate on both change and blur (default)
<Field name="email" validate={validate} validateOn="all">
Examples
Login Form
import { FormProvider, Field, useForm } from "react-z-form";
function LoginForm() {
return (
<FormProvider form="login" initialValues={{ email: "", password: "" }}>
<LoginFormContent />
</FormProvider>
);
}
function LoginFormContent() {
const { submitWith, isSubmitting, isValid } = useForm();
return (
<form onSubmit={(e) => {
e.preventDefault();
submitWith(async (values) => {
const response = await fetch("/api/login", {
method: "POST",
body: JSON.stringify(values),
});
if (!response.ok) throw new Error("Login failed");
});
}}>
<Field name="email" validate={(v) => !v ? "Required" : undefined}>
{({ input, meta }) => (
<div>
<label>Email</label>
<input {...input} type="email" />
{meta.touched && meta.error && <span className="error">{meta.error}</span>}
</div>
)}
</Field>
<Field name="password" validate={(v) => !v ? "Required" : undefined}>
{({ input, meta }) => (
<div>
<label>Password</label>
<input {...input} type="password" />
{meta.touched && meta.error && <span className="error">{meta.error}</span>}
</div>
)}
</Field>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Logging in..." : "Login"}
</button>
</form>
);
}
Registration Form with Zod
import { z } from "zod";
import { FormProvider, Field, useForm, zodField } from "react-z-form";
const schema = {
name: z.string().min(2, "Name must be at least 2 characters"),
email: z.string().email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
confirmPassword: z.string(),
};
function RegistrationForm() {
return (
<FormProvider
form="register"
initialValues={{ name: "", email: "", password: "", confirmPassword: "" }}
>
<RegistrationFormContent />
</FormProvider>
);
}
function RegistrationFormContent() {
const { submitWith, isSubmitting, values, setFieldError } = useForm();
return (
<form onSubmit={(e) => {
e.preventDefault();
// Cross-field validation
if (values.password !== values.confirmPassword) {
setFieldError("confirmPassword", "Passwords do not match");
return;
}
submitWith(async (values) => {
await fetch("/api/register", {
method: "POST",
body: JSON.stringify(values),
});
});
}}>
<Field name="name" validate={zodField(schema.name)}>
{({ input, meta }) => (
<div>
<input {...input} placeholder="Full Name" />
{meta.touched && meta.error && <span>{meta.error}</span>}
</div>
)}
</Field>
<Field name="email" validate={zodField(schema.email)}>
{({ input, meta }) => (
<div>
<input {...input} type="email" placeholder="Email" />
{meta.touched && meta.error && <span>{meta.error}</span>}
</div>
)}
</Field>
<Field name="password" validate={zodField(schema.password)}>
{({ input, meta }) => (
<div>
<input {...input} type="password" placeholder="Password" />
{meta.touched && meta.error && <span>{meta.error}</span>}
</div>
)}
</Field>
<Field name="confirmPassword">
{({ input, meta }) => (
<div>
<input {...input} type="password" placeholder="Confirm Password" />
{meta.touched && meta.error && <span>{meta.error}</span>}
</div>
)}
</Field>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Creating account..." : "Register"}
</button>
</form>
);
}
Programmatic Field Control
function DynamicForm() {
const { setFieldValue, setValues, reset, values } = useForm("dynamic");
return (
<div>
{/* Pre-fill from API */}
<button onClick={() => setValues({
name: "John Doe",
email: "john@example.com"
})}>
Load User Data
</button>
{/* Update single field */}
<button onClick={() => setFieldValue("status", "active")}>
Set Active
</button>
{/* Reset form */}
<button onClick={() => reset()}>
Clear Form
</button>
{/* Reset with new values */}
<button onClick={() => reset({ name: "", email: "", status: "pending" })}>
Reset to Defaults
</button>
</div>
);
}
With Material UI
import { TextField, Button, CircularProgress } from "@mui/material";
import { FormProvider, Field, useForm } from "react-z-form";
function MUIForm() {
return (
<FormProvider form="mui-form" initialValues={{ email: "" }}>
<MUIFormContent />
</FormProvider>
);
}
function MUIFormContent() {
const { submitWith, isSubmitting } = useForm();
return (
<form onSubmit={(e) => { e.preventDefault(); submitWith(console.log); }}>
<Field name="email" validate={(v) => !v ? "Required" : undefined}>
{({ input, meta }) => (
<TextField
{...input}
label="Email"
error={meta.touched && !!meta.error}
helperText={meta.touched && meta.error}
fullWidth
margin="normal"
/>
)}
</Field>
<Button
type="submit"
variant="contained"
disabled={isSubmitting}
startIcon={isSubmitting && <CircularProgress size={20} />}
>
Submit
</Button>
</form>
);
}
TypeScript
Full TypeScript support with generics:
import { FormProvider, Field, useForm } from "react-z-form";
interface LoginFormValues {
email: string;
password: string;
}
function LoginForm() {
const { values, submitWith } = useForm("login");
const typedValues = values as LoginFormValues;
return (
<form onSubmit={(e) => {
e.preventDefault();
submitWith(async (vals) => {
const typedVals = vals as LoginFormValues;
await login(typedVals.email, typedVals.password);
});
}}>
{/* ... */}
</form>
);
}
Migration from Other Libraries
From Formik
<Formik initialValues={{ email: "" }} onSubmit={handleSubmit}>
{({ values, handleChange, handleBlur }) => (
<input name="email" value={values.email} onChange={handleChange} onBlur={handleBlur} />
)}
</Formik>
<FormProvider form="myForm" initialValues={{ email: "" }}>
<Field name="email">
{({ input }) => <input {...input} />}
</Field>
</FormProvider>
From React Hook Form
const { register, handleSubmit } = useForm();
<input {...register("email")} />
const { submitWith } = useForm();
<Field name="email">
{({ input }) => <input {...input} />}
</Field>
Browser Support
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
Requires React 18+
Contributing
Contributions are welcome! Please read our contributing guidelines before submitting a PR.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature)
- Commit your changes (
git commit -m 'Add amazing feature')
- Push to the branch (
git push origin feature/amazing-feature)
- Open a Pull Request
License
MIT License - see the LICENSE file for details.
Links
Keywords: react form, react forms, form validation, react form library, zustand form, formik alternative, react hook form alternative, react 18 forms, typescript form, zod validation, form state management, react form builder, lightweight form library