Reform is a powerful, type-safe, and extensible validation and form management library for TypeScript and React. A unique feature of this framework is its use of modern TypeScript class decorators to define validation schemas and constraints directly on your model classes, enabling highly expressive, maintainable, and type-safe form logic. It provides advanced features for building complex forms, handling validation, and managing form state with a focus on flexibility and developer experience.
- Type-safe validation schemas using decorators and constraints
- Composable constraints for fields, arrays, objects, and custom types
- Localized validation messages with pluggable message providers
- Advanced form state management (dirty, touched, errors, async validation)
- Observer pattern for reacting to changes in form fields
- Deep object path utilities for accessing and updating nested data
- Extensible metadata system for field and form configuration
Using npm:
$ npm install @4riders/reformUsing yarn:
$ yarn add @4riders/reformimport { string } from '@4riders/reform'
class Person {
@string({ required: true, min: 1 })
name: string | null = null
}The name property above can neither be null nor undefined because of the required: true constraint but could be an empty string without the min: 1 constraint. See the string decorator for more options.
To validate a value based on this model, you can use the validate function (we will later use the useForm hook to manage form state and validation in React, but this is how you can validate any value against a model):
import { Yop, instance } from '@4riders/reform'
const statuses = Yop.validate(
{}, // (1)
instance({ of: Person }) // (2)
)
console.log(statuses)- The value we want to validate, in this case an empty object
{}. This could be any value, evennullorundefined, and of course anew Person(). - A validation schema defined as an instance of the
Personclass, which tells the validator to use the constraints defined by the decorators used in thePersonclass.
Running the code above will print in the console an array of one validation status, because the name property is required but is undefined in the {} value:
[{
"level": "error",
"path": "name",
"value": undefined,
"kind": "string",
"code": "required",
"constraint": true,
"message": "Required field"
}]See ValidationStatus for more details on the validation status object.
You can provide custom validation messages directly in the constraints as a tuple where the first element is the constraint value and the second element is the custom message (which can be a string or a JSX.Element):
import { string } from '@4riders/reform'
class Person {
@string({ required: [true, "Please enter your name!"] })
name: string | null = null
}
const statuses = Yop.validate(new Person(), instance({ of: Person }))
console.log(statuses)Running this code will print the following validation status with the custom message:
[{
"level": "error",
"path": "name",
"value": null,
"kind": "string",
"code": "required",
"constraint": true,
"message": "Please enter your name!"
}]Constraints can also be defined as a function that returns a tuple of the constraint value and the message, which allows for dynamic messages based on the value or other factors:
import { string } from '@4riders/reform'
class Person {
minNameLength = 4
@string({ min: ctx => [ctx.parent.minNameLength, `Name must be at least ${ctx.parent.minNameLength} characters long, but got ${ctx.value.length}!`] })
name: string | null = "Bob"
}
const statuses = Yop.validate(new Person(), instance({ of: Person }))
console.log(statuses)Running this code will print the following validation status with the parameterized custom message:
[{
"level": "error",
"path": "name",
"value": "Bob",
"kind": "string",
"code": "min",
"constraint": 4,
"message": "Name must be at least 4 characters long, but got 3!"
}]After defining your model with decorators, you can use the useForm hook to manage form state and validation in React. The useForm hook has two overloads, the simplest one takes the model and a submit function, and returns a FormManager instance.
import { useForm, Form } from '@4riders/reform'
function UserForm() {
const form = useForm(Person, form => {
// This function is called when the form is submitted and valid
console.log('Form submitted with values:', form.values)
form.setSubmitting(false)
})
return (
<Form form={ form } autoComplete="off" noValidate disabled={ form.submitting }>
{/* Inputs here */}
<button type="submit">Submit</button>
</Form>
)
}The Form component is a wrapper around the standard HTML <form> element that handles the submit event and calls the provided submit function with the form manager instance. It also sets a React Context that allows child components to access the form manager and its state through the useFormContext hook. All children of the Form component are enclosed within an HTML <fieldset> element, which is disabled when the disabled property is set to true.
You can create your own form input components that are connected to the form state and validation by using the useFormField hook, which takes a field path and returns the field's constraints, validation status, and a render function. For example, here is a simple TextField component that uses the BaseTextField component and connects it to the form state:
import { ComponentType } from 'react'
import { BaseTextField, Form, string, StringConstraints, StringValue, useForm, useFormField } from '@4riders/reform'
function TextField(props: { label: string, path: string }) { // (1)
const { constraints, status, render } = useFormField<StringValue, number>(props.path!)
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
<div>{ props.label + (constraints?.required ? " *" : "") }</div>
<BaseTextField name={ props.path! } render={ render } />
{ status?.message && <div style={{ color: 'red' }}>{ status.message }</div> }
</div>
)
}
type TextFieldProps<Parent> = StringConstraints<StringValue, Parent> & { // (2)
input?: ComponentType<any>
label?: string
path?: string
}
function textField<Parent>(props?: TextFieldProps<Parent>) { // (3)
return string<StringValue, Parent>({ input: TextField, ...props })
}
class Person {
@textField({ label: "Name", required: true }) // (4)
name: string | null = null
}
function PersonForm() { // (5)
const form = useForm(Person, form => {
console.log('Form submitted with values:', form.values)
form.setSubmitting(false)
})
return (
<Form form={ form } autoComplete="off" noValidate disabled={ form.submitting }>
<TextField path="name" label="Name" />
<button type="submit">Submit</button>
</Form>
)
}- The
TextFieldcomponent uses the useFormField hook to get the constraints and validation status for the field based on the provided path, and renders a label, the input component, and any validation message. It uses theBaseTextFieldcomponent as the input, which is a simple wrapper around an HTML<input type="text">element that handles change and blur events and calls the provided render function to update the form state. - The
TextFieldPropstype defines the props for theTextFieldcomponent and thetextFielddecorator. It extends StringConstraints and adds apathproperty (the path to the field in the model), alabelproperty, and aninputcomponent to render. - The
textFieldfunction is a decorator that creates a string constraint with theTextFieldcomponent as the default input, allowing us to use it directly in the model definition. - The
nameproperty in thePersonclass is decorated with the@textFielddecorator, which defines it as a required string field with theTextFieldcomponent as its input and a label of "Name". Note that theBaseTextFieldcomponent converts automatically an empty string tonullso there is no need to add amin: 1constraint to disallow empty values. - The
PersonFormcomponent uses the useForm hook to create a form manager for thePersonmodel, and renders a Form component with aTextFieldfor thenameproperty and a submit button.
You can also create observers that react to changes in form fields using the observer decorator, which takes a field path and a callback function that is called whenever the field value changes. For example, you can create an observer that logs the current value and validation status of the name field whenever it changes:
import { observer, useForm } from '@4riders/reform'
class Person {
age: number | null = null
@observer("age", (context) => context.setValue(
context.observedValue != null ? (context.observedValue as number) >= 18 : null
))
adult: boolean | null = null
}
const form = useForm(MyFormModel, () => {})In this example, the adult field is automatically updated to true or false based on the value of the age field, and it is also marked as untouched to avoid triggering validation messages when it changes.
See the observer decorator for more details and options.
Note that observers are automatically set up when using the simpler overload of the useForm hook, so you don't need to do anything special to enable them. However, if you are using the more advanced overload of the useForm hook, you need to call the useObservers hook after initializing the form:
const form = useForm(MyFormModel, () => {})
useObservers(MyFormModel, form)See full API reference, including all decorators, utilities, and form management hooks.
Use the following commands to build, test, and publish the package:
$ yarn build # build the library
$ yarn test # run tests
$ npm publish # publish to npmMIT