Every form in react-declarative is driven by a single JavaScript array: TypedField[]. Instead of writing JSX for each input, you describe what you need as plain objects, and the <One /> component renders the full UI from that description. This means your form logic lives in data you can inspect, transform, and reuse — not scattered across component trees.
Each entry in the fields array is a plain object with a required type property drawn from the FieldType enum. All other properties are optional and depend on the type you choose.
import { TypedField, FieldType } from 'react-declarative';
const fields: TypedField[] = [
{
type: FieldType.Text, // required — determines which widget renders
name: 'firstName', // maps to the key in your data object
title: 'First name', // label shown above the input
description: 'Your legal first name', // helper text shown below
defaultValue: '', // value used when handler returns nothing for this key
},
];
The core properties shared by almost every field are:
| Property | Type | Purpose |
|---|---|---|
type |
FieldType |
Selects the widget or layout to render |
name |
string |
Maps the field to a key in your data object |
title |
string |
Label displayed with the field |
description |
string |
Helper text shown below the input |
defaultValue |
Value | ((payload) => Value) |
Initial value when the data object has no entry for this key |
columns |
string |
Column span (out of 12) at all breakpoints |
phoneColumns |
string |
Column span on phone-sized screens |
tabletColumns |
string |
Column span on tablet-sized screens |
desktopColumns |
string |
Column span on desktop-sized screens |
name maps to your dataThe name property is the bridge between a field and your data object. When <One /> reads the initial data from your handler, it looks up data[field.name] to populate each input. When the user changes a value, onChange receives a new data object with data[field.name] updated.
// This field schema...
const fields: TypedField[] = [
{ type: FieldType.Text, name: 'email' },
{ type: FieldType.Text, name: 'phone' },
];
// ...reads from and writes to objects shaped like this:
interface FormData {
email: string;
phone: string;
}
Layout types such as FieldType.Group, FieldType.Paper, and FieldType.Expansion do not need a name because they are containers — they wrap other fields but do not store values themselves.
Layout fields accept a fields array (or a single child) that contains the fields to render inside them. The most common layout is FieldType.Group, which arranges its children in a responsive grid.
const fields: TypedField[] = [
{
type: FieldType.Group,
phoneColumns: '12', // full width on phones
tabletColumns: '6', // half width on tablets
desktopColumns: '4', // one-third width on desktops
fields: [
{
type: FieldType.Text,
name: 'firstName',
title: 'First name',
defaultValue: 'Jane',
},
{
type: FieldType.Text,
name: 'lastName',
title: 'Last name',
defaultValue: 'Smith',
},
],
},
{
type: FieldType.Text,
name: 'bio',
title: 'Short bio',
inputRows: 3, // renders as a multiline textarea
},
];
react-declarative uses a 12-column grid system. The columns, phoneColumns, tabletColumns, and desktopColumns properties all accept a string that represents how many of the 12 columns the element should occupy.
Note: If you set only
columns, the other three breakpoints inherit its value. Set the per-breakpoint variants only when you need different behaviour at specific screen sizes.
// Full width on every screen
{ type: FieldType.Text, name: 'bio', columns: '12' }
// Responsive: stacks on mobile, side-by-side on larger screens
{ type: FieldType.Text, name: 'firstName', phoneColumns: '12', tabletColumns: '6', desktopColumns: '4' }
{ type: FieldType.Text, name: 'lastName', phoneColumns: '12', tabletColumns: '6', desktopColumns: '4' }
TypedField accepts two generic parameters that flow through every callback in the schema:
Data — the shape of your form's data object. Typed callbacks like isInvalid, isVisible, and compute receive a Data value, so TypeScript can tell you exactly which keys exist.Payload — an extra context object you pass into <One /> via the payload prop (for example, a user role or feature flags). Callbacks also receive Payload.interface ProfileData {
email: string;
role: 'admin' | 'user';
}
interface AppPayload {
currentUserId: string;
}
const fields: TypedField<ProfileData, AppPayload>[] = [
{
type: FieldType.Text,
name: 'email',
title: 'Email',
isInvalid(data, payload) {
// data is typed as ProfileData — full autocomplete
// payload is typed as AppPayload
if (!data.email.includes('@')) return 'Invalid email';
return null;
},
},
];
Tip: Start with
TypedField[](no generics) while exploring. Add the generics once you know the shape of your data — TypeScript will then catch mismatches in every field callback.