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*asyupfrom'yup';importuseFormbitfrom'@radicalbit/formbit';// 1. Define a Yup schema and infer the TypeScript type from itconstschema=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'),});typeFormData=yup.InferType<typeofschema>;constinitialValues: Partial<FormData>={name: undefined,age: undefined};// 2. Call the hook with generics so every callback is fully typedfunctionExample(){const{ form, submitForm, write, error, isDirty }=useFormbit<FormData>({
initialValues,yup: schema,});consthandleChangeName=({target: { value }}: React.ChangeEvent<HTMLInputElement>)=>{write('name',value);};consthandleChangeAge=({target: { value }}: React.ChangeEvent<HTMLInputElement>)=>{write('age',Number(value));};consthandleSubmit=()=>{submitForm(({ form })=>console.log('Validated form:',form),({ errors })=>console.error('Validation errors:',errors),);};// 3. Bind inputs, errors, and submit — Formbit stays out of your UIreturn(<div><labelhtmlFor="name">Name</label><inputid="name"type="text"value={form.name??''}onChange={handleChangeName}/><div>{error('name')}</div><labelhtmlFor="age">Age</label><inputid="age"type="number"value={form.age??''}onChange={handleChangeAge}/><div>{error('age')}</div><buttondisabled={!isDirty}onClick={handleSubmit}type="button">
Submit
</button></div>);}exportdefaultExample;Use FormbitContextProvider when you need to share form state across deeply nested components without prop drilling.
import{FormbitContextProvider,useFormbitContext}from'@radicalbit/formbit';import*asyupfrom'yup';constschema=yup.object({name: yup.string().required('Name is required'),surname: yup.string().required('Surname is required'),age: yup.number().required('Age is required'),});typeFormData=yup.InferType<typeofschema>;constinitialValues: Partial<FormData>={name: undefined,surname: undefined,age: undefined};// Wrap your form tree with the providerfunctionApp(){return(<FormbitContextProvider<FormData>initialValues={initialValues}yup={schema}><NameField/><SubmitButton/></FormbitContextProvider>);}// Any child can access form state without propsfunctionNameField(){const{ form, write, error }=useFormbitContext<FormData>();consthandleChangeName=({target: { value }}: React.ChangeEvent<HTMLInputElement>)=>{write('name',value);};return(<div><inputvalue={form.name??''}onChange={handleChangeName}/><span>{error('name')}</span></div>);}functionSubmitButton(){const{ submitForm, isDirty }=useFormbitContext<FormData>();return(<buttondisabled={!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';importuseFormbitfrom'@radicalbit/formbit';import*asyupfrom'yup';constschema=yup.object({name: yup.string().required(),email: yup.string().email().required(),});typeFormData=yup.InferType<typeofschema>;constinitialValues: Partial<FormData>={name: undefined,email: undefined};functionEditUserForm({ 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 valuesuseEffect(()=>{fetch(`/api/users/${userId}`).then((res)=>res.json()).then((user)=>{initialize(user);setLoading(false);});},[userId]);consthandleChangeName=({target: { value }}: React.ChangeEvent<HTMLInputElement>)=>{write('name',value);};consthandleChangeEmail=({target: { value }}: React.ChangeEvent<HTMLInputElement>)=>{write('email',value);};consthandleSubmit=(e: React.FormEvent)=>{e.preventDefault();submitForm(({ form })=>console.log(form));};if(loading)return<p>Loading...</p>;return(<formonSubmit={handleSubmit}><inputvalue={form.name??''}onChange={handleChangeName}/><div>{error('name')}</div><inputvalue={form.email??''}onChange={handleChangeEmail}/><div>{error('email')}</div><buttontype="submit">Save</button></form>);}Use __metadata to store step state and validateAll to gate navigation between steps.
importuseFormbitfrom'@radicalbit/formbit';import*asyupfrom'yup';constschema=yup.object({name: yup.string().required('Name is required'),age: yup.number().required('Age is required'),email: yup.string().email().required('Email is required'),});typeFormData=yup.InferType<typeofschema>;constinitialValues: Partial<FormData>&{__metadata: {step: number}}={name: undefined,age: undefined,email: undefined,__metadata: {step: 0},};functionMultiStepForm(){const{ form, write, error, validateAll, submitForm }=useFormbit<FormData>({
initialValues,yup: schema,});conststep=(form.__metadata?.stepasnumber)??0;constgoTo=(n: number)=>write('__metadata.step',n);// Validate only the current step's fields before advancingconstnext=(paths: string[])=>{validateAll(paths,{successCallback: ()=>goTo(step+1),});};consthandleChangeName=({target: { value }}: React.ChangeEvent<HTMLInputElement>)=>{write('name',value);};consthandleChangeAge=({target: { value }}: React.ChangeEvent<HTMLInputElement>)=>{write('age',Number(value));};consthandleChangeEmail=({target: { value }}: React.ChangeEvent<HTMLInputElement>)=>{write('email',value);};consthandleSubmit=()=>{submitForm(({ form })=>console.log('Submit:',form));};return(<div>{step===0&&(<div><inputvalue={form.name??''}onChange={handleChangeName}/><div>{error('name')}</div><buttononClick={()=>next(['name'])}>Next</button></div>)}{step===1&&(<div><inputtype="number"value={form.age??''}onChange={handleChangeAge}/><div>{error('age')}</div><buttononClick={()=>goTo(0)}>Back</button><buttononClick={()=>next(['age'])}>Next</button></div>)}{step===2&&(<div><inputvalue={form.email??''}onChange={handleChangeEmail}/><div>{error('email')}</div><buttononClick={()=>goTo(1)}>Back</button><buttononClick={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. Examplets 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)]