Formbit is a lightweight React state form library designed to simplify form management within your applications. With Formbit, you can easily handle form state, validate user input, and submit data efficiently.
- Intuitive and easy-to-use form state management.
- Out of the box support for validation with yup.
- Full TypeScript generics —
useFormbit<FormData>(...)infers paths, values, and callbacks. - Support for handling complex forms with dynamic and nested fields via dot-path notation.
- Context Provider for sharing form state across deeply nested component trees.
- Seamless and flexible integration with React — works with Antd, MaterialUI, or plain HTML.
npm install --save formbityarn add formbitThree steps: define a schema, call the hook, bind the UI.
import * as yup from 'yup';
import useFormbit from '@radicalbit/formbit';
// 1. Define a Yup schema and infer the TypeScript type from it
const schema = yup.object({
name: yup.string().max(25, 'Max 25 characters').required('Name is required'),
age: yup.number().max(120, 'Must be 0–120').required('Age is required'),
});
type FormData = yup.InferType<typeof schema>;
const initialValues: Partial<FormData> = { name: undefined, age: undefined };
// 2. Call the hook with generics so every callback is fully typed
function Example() {
const { form, submitForm, write, error, isDirty } = useFormbit<FormData>({
initialValues,
yup: schema,
});
const handleChangeName = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
write('name', value);
};
const handleChangeAge = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
write('age', Number(value));
};
const handleSubmit = () => {
submitForm(
({ form }) => console.log('Validated form:', form),
({ errors }) => console.error('Validation errors:', errors),
);
};
// 3. Bind inputs, errors, and submit — Formbit stays out of your UI
return (
<div>
<label htmlFor="name">Name</label>
<input
id="name"
type="text"
value={form.name ?? ''}
onChange={handleChangeName}
/>
<div>{error('name')}</div>
<label htmlFor="age">Age</label>
<input
id="age"
type="number"
value={form.age ?? ''}
onChange={handleChangeAge}
/>
<div>{error('age')}</div>
<button disabled={!isDirty} onClick={handleSubmit} type="button">
Submit
</button>
</div>
);
}
export default Example;Use FormbitContextProvider when you need to share form state across deeply nested components without prop drilling.
import { FormbitContextProvider, useFormbitContext } from '@radicalbit/formbit';
import * as yup from 'yup';
const schema = yup.object({
name: yup.string().required('Name is required'),
surname: yup.string().required('Surname is required'),
age: yup.number().required('Age is required'),
});
type FormData = yup.InferType<typeof schema>;
const initialValues: Partial<FormData> = { name: undefined, surname: undefined, age: undefined };
// Wrap your form tree with the provider
function App() {
return (
<FormbitContextProvider<FormData>
initialValues={initialValues}
yup={schema}
>
<NameField />
<SubmitButton />
</FormbitContextProvider>
);
}
// Any child can access form state without props
function NameField() {
const { form, write, error } = useFormbitContext<FormData>();
const handleChangeName = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
write('name', value);
};
return (
<div>
<input
value={form.name ?? ''}
onChange={handleChangeName}
/>
<span>{error('name')}</span>
</div>
);
}
function SubmitButton() {
const { submitForm, isDirty } = useFormbitContext<FormData>();
return (
<button
disabled={!isDirty}
onClick={() => submitForm(({ form }) => console.log(form))}
>
Submit
</button>
);
}Start with empty initial values and call initialize() once data arrives from an API.
import { useEffect, useState } from 'react';
import useFormbit from '@radicalbit/formbit';
import * as yup from 'yup';
const schema = yup.object({
name: yup.string().required(),
email: yup.string().email().required(),
});
type FormData = yup.InferType<typeof schema>;
const initialValues: Partial<FormData> = { name: undefined, email: undefined };
function EditUserForm({ userId }: { userId: string }) {
const { form, write, error, initialize, submitForm } = useFormbit<FormData>({
initialValues,
yup: schema,
});
const [loading, setLoading] = useState(true);
// Fetch and initialize — resetForm() will revert to these values
useEffect(() => {
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((user) => { initialize(user); setLoading(false); });
}, [userId]);
const handleChangeName = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
write('name', value);
};
const handleChangeEmail = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
write('email', value);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
submitForm(({ form }) => console.log(form));
};
if (loading) return <p>Loading...</p>;
return (
<form onSubmit={handleSubmit}>
<input value={form.name ?? ''} onChange={handleChangeName} />
<div>{error('name')}</div>
<input value={form.email ?? ''} onChange={handleChangeEmail} />
<div>{error('email')}</div>
<button type="submit">Save</button>
</form>
);
}Use __metadata to store step state and validateAll to gate navigation between steps.
import useFormbit from '@radicalbit/formbit';
import * as yup from 'yup';
const schema = yup.object({
name: yup.string().required('Name is required'),
age: yup.number().required('Age is required'),
email: yup.string().email().required('Email is required'),
});
type FormData = yup.InferType<typeof schema>;
const initialValues: Partial<FormData> & { __metadata: { step: number } } = {
name: undefined,
age: undefined,
email: undefined,
__metadata: { step: 0 },
};
function MultiStepForm() {
const { form, write, error, validateAll, submitForm } = useFormbit<FormData>({
initialValues,
yup: schema,
});
const step = (form.__metadata?.step as number) ?? 0;
const goTo = (n: number) => write('__metadata.step', n);
// Validate only the current step's fields before advancing
const next = (paths: string[]) => {
validateAll(paths, {
successCallback: () => goTo(step + 1),
});
};
const handleChangeName = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
write('name', value);
};
const handleChangeAge = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
write('age', Number(value));
};
const handleChangeEmail = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
write('email', value);
};
const handleSubmit = () => {
submitForm(({ form }) => console.log('Submit:', form));
};
return (
<div>
{step === 0 && (
<div>
<input value={form.name ?? ''} onChange={handleChangeName} />
<div>{error('name')}</div>
<button onClick={() => next(['name'])}>Next</button>
</div>
)}
{step === 1 && (
<div>
<input type="number" value={form.age ?? ''} onChange={handleChangeAge} />
<div>{error('age')}</div>
<button onClick={() => goTo(0)}>Back</button>
<button onClick={() => next(['age'])}>Next</button>
</div>
)}
{step === 2 && (
<div>
<input value={form.email ?? ''} onChange={handleChangeEmail} />
<div>{error('email')}</div>
<button onClick={() => goTo(1)}>Back</button>
<button onClick={handleSubmit}>Submit</button>
</div>
)}
</div>
);
}For local development we suggest using Yalc to test your local version of formbit in your projects.
Ƭ FormbitObject<T>: Object
The object returned by useFormbit() and useFormbitContext(). Holds the form
state and every method needed to read, mutate and validate the form.
| Name | Type |
|---|---|
T |
extends FormbitValues |
| Name | Type | Description |
|---|---|---|
check |
Check<Partial<T>> |
Validates json against the current schema; returns the errors, or undefined if valid. |
error |
(path: string) => string | undefined |
- |
errors |
Errors |
Error messages registered since the last validation, keyed by the value's dot-path. Example ts form: { age: 1 } errors: { age: "Age must be greater than 18" } |
form |
Partial<T> |
The current form values. Partial: fields may be missing until validated. |
initialize |
Initialize<T> |
Re-initializes the form with new initial values. |
isDirty |
boolean |
True once the user has interacted with the form. |
isFormInvalid |
() => boolean |
- |
isFormValid |
() => boolean |
- |
liveValidation |
(path: string) => true | undefined |
- |
remove |
Remove<T> |
Removes the value at path, sets isDirty, then validates pathsToValidate plus every live-validated field. |
removeAll |
RemoveAll<T> |
Removes every given path, sets isDirty, then validates pathsToValidate plus every live-validated field. |
resetForm |
() => void |
- |
setError |
SetError |
Sets the error message at path. |
setSchema |
SetSchema<T> |
Replaces the current validation schema. |
submitForm |
SubmitForm<T> |
Validates the whole form and, if valid, runs the success callback to submit. |
validate |
Validate<T> |
Validates only path (ignores live-validated fields). |
validateAll |
ValidateAll<T> |
Validates only the given paths (ignores live-validated fields). |
validateForm |
ValidateForm<Partial<T>> |
Validates the whole form and registers any error. |
write |
Write<T> |
Writes value at path, sets isDirty, then validates pathsToValidate plus every live-validated field. |
writeAll |
WriteAll<T> |
Writes every [path, value] pair, sets isDirty, then validates pathsToValidate plus every live-validated field. |
Ƭ Errors: Record<string, string>
Error messages registered since the last validation, stored under the same dot-path as the corresponding form value.
Example
form: { age: 1 }
errors: { age: "Age must be greater than 18" }Ƭ FormState<T>: Object
The whole internal state of the form (everything except the validation schema).
| Name | Type |
|---|---|
T |
extends FormbitValues |
| Name | Type |
|---|---|
errors |
Errors |
form |
T |
initialValues |
T |
isDirty |
boolean |
liveValidation |
LiveValidation |
Ƭ FormbitValues: Record<string, unknown> & { __metadata?: Record<string, unknown> }
Base shape of every form handled by formbit: an open record of values, plus an
optional __metadata field formbit uses to carry data that must survive a
reset/initialize but must NOT be submitted.
The generic T you pass to useFormbit<T>() must extend this type.
Ƭ LiveValidation: Record<string, true>
Fields currently under live-validation (re-validated on every form change). A field is added here automatically when it fails a validation. Empty by default.
Example
form: { age: 1 }
liveValidation: { age: true }Ƭ CheckErrorCallback<T>: (json: FormbitValues, inner: ValidationError[], writer: FormState<T>, setError: SetError) => void
Invoked by check() when the given json is invalid.
| Name | Type |
|---|---|
T |
extends FormbitValues |
▸ (json, inner, writer, setError): void
| Name | Type |
|---|---|
json |
FormbitValues |
inner |
ValidationError[] |
writer |
FormState<T> |
setError |
SetError |
void
Ƭ CheckSuccessCallback<T>: (json: FormbitValues, writer: FormState<T>, setError: SetError) => void
Invoked by check() when the given json is valid.
| Name | Type |
|---|---|
T |
extends FormbitValues |
▸ (json, writer, setError): void
| Name | Type |
|---|---|
json |
FormbitValues |
writer |
FormState<T> |
setError |
SetError |
void
Ƭ ErrorCallback<T>: (writer: FormState<T>, setError: SetError) => void
Invoked by validation methods when validation fails.
| Name | Type |
|---|---|
T |
extends FormbitValues |
▸ (writer, setError): void
| Name | Type |
|---|---|
writer |
FormState<T> |
setError |
SetError |
void
Ƭ SubmitSuccessCallback<T>: (writer: FormState<Omit<T, "__metadata">>, setError: SetError, clearIsDirty: () => void) => void
Invoked by submitForm() once the whole form is valid — the place to send data
to the backend. __metadata is stripped from writer.form before this runs.
| Name | Type |
|---|---|
T |
extends FormbitValues |
▸ (writer, setError, clearIsDirty): void
| Name | Type |
|---|---|
writer |
FormState<Omit<T, "__metadata">> |
setError |
SetError |
clearIsDirty |
() => void |
void
Ƭ SuccessCallback<T>: (writer: FormState<T>, setError: SetError) => void
Invoked by validation methods when the form (or the validated paths) are valid.
| Name | Type |
|---|---|
T |
extends FormbitValues |
▸ (writer, setError): void
| Name | Type |
|---|---|
writer |
FormState<T> |
setError |
SetError |
void
Ƭ Check<T>: (json: FormbitValues, options?: CheckFnOptions<T>) => ValidationError[] | undefined
See FormbitObject.check.
| Name | Type |
|---|---|
T |
extends FormbitValues |
▸ (json, options?): ValidationError[] | undefined
| Name | Type |
|---|---|
json |
FormbitValues |
options? |
CheckFnOptions<T> |
ValidationError[] | undefined
Ƭ Initialize<T>: (values: Partial<T>) => void
| Name | Type |
|---|---|
T |
extends FormbitValues |
▸ (values): void
| Name | Type |
|---|---|
values |
Partial<T> |
void
Ƭ Remove<T>: (path: string, options?: WriteFnOptions<T>) => void
See FormbitObject.remove.
| Name | Type |
|---|---|
T |
extends FormbitValues |
▸ (path, options?): void
| Name | Type |
|---|---|
path |
string |
options? |
WriteFnOptions<T> |
void
Ƭ RemoveAll<T>: (arr: string[], options?: WriteFnOptions<T>) => void
| Name | Type |
|---|---|
T |
extends FormbitValues |
▸ (arr, options?): void
| Name | Type |
|---|---|
arr |
string[] |
options? |
WriteFnOptions<T> |
void
Ƭ SetError: (path: string, value: string) => void
▸ (path, value): void
| Name | Type |
|---|---|
path |
string |
value |
string |
void
Ƭ SetSchema<T>: (newSchema: ValidationSchema<T>) => void
| Name | Type |
|---|---|
T |
extends FormbitValues |
▸ (newSchema): void
| Name | Type |
|---|---|
newSchema |
ValidationSchema<T> |
void
Ƭ SubmitForm<T>: (successCallback: SubmitSuccessCallback<T>, errorCallback?: ErrorCallback<Partial<T>>, options?: ValidateOptions) => void
| Name | Type |
|---|---|
T |
extends FormbitValues |
▸ (successCallback, errorCallback?, options?): void
| Name | Type |
|---|---|
successCallback |
SubmitSuccessCallback<T> |
errorCallback? |
ErrorCallback<Partial<T>> |
options? |
ValidateOptions |
void
Ƭ Validate<T>: (path: string, options?: ValidateFnOptions<T>) => void
| Name | Type |
|---|---|
T |
extends FormbitValues |
▸ (path, options?): void
| Name | Type |
|---|---|
path |
string |
options? |
ValidateFnOptions<T> |
void
Ƭ ValidateAll<T>: (paths: string[], options?: ValidateFnOptions<T>) => void
See FormbitObject.validateAll.
| Name | Type |
|---|---|
T |
extends FormbitValues |
▸ (paths, options?): void
| Name | Type |
|---|---|
paths |
string[] |
options? |
ValidateFnOptions<T> |
void
Ƭ ValidateForm<T>: (successCallback?: SuccessCallback<T>, errorCallback?: ErrorCallback<T>, options?: ValidateOptions) => void
See FormbitObject.validateForm.
| Name | Type |
|---|---|
T |
extends FormbitValues |
▸ (successCallback?, errorCallback?, options?): void
| Name | Type |
|---|---|
successCallback? |
SuccessCallback<T> |
errorCallback? |
ErrorCallback<T> |
options? |
ValidateOptions |
void
Ƭ Write<T>: (path: keyof T | string, value: unknown, options?: WriteFnOptions<T>) => void
See FormbitObject.write.
| Name | Type |
|---|---|
T |
extends FormbitValues |
▸ (path, value, options?): void
| Name | Type |
|---|---|
path |
keyof T | string |
value |
unknown |
options? |
WriteFnOptions<T> |
void
Ƭ WriteAll<T>: (arr: WriteAllValue<T>[], options?: WriteFnOptions<T>) => void
| Name | Type |
|---|---|
T |
extends FormbitValues |
▸ (arr, options?): void
| Name | Type |
|---|---|
arr |
WriteAllValue<T>[] |
options? |
WriteFnOptions<T> |
void
Ƭ CheckFnOptions<T>: Object
Options accepted by check().
| Name | Type |
|---|---|
T |
extends FormbitValues |
| Name | Type |
|---|---|
errorCallback? |
CheckErrorCallback<T> |
options? |
ValidateOptions |
successCallback? |
CheckSuccessCallback<T> |
Ƭ ValidateFnOptions<T>: Object
Options accepted by the validate methods.
| Name | Type |
|---|---|
T |
extends FormbitValues |
| Name | Type |
|---|---|
errorCallback? |
ErrorCallback<Partial<T>> |
options? |
ValidateOptions |
successCallback? |
SuccessCallback<Partial<T>> |
Ƭ WriteAllValue<T>: [keyof T | string, unknown]
A single [path, value] pair accepted by writeAll.
| Name | Type |
|---|---|
T |
extends FormbitValues |
Ƭ WriteFnOptions<T>: { noLiveValidation?: boolean ; pathsToValidate?: string[] } & ValidateFnOptions<T>
Options accepted by the write/remove methods (validate options plus path control).
| Name | Type |
|---|---|
T |
extends FormbitValues |
Ƭ ValidateOptions: YupValidateOptions
Options forwarded to yup's validation methods. See https://github.com/jquense/yup.
Ƭ ValidationError: YupValidationError
The error object yup throws when a validation fails. See https://github.com/jquense/yup.
Ƭ ValidationSchema<T>: ObjectSchema<T>
A validation schema built with yup.object(). See https://github.com/jquense/yup.
| Name | Type |
|---|---|
T |
extends FormbitValues |
MIT © [Radicalbit (https://github.com/radicalbit)]