The Jotai 👻 form-atoms list extension.
npm install @form-atoms/list-atom jotai-effect- 🏎️ Performant rendering. Each list item is rendered with a stable
keyderived from Jotai atom. - 🎮 Easy controls. The
<List.Item>providesadd,remove,moveUpandmoveDownactions for each item. - 🧩 Compound JSX Components. Use the
<List.Add />, or<List.Empty />instead of manual hooks. - 🔑 Scoped field
names. The fields are dynamically indexed enabling serialization toFormData. - 🐢 Lists all the way down. Render lists within lists within lists... with the
<List.Of>.
Quick start 🎨 Demo
import { formAtom, useForm, fieldAtom, InputField } from "form-atoms";
import { listAtom, createList } from "@form-atoms/list-atom";
const envVars = listAtom({
name: "envVars",
fields: () => ({
variable: fieldAtom({ name: "variable", value: "" }),
value: fieldAtom({ name: "value", value: "" }),
}),
});
const form = formAtom({ envVars });
const { List } = createList(envVars);
export const Form = () => {
const { submit } = useForm(form);
return (
<form onSubmit={submit(console.log)}>
<List
initialValue={[
{ variable: "PACKAGE_NAME", value: "form-atoms" },
{ variable: "APP_URL", value: "https://jotai.org" },
]}
>
<List.Item>
{({ fields, remove }) => (
<>
<InputField atom={fields.variable} component="input" />
<InputField atom={fields.value} component="input" />
<button type="button" onClick={remove}>
Remove
</button>
</>
)}
</List.Item>
<List.Add>
{({ add }) => (
<button type="button" onClick={() => add()}>
Add variable
</button>
)}
</List.Add>
</List>
</form>
);
};| Atoms | Description |
|---|---|
listAtom() |
An atom that represents a list of form fields in a form. It manages state for the list, including the name, value, errors, dirty, validation and empty state. |
| Hooks | Description |
|---|---|
useListActions() |
A hook that returns a add, remove & move actions, that can be used to interact with the list atom state. |
useList() |
A hook that returns the list items ready to be rendred together with the list actions. |
| Components | Description | Demo |
|---|---|---|
createList(listAtom) |
A function to create components bound to the listAtom. | |
<List> |
A component to initialize the listAtom via initialValue prop. |
🎨 |
<List.Add> |
Adds new or initialized items to the list. | 🎨 |
<List.Empty> |
Render children only when the list has no items. | 🎨 |
<List.Item> |
Iterate and render each of the list items. | 🎨 |
| <List.Of> | Render a nested list within a <List.Item>. | 🎨 |
An atom that represents a list of form fields in a form. It manages state for the list, including the name, value, errors, dirty, validation and empty state.
| Name | Type | Required? | Description |
|---|---|---|---|
| config | ListAtomConfig<Fields, Value> |
Yes | The initial state and configuration of the field. |
export type ListAtomConfig<Fields extends FormFields, Value> = {
/**
* Optionally provide a name for the field that will be added
* prefixed to inner fields
* E.g. list name "contacts" and field name "email"
* will have scoped name for the 4th item "contacts[3].email"
*/
name?: string;
/**
* The initial array of values of the list
*/
value: Value[];
/**
* A function to initialize the fields for each of the initial values.
*/
fields: (value: Value) => Fields;
/**
* Error message the listAtom will have, when its items have nested errors.
* It will be one of errors returned by the `useFieldErrors()` hook.
*/
invalidItemError?: string;
/**
* 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: ValidateOn;
}) => void | string[] | Promise<void | string[]>;
};An extended FieldAtom:
export type ListAtom<Fields extends FormFields, Value> = ExtendFieldAtom<
Value[],
{
/**
* An atom indicating whether the list is empty.
*/
empty: Atom<boolean>;
/**
* A splitAtom() instance from jotai/utils.
* It handles adding, removing and moving of items in the list.
* @internal
*/
_splitList: WritableAtom<
SplitListItem<Fields>[],
[SplitAtomAction<ListItemForm<Fields>>],
void
>;
/**
* An atom holding the list of forms of each item.
* @internal
*/
_formList: WritableAtom<
ListItemForm<Fields>[],
[typeof RESET | SetStateAction<ListItemForm<Fields>[]>],
void
>;
/**
* An atom holding the fields of the internal formAtom of each item.
* @internal
*/
_formFields: Atom<Fields[]>;
buildItem(): ListItemForm<Fields>;
}
>;A hook that returns a add, remove & move actions, that can be used to interact with the list atom state.
| Name | Type | Required? | Description |
|---|---|---|---|
| listAtom | ListAtom<Fields extends FormFields, Value> |
Yes | The atom that stores the list's state |
| options | UseAtomOptions |
No | Options that are forwarded to the useAtom, useAtomValue, and useSetAtom hooks |
export type UseListActions<Fields extends FormFields, Value> = {
/**
* Removes the item from the list.
*
* @param item - An item from the listAtom's splitList array.
*/
remove: (item: SplitListItem<Fields>) => void;
/**
* Appends a new item to the list by default, when no 'before' position is used.
* Optionally the item can be initialized, with the 'fields' argument.
*
* @param before - An item from the listAtom's splitList array.
* @param value - A custom list item value.
* @returns The created ListItemForm<Fields>
*/
add: (
before?: SplitListItem<Fields> | undefined,
value?: Value | undefined,
) => ListItemForm<Fields>;
/**
* Moves the item to the end of the list, or where specified when the 'before' is defined.
*
* @param item - A splitList item to be moved.
* @param before - A splitList item before which to place the moved item.
*/
move: (
item: SplitListItem<Fields>,
before?: SplitListItem<Fields> | undefined,
) => void;
};A hook that returns the list items ready to be rendred together with the list actions.
| Name | Type | Required? | Description |
|---|---|---|---|
| listAtom | ListAtom<Fields extends FormFields, Value> |
Yes | The atom that stores the list's state |
| options | UseAtomOptions |
No | Options that are forwarded to the useAtom, useAtomValue, and useSetAtom hooks |
export type UseList<Fields extends FormFields, Value> = UseListActions<
Fields,
Value
> & {
/**
* Resolved value from the list.empty atom.
*/
isEmpty: boolean;
items: {
/**
* The item from the internal splitList.
*/
item: SplitListItem<Fields>;
/**
* Stable React key prop derived from atom id.
*/
key: string;
/**
* The form fields of the current item.
*/
fields: Fields;
/**
* A function to remove the current item from the list.
*/
remove: () => void;
/**
* A helper function to move the item to the previous index in the list.
*/
moveUp: () => void;
/**
* A helper function to move the item to the next index in the list.
*/
moveDown: () => void;
};
};Create a compound List components:
import { createList } from "@form-atoms/list-atom";
const { List } = createList(myListAtom);
// Usage:
// List.Add
// List.Empty
// List.Item
// List.Ofexport type ListComponents<Fields extends FormFields> = {
/**
* A component to initialize the listAtom value.
*/
List: FunctionComponent<ListProps<FormFieldValues<Fields>>> & {
/**
* A component to control adding new or initialized items to the list.
*/
Add: FunctionComponent<AddButtonProps<Fields>>;
/**
* A component which renders children only when the list is empty.
*/
Empty: FunctionComponent<EmptyProps>;
/**
* A component to iterate and render each of the list items.
*/
Item: FunctionComponent<ItemProps<Fields>>;
/**
* A component to create these ListComponents for a nested listAtom within a <List.Item>
*/
Of: <Fields extends FormFields>(props: ListOfProps<Fields>) => ReactNode;
};
};| Name | Type | Required? | Description |
|---|---|---|---|
| children | ReactNode |
Yes | A react nodes |
| initialValue | Value[] |
No | A value to initialize the listAtom |
| store | AtomStore |
No | A Jotai store |
| Name | Type | Required? | Description |
|---|---|---|---|
| children | (props: AddChildrenProps) => JSX.Element |
No | A render prop |
type AddChildrenProps<Fields extends FormFields> = {
/**
* An action to append a new item to the end of the list.
* @param fields optionaly set the items initial value.
* @returns The created ListItemForm<Fields>
*/
add: (value?: FormFieldValues<Fields>) => ListItemForm<Fields>;
};| Name | Type | Required? | Description |
|---|---|---|---|
| children | ReactNode |
No | Content to render when the list is empty |
| Name | Type | Required? | Description |
|---|---|---|---|
| children | (props: ListItemProps) => JSX.Element |
Yes | A render prop |
type ListItemProps<Fields extends FormFields> = {
/**
* The fields of the current item, as created with the listAtom's `fields` config function.
*/
fields: Fields;
/**
* The item from the internal splitList.
*/
item: SplitListItem<Fields>;
/**
* The index of the current item.
*/
index: number;
/**
* Total count of items in the list.
*/
count: number;
/**
* Append a new item to the list.
* When called with the current item, it will prepend it.
* @returns The created ListItemForm<Fields>
*/
add: (
before?: SplitListItem<Fields>,
value?: FormFieldValues<Fields>,
) => ListItemForm<Fields>;
/**
* Removes the current item from the list.
*/
remove: () => void;
/**
* Moves the current item one slot up in the list.
* Supports carousel - the first item will become the last.
*/
moveUp: () => void;
/**
* Moves the current item one slot down in the list.
* Supports carousel - the last item will become the first.
*/
moveDown: () => void;
};| Name | Type | Required? | Description |
|---|---|---|---|
| atom | ListAtom<Fields> |
Yes | A listAtom for which to create nested compound components |
| children | (props: Components) => JSX.Element |
Yes | A render prop with a List compound component bound to the listAtom |
MIT

