README
form-atoms
Atomic form primitives for Jotai
npm i form-atoms jotai
Features
- Renders what changes and nothing else
- Strongly typed allowing you to quickly iterate on durable code
- Tiny (<3kB gzipped) but powerful API
- Nested/array fields without parsing field names
- Dynamic fields - you aren't stuck with your initial config
- Controlled inputs because no, uncontrolled inputs are not preferrable
- Ready for concurrent React - validation updates have a lower priority
- Familiar API that is very similar to other form libraries
- Async field-level validation
- Async submission
Quick start
Check out the example on CodeSandbox ↗
import { fieldAtom, useFieldAtom, formAtom, useFormAtom } from "form-atoms";
const nameFormAtom = formAtom({
name: {
first: fieldAtom({ value: "" }),
last: fieldAtom({ value: "" }),
},
});
function Form() {
const { fieldAtoms, submit } = useFormAtom(nameFormAtom);
return (
<form
onSubmit={submit((values) => {
console.log(values);
})}
>
<Field label="First name" atom={fieldAtoms.name.first} />
<Field label="Last name" atom={fieldAtoms.name.last} />
</form>
);
}
function Field({ label, atom }) {
const field = useFieldAtom(atom);
return (
<label>
<span>{label}</span>
<input {...field.props} />
</label>
);
}
Concepts
Jotai was born to solve extra re-render issue in React. Extra re-render is a render process that produces the same UI result, with which users won't see any differences.
Like Jotai, this library was built to solve the extra re-render issue with
React Forms. It takes a bottom-up approach using Jotai's atomic model.
In practice that means that formAtom()
derives its state from
fieldAtom()
. For example, validation occurs at the field-level
rather than the form-level. Normally that would pose a problem for fields with
validation that is dependent on other state or other fields, but using fieldAtom
's
validate
function allows you to read the value of other atoms.
The form-atoms
minimal API is written to be ergonomic and powerful. It feels
like other form libraries (even better in my opinion). You don't lose anything
by using it, but you gain a ton of performance and without footguns.
Table of contents
Field atoms | Description |
---|---|
fieldAtom() |
An atom that represents a field in a form. It manages state for the field, including the name, value, errors, dirty, validation, and touched state. |
useFieldAtom() |
A hook that returns props , state , and actions of a field atom from useFieldAtomProps , useFieldAtomState , and useFieldAtomActions . |
useFieldAtomProps() |
A hook that returns a set of props that can be destructured directly into an <input> , <select> , or <textarea> element. |
useFieldAtomState() |
A hook that returns the state of a field atom. This includes the field's value, whether it has been touched, whether it is dirty, the validation status, and any errors. |
useFieldAtomActions() |
A hook that returns a set of actions that can be used to interact with the field atom state. |
useFieldAtomInitialValue() |
A hook that sets the initial value of a field atom. Initial values can only be set once per scope. Therefore, if the initial value used is changed during rerenders, it won't update the atom value. |
useFieldAtomValue() |
A hook that returns the value of a field atom. |
useFieldAtomErrors() |
A hook that returns the errors of a field atom. |
Form atoms | Description |
---|---|
formAtom() |
An atom that derives its state fields atoms and allows you to submit, validate, and reset your form. |
useFormAtom() |
A hook that returns an object that contains the fieldAtoms and actions to validate, submit, and reset the form. |
useFormAtomState() |
A hook that returns the primary state of the form atom including values, errors, submit and validation status, as well as the fieldAtoms . Note that this hook will cuase its parent component to re-render any time those states change, so it can be useful to use more targeted state hooks like useFormAtomStatus . |
useFormAtomActions() |
A hook that returns a set of actions that can be used to update the state of the form atom. This includes updating fields, submitting, resetting, and validating the form. |
useFieldAtomValues() |
A hook that returns the values of the form atom. |
useFieldAtomErrors() |
A hook that returns the errors of the form atom. |
useFieldAtomStatus() |
A hook that returns the submitStatus and validateStatus of the form atom. |
useFieldAtomSubmit() |
A hook that returns a callback for handling form submission. |
Components | Description |
---|---|
<Form> |
A React component that renders form atoms and their fields in an isolated scope using a Jotai Provider. |
<InputField> |
A React component that renders field atoms with initial values. This is useful for fields that are rendered as native HTML elements because the props can unpack directly into the underlying component. |
<Field> |
A React component that renders field atoms with initial values. This is useful for fields that aren't rendered as native HTML elements. |
Recipes
- How to validate on
(blur, change, touch, submit)
- How to validate a field conditional to the state of another field
- How to validate a field asynchronously
- How to create a nested fields
- How to create an array of fields
☀︎ Coming soon
- How to handle errors
- How to set initial values inside of a React component
- How to use a custom input
Field atoms
fieldAtom()
An atom that represents a field in a form. It manages state for the field, including the name, value, errors, dirty, validation, and touched state.
Arguments
Name | Type | Required? | Description |
---|---|---|---|
config | FieldAtomConfig<Value> |
Yes | The initial state and configuration of the field. |
FieldAtomConfig
interface FieldAtomConfig<Value> {
/**
* Optionally provide a name for the field that will be added
* to any attached `<input>`, `<select>`, or `<textarea>` elements
*/
name?: string;
/**
* The initial value of the field
*/
value: Value;
/**
* The initial touched state of the field
*/
touched?: boolean;
/**
* A function that validates the value of the field any time
* one of its atoms changes. It must either return an array of
* string error messages or undefined. If it returns undefined,
* the validation is "skipped" and the current errors in state
* are retained.
*/
validate?: (state: {
/**
* A Jotai getter that can read other atoms
*/
get: Getter;
/**
* The current value of the field
*/
value: Value;
/**
* The dirty state of the field
*/
dirty: boolean;
/**
* The touched state of the field
*/
touched: boolean;
/**
* The event that caused the validation. Either:
*
* - `"change"` - The value of the field has changed
* - `"touch"` - The field has been touched
* - `"blur"` - The field has been blurred
* - `"submit"` - The form has been submitted
* - `"user"` - A user/developer has triggered the validation
*/
event: FieldAtomValidateOn;
}) => void | string[] | Promise<void | string[]>;
}
Returns
type FieldAtom<Value> = Atom<{
/**
* An atom containing the field's name
*/
name: WritableAtom<string | undefined, string | undefined | typeof RESET>;
/**
* An atom containing the field's value
*/
value: WritableAtom<Value, Value | typeof RESET | ((prev: Value) => Value)>;
/**
* An atom containing the field's touched status
*/
touched: WritableAtom<
boolean,
boolean | typeof RESET | ((prev: boolean) => boolean)
>;
/**
* An atom containing the field's dirty status
*/
dirty: Atom<boolean>;
/**
* A write-only atom for validating the field's value
*/
validate: WritableAtom<null, void | FieldAtomValidateOn>;
/**
* An atom containing the field's validation status
*/
validateStatus: WritableAtom<FormAtomValidateStatus, FormAtomValidateStatus>;
/**
* An atom containing the field's validation errors
*/
errors: WritableAtom<string[], string[] | ((value: string[]) => string[])>;
/**
* A write-only atom for resetting the field atoms to their
* initial states.
*/
reset: WritableAtom<null, void>;
/**
* An atom containing a reference to the `HTMLElement` the field
* is bound to.
*/
ref: WritableAtom<
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | null,
| HTMLInputElement
| HTMLTextAreaElement
| HTMLSelectElement
| null
| ((
value: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | null
) => HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | null)
>;
_validateCount: WritableAtom<number, number | ((current: number) => number)>;
_validateCallback?: FieldAtomConfig<Value>["validate"];
}>;
⇗ Back to top
useFieldAtom()
A hook that returns props
, state
, and actions
of a field atom from
useFieldAtomProps
, useFieldAtomState
,
and useFieldAtomActions
.
Arguments
Name | Type | Required? | Description |
---|---|---|---|
fieldAtom | FieldAtom<Value> |
Yes | The atom that stores the field's state |
scope | Scope |
No | A Jotai scope |
Returns
interface UseFieldAtom<Value> {
/**
* `<input>`, `<select>`, or `<textarea>` props for the field
*/
props: FieldAtomProps<Value>;
/**
* Actions for managing the state of the field
*/
actions: FieldAtomActions<Value>;
/**
* The current state of the field
*/
state: FieldAtomState<Value>;
}
⇗ Back to top
useFieldAtomProps()
A hook that returns a set of props that can be destructured directly into an <input>
, <select>
, or <textarea>
element.
Arguments
Name | Type | Required? | Description |
---|---|---|---|
fieldAtom | FieldAtom<Value> |
Yes | The atom that stores the field's state |
scope | Scope |
No | A Jotai scope |
Returns
interface FieldAtomProps<Value> {
/**
* The name of the field if there is one
*/
name: string | undefined;
/**
* The value of the field
*/
value: Value;
/**
* A WAI-ARIA property that tells a screen reader whether the
* field is invalid
*/
"aria-invalid": boolean;
/**
* A React callback ref that is used to bind the field atom to
* an `<input>`, `<select>`, or `<textarea>` element so that it
* can be read and focused.
*/
ref: React.RefCallback<
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
>;
onBlur(event: React.FormEvent<HTMLInputElement>): void;
onBlur(event: React.FormEvent<HTMLTextAreaElement>): void;
onBlur(event: React.FormEvent<HTMLSelectElement>): void;
onChange(event: React.ChangeEvent<HTMLInputElement>): void;
onChange(event: React.ChangeEvent<HTMLTextAreaElement>): void;
onChange(event: React.ChangeEvent<HTMLSelectElement>): void;
}
⇗ Back to top
useFieldAtomState()
A hook that returns the state of a field atom. This includes the field's value, whether it has been touched, whether it is dirty, the validation status, and any errors.
Arguments
Name | Type | Required? | Description |
---|---|---|---|
fieldAtom | FieldAtom<Value> |
Yes | The atom that stores the field's state |
scope | Scope |
No | A Jotai scope |
Returns
interface FieldAtomState<Value> {
/**
* The value of the field
*/
value: ExtractAtomValue<ExtractAtomValue<FieldAtom<Value>>["value"]>;
/**
* The touched state of the field
*/
touched: ExtractAtomValue<ExtractAtomValue<FieldAtom<Value>>["touched"]>;
/**
* The dirty state of the field. A field is "dirty" if it's value has
* been changed.
*/
dirty: ExtractAtomValue<ExtractAtomValue<FieldAtom<Value>>["dirty"]>;
/**
* The validation status of the field
*/
validateStatus: ExtractAtomValue<
ExtractAtomValue<FieldAtom<Value>>["validateStatus"]
>;
/**
* The error state of the field
*/
errors: ExtractAtomValue<ExtractAtomValue<FieldAtom<Value>>["errors"]>;
}
⇗ Back to top
useFieldAtomActions()
A hook that returns a set of actions that can be used to interact with the field atom state.
Arguments
Name | Type | Required? | Description |
---|---|---|---|
fieldAtom | FieldAtom<Value> |
Yes | The atom that stores the field's state |
scope | Scope |
No | A Jotai scope |
Returns
interface FieldAtomActions<Value> {
/**
* A function that validates the field's value with a `"user"` validation
* event.
*/
validate(): void;
/**
* A function for changing the value of a field. This will trigger a `"change"`
* validation event.
*
* @param {Value} value - The new value of the field
*/
setValue(
value: ExtractAtomUpdate<ExtractAtomValue<FieldAtom<Value>>["value"]>
): void;
/**
* A function for changing the touched state of a field. This will trigger a
* `"touch"` validation event.
*
* @param {boolean} touched - The new touched state of the field
*/
setTouched(
touched: ExtractAtomUpdate<ExtractAtomValue<FieldAtom<Value>>["touched"]>
): void;
/**
* A function for changing the error state of a field
*
* @param {string[]} errors - The new error state of the field
*/
setErrors(
errors: ExtractAtomUpdate<ExtractAtomValue<FieldAtom<Value>>["errors"]>
): void;
/**
* Focuses the field atom's `<input>`, `<select>`, or `<textarea>` element
* if there is one bound to it.
*/
focus(): void;
/**
* Resets the field atom to its initial state.
*/
reset(): void;
}
⇗ Back to top
useFieldAtomInitialValue()
A hook that sets the initial value of a field atom. Initial values can only be set once per scope. Therefore, if the initial value used is changed during rerenders, it won't update the atom value.
Arguments
Name | Type | Required? | Description |
---|---|---|---|
fieldAtom | FieldAtom<Value> |
Yes | The atom that stores the field's state |
initialValue | Value |
No | The initial value to set the atom to. If this is undefined , no initial value will be set. |
scope | Scope |
No | A Jotai scope |
⇗ Back to top
useFieldAtomValue()
A hook that returns the value of a field atom.
Arguments
Name | Type | Required? | Description |
---|---|---|---|
fieldAtom | FieldAtom<Value> |
Yes | The atom that stores the field's state |
scope | Scope |
No | A Jotai scope |
Returns
typeof value;
⇗ Back to top
useFieldAtomErrors()
A hook that returns the errors of a field atom.
Arguments
Name | Type | Required? | Description |
---|---|---|---|
fieldAtom | FieldAtom<Value> |
Yes | The atom that stores the field's state |
scope | Scope |
No | A Jotai scope |
Returns
string[]
⇗ Back to top
Form atoms
formAtom()
An atom that derives its state fields atoms and allows you to submit, validate, and reset your form.
Arguments
Name | Type | Required? | Description |
---|---|---|---|
fields | FormAtomFields |
Yes | An object containing field atoms to be included in the form. Field atoms can be deeply nested in objects and arrays. |
FormAtomFields
type FormAtomFields = {
[key: string | number]:
| FieldAtom<any>
| FormAtomFields
| FormAtomFields[]
| FieldAtom<any>[];
};
Returns
type FormAtom<Fields extends FormAtomFields> = Atom<{
/**
* An atom containing an object of nested field atoms
*/
fields: WritableAtom<
Fields,
Fields | typeof RESET | ((prev: Fields) => Fields),
void
>;
/**
* An read-only atom that derives the form's values from
* its nested field atoms.
*/
values: Atom<FormAtomValues<Fields>>;
/**
* An read-only atom that derives the form's errors from
* its nested field atoms.
*/
errors: Atom<FormAtomErrors<Fields>>;
/**
* A read-only atom that returns `true` if any of the fields in
* the form are dirty.
*/
dirty: Atom<boolean>;
/**
* A read-only atom derives the touched state of its nested field atoms.
*/
touchedFields: Atom<FormAtomTouchedFields<Fields>>;
/**
* A write-only atom that resets the form's nested field atoms
*/
reset: WritableAtom<null, void>;
/**
* A write-only atom that validates the form's nested field atoms
*/
validate: WritableAtom<null, void | FieldAtomValidateOn>;
/**
* A read-only atom that derives the form's validation status
*/
validateStatus: Atom<FormAtomValidateStatus>;
/**
* A write-only atom for submitting the form
*/
submit: WritableAtom<
null,
(values: FormAtomValues<Fields>) => void | Promise<void>
>;
/**
* A read-only atom that reads the number of times the form has
* been submitted
*/
submitCount: Atom<number>;
/**
* An atom that contains the form's submission status
*/
submitStatus: WritableAtom<FormAtomSubmitStatus, FormAtomSubmitStatus>;
}>;
⇗ Back to top
useFormAtom()
A hook that returns an object that contains the fieldAtoms
and actions to validate, submit, and reset the form.
Arguments
Name | Type | Required? | Description |
---|---|---|---|
formAtom | FormAtom<Fields> |
Yes | The atom that stores the form's state |
scope | Scope |
No | A Jotai scope |
Returns
interface UseFormAtom<Fields extends FormAtomFields> {
/**
* An object containing the values of a form's nested field atoms
*/
fieldAtoms: Fields;
/**
* A function for handling form submissions.
*
* @param handleSubmit - A function that is called with the form's values
* when the form is submitted
*/
submit(
handleSubmit: (
values: Parameters<
ExtractAtomUpdate<ExtractAtomValue<FormAtom<Fields>>["submit"]>
>[0]
) => void | Promise<void>
): (e?: React.FormEvent<HTMLFormElement>) => void;
/**
* A function that validates the form's nested field atoms with a
* `"user"` validation event.
*/
validate(): void;
/**
* A function that resets the form's nested field atoms to their
* initial states.
*/
reset(): void;
}
⇗ Back to top
useFormAtomState()
A hook that returns the primary state of the form atom including values, errors, submit and validation status, as well as the fieldAtoms
. Note that this hook will cuase its parent component to re-render any time those states change, so it can be useful to use more targeted state hooks like useFormAtomStatus
.
Arguments
Name | Type | Required? | Description |
---|---|---|---|
formAtom | FormAtom<Fields> |
Yes | The atom that stores the form's state |
scope | Scope |
No | A Jotai scope |
Returns
interface FormAtomState<Fields extends FormAtomFields> {
/**
* An object containing the form's nested field atoms
*/
fieldAtoms: Fields;
/**
* An object containing the values of a form's nested field atoms
*/
values: FormAtomValues<Fields>;
/**
* An object containing the errors of a form's nested field atoms
*/
errors: FormAtomErrors<Fields>;
/**
* `true` if any of the fields in the form are dirty.
*/
dirty: boolean;
/**
* An object containing the touched state of the form's nested field atoms.
*/
touchedFields: FormAtomTouchedFields<Fields>;
/**
* The number of times a form has been submitted
*/
submitCount: number;
/**
* The validation status of the form
*/
validateStatus: FormAtomValidateStatus;
/**
* The submission status of the form
*/
submitStatus: FormAtomSubmitStatus;
}
⇗ Back to top
useFormAtomActions()
A hook that returns a set of actions that can be used to update the state of the form atom. This includes updating fields, submitting, resetting, and validating the form.
Arguments
Name | Type | Required? | Description |
---|---|---|---|
formAtom | FormAtom<Fields> |
Yes | The atom that stores the form's state |
scope | Scope |
No | A Jotai scope |
Returns
interface FormAtomActions<Fields extends FormAtomFields> {
/**
* A function for adding/removing fields from the form.
*
* @param fields - An object containing the form's nested field atoms or
* a callback that receives the current fields and returns the next
* fields.
*/
updateFields(
fields: ExtractAtomUpdate<ExtractAtomValue<FormAtom<Fields>>["fields"]>
): void;
/**
* A function for handling form submissions.
*
* @param handleSubmit - A function that is called with the form's values
* when the form is submitted
*/
submit(
handleSubmit: (
values: Parameters<
ExtractAtomUpdate<ExtractAtomValue<FormAtom<Fields>>["submit"]>
>[0]
) => void | Promise<void>
): (e?: React.FormEvent<HTMLFormElement>) => void;
/**
* A function that validates the form's nested field atoms with a
* `"user"` validation event.
*/
validate(): void;
/**
* A function that resets the form's nested field atoms to their
* initial states.
*/
reset(): void;
}
⇗ Back to top
useFormAtomValues()
A hook that returns the values of the form atom.
Arguments
Name | Type | Required? | Description |
---|---|---|---|
formAtom | FormAtom<Fields> |
Yes | The atom that stores the form's state |
scope | Scope |
No | A Jotai scope |
Returns
type FormAtomValues<Fields extends FormAtomFields> = {
[Key in keyof Fields]: Fields[Key] extends FieldAtom<infer Value>
? Value
: Fields[Key] extends FormAtomFields
? FormAtomValues<Fields[Key]>
: Fields[Key] extends any[]
? FormAtomValues<{
[Index in keyof Fields[Key]]: Fields[Key][Index];
}>
: never;
};
⇗ Back to top
useFormAtomErrors()
A hook that returns the errors of the form atom.
Arguments
Name | Type | Required? | Description |
---|---|---|---|
formAtom | FormAtom<Fields> |
Yes | The atom that stores the form's state |
scope | Scope |
No | A Jotai scope |
Returns
type FormAtomErrors<Fields extends FormAtomFields> = {
[Key in keyof Fields]: Fields[Key] extends FieldAtom<any>
? string[]
: Fields[Key] extends FormAtomFields
? FormAtomErrors<Fields[Key]>
: Fields[Key] extends any[]
? FormAtomErrors<{
[Index in keyof Fields[Key]]: Fields[Key][Index];
}>
: never;
};
⇗ Back to top
useFormAtomStatus()
A hook that returns the submitStatus
and validateStatus
of the form atom.
Arguments
Name | Type | Required? | Description |
---|---|---|---|
formAtom | FormAtom<Fields> |
Yes | The atom that stores the form's state |
scope | Scope |
No | A Jotai scope |
Returns
interface FormAtomStatus {
/**
* The validation status of the form
*/
validateStatus: FormAtomValidateStatus;
/**
* The submission status of the form
*/
submitStatus: FormAtomSubmitStatus;
}
⇗ Back to top
useFormAtomSubmit()
A hook that returns a callback for handling form submission.
Arguments
Name | Type | Required? | Description |
---|---|---|---|
formAtom | FormAtom<Fields> |
Yes | The atom that stores the form's state |
scope | Scope |
No | A Jotai scope |
Returns
(values: FormAtomValues) => (e?: React.FormEvent<HTMLFormElement>) => void | Promise<void>
⇗ Back to top
Components
<Form>
A React component that renders form atoms and their fields in an isolated scope using a Jotai Provider.
Props
Name | Type | Required? | Description |
---|---|---|---|
atom | FormAtom<FormAtomFields> |
Yes | A form atom |
scope | Scope |
No | A Jotai scope |
component | React.ComponentType<{state: FieldAtomState<Value>; actions: FieldAtomActions<Value>;}> |
No | A React component to render as the input field |
render | (state: FieldAtomState<Value>, actions: FieldAtomActions<Value>) => JSX.Element |
No | A render prop |
⇗ Back to top
<InputField>
A React component that renders field atoms with initial values. This is most useful for fields that are rendered as native HTML elements because the props can unpack directly into the underlying component.
Props
Name | Type | Required? | Description |
---|---|---|---|
atom | FieldAtom<Value> |
Yes | A field atom |
initialValue | Value |
No | The initial value of the field |
scope | Scope |
No | A Jotai scope |
component | React.ComponentType<{state: FieldAtomState<Value>; actions: FieldAtomActions<Value>;}> |
No | A React component to render as the input field |
render | (state: FieldAtomState<Value>, actions: FieldAtomActions<Value>) => JSX.Element |
No | A render prop |
⇗ Back to top
<Field>
A React component that renders field atoms with initial values. This is most useful for fields that aren't rendered as native HTML elements.
Props
Name | Type | Required? | Description |
---|---|---|---|
atom | FieldAtom<Value> |
Yes | A field atom |
initialValue | Value |
No | The initial value of the field |
scope | Scope |
No | A Jotai scope |
component | React.ComponentType<{state: FieldAtomState<Value>; actions: FieldAtomActions<Value>;}> |
No | A React component to render as the field |
render | (state: FieldAtomState<Value>, actions: FieldAtomActions<Value>) => JSX.Element |
No | A render prop |
⇗ Back to top
Utilities
walkFields()
A function that walks through an object containing nested field atoms and calls a visitor function for each atom it finds.
Arguments
Name | Type | Required? | Description |
---|---|---|---|
fields | FormAtomFields |
Yes | An object containing nested field atoms |
visitor | (field: FieldAtom<any>, path: string[]) => void \| false |
Yes | A function that will be called for each field atom. You can exit early by returning false from the function. |
Returns
void
⇗ Back to top
Wait, it's all atoms?
⇗ Back to top
LICENSE
MIT