How to write forms with Jazz

This guide shows you a simple and powerful way to implement forms for creating and updating CoValues.

See the full example here.

Updating a CoValue

To update a CoValue, we simply assign the new value directly as changes happen. These changes are synced to the server.

<input
  type="text"
  value={order.name}
  onChange={(e) => order.$jazz.set("name", e.target.value)}
/>

It's that simple!

Creating a CoValue

When creating a CoValue, we can use a partial version that allows us to build up the data before submitting.

Using a Partial CoValue

Let's say we have a CoValue called BubbleTeaOrder. We can create a partial version, PartialBubbleTeaOrder, which has some fields made optional so we can build up the data incrementally.

// schema.ts
export const 
const BubbleTeaOrder: co.Map<{
    name: z.z.ZodString;
}, unknown, Account | Group>
BubbleTeaOrder
= import coco.
map<{
    name: z.z.ZodString;
}>(shape: {
    name: z.z.ZodString;
}): co.Map<{
    name: z.z.ZodString;
}, unknown, Account | Group>
export map
map
({
name: z.z.ZodStringname: import zz.
function string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString (+1 overload)
export string
string
(),
}); export type
type BubbleTeaOrder = {
    readonly name: string;
} & CoMap
BubbleTeaOrder
= import coco.
type loaded<T extends CoValueClassOrSchema, R extends ResolveQuery<T> = true> = R extends boolean | undefined ? NonNullable<InstanceOfSchemaCoValuesNullable<T>> : [NonNullable<InstanceOfSchemaCoValuesNullable<T>>] extends [...] ? Exclude<...> extends CoValue ? R extends {
    ...;
} ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean ...
export loaded
loaded
<typeof
const BubbleTeaOrder: co.Map<{
    name: z.z.ZodString;
}, unknown, Account | Group>
BubbleTeaOrder
>;
export const
const PartialBubbleTeaOrder: co.Map<{
    name: z.ZodOptional<z.z.ZodString>;
}, unknown, Account | Group>
PartialBubbleTeaOrder
=
const BubbleTeaOrder: co.Map<{
    name: z.z.ZodString;
}, unknown, Account | Group>
BubbleTeaOrder
.
CoMapSchema<{ name: ZodString; }, unknown, Account | Group>.partial<"name">(keys?: {
    name: true;
} | undefined): co.Map<{
    name: z.ZodOptional<z.z.ZodString>;
}, unknown, Account | Group>
Creates a new CoMap schema by making all fields optional.
@returnsA new CoMap schema with all fields optional.
partial
();
export type
type PartialBubbleTeaOrder = {
    readonly name: string | undefined;
} & CoMap
PartialBubbleTeaOrder
= import coco.
type loaded<T extends CoValueClassOrSchema, R extends ResolveQuery<T> = true> = R extends boolean | undefined ? NonNullable<InstanceOfSchemaCoValuesNullable<T>> : [NonNullable<InstanceOfSchemaCoValuesNullable<T>>] extends [...] ? Exclude<...> extends CoValue ? R extends {
    ...;
} ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean ...
export loaded
loaded
<typeof
const PartialBubbleTeaOrder: co.Map<{
    name: z.ZodOptional<z.z.ZodString>;
}, unknown, Account | Group>
PartialBubbleTeaOrder
>;

Writing the components in React

Let's write the form component that will be used for both create and update.

// OrderForm.tsx
export function 
function OrderForm({ order, onSave }: {
    order: BubbleTeaOrder | PartialBubbleTeaOrder;
    onSave?: (e: React.FormEvent<HTMLFormElement>) => void;
}): React.JSX.Element
OrderForm
({
order: ({
    readonly name: string;
} & CoMap) | ({
    readonly name: string | undefined;
} & CoMap)
order
,
onSave: ((e: React.FormEvent<HTMLFormElement>) => void) | undefinedonSave }: {
order: ({
    readonly name: string;
} & CoMap) | ({
    readonly name: string | undefined;
} & CoMap)
order
:
type BubbleTeaOrder = {
    readonly name: string;
} & CoMap
BubbleTeaOrder
|
type PartialBubbleTeaOrder = {
    readonly name: string | undefined;
} & CoMap
PartialBubbleTeaOrder
;
onSave?: ((e: React.FormEvent<HTMLFormElement>) => void) | undefinedonSave?: (e: React.FormEvent<HTMLFormElement>e: React.interface React.FormEvent<T = Element>FormEvent<HTMLFormElement>) => void; }) { return ( <React.JSX.IntrinsicElements.form: React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>form React.DOMAttributes<HTMLFormElement>.onSubmit?: React.FormEventHandler<HTMLFormElement> | undefinedonSubmit={onSave: ((e: React.FormEvent<HTMLFormElement>) => void) | undefinedonSave || (e: React.FormEvent<HTMLFormElement>e => e: React.FormEvent<HTMLFormElement>e.React.BaseSyntheticEvent<Event, EventTarget & HTMLFormElement, EventTarget>.preventDefault(): voidpreventDefault())}> <React.JSX.IntrinsicElements.label: React.DetailedHTMLProps<React.LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>label> Name <React.JSX.IntrinsicElements.input: React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>input React.InputHTMLAttributes<HTMLInputElement>.type?: React.HTMLInputTypeAttribute | undefinedtype="text" React.InputHTMLAttributes<HTMLInputElement>.value?: string | number | readonly string[] | undefinedvalue={
order: ({
    readonly name: string;
} & CoMap) | ({
    readonly name: string | undefined;
} & CoMap)
order
.name: string | undefinedname || ""}
React.InputHTMLAttributes<HTMLInputElement>.onChange?: React.ChangeEventHandler<HTMLInputElement> | undefinedonChange={(e: React.ChangeEvent<HTMLInputElement>e) =>
order: ({
    readonly name: string;
} & CoMap) | ({
    readonly name: string | undefined;
} & CoMap)
order
.
CoMap.$jazz: CoMapJazzApi<{
    readonly name: string;
} & CoMap> | CoMapJazzApi<{
    readonly name: string | undefined;
} & CoMap>
Jazz methods for CoMaps are inside this property. This allows CoMaps to be used as plain objects while still having access to Jazz methods, and also doesn't limit which key names can be used inside CoMaps.
$jazz
.CoMapJazzApi<M extends CoMap>.set<"name">(key: "name", value: string): void
Set a value on the CoMap
@paramkey The key to set@paramvalue The value to set@categoryContent
set
("name", e: React.ChangeEvent<HTMLInputElement>e.React.ChangeEvent<HTMLInputElement>.target: EventTarget & HTMLInputElementtarget.HTMLInputElement.value: string
Returns the value of the data at the cursor's current position.
value
)}
React.InputHTMLAttributes<HTMLInputElement>.required?: boolean | undefinedrequired /> </React.JSX.IntrinsicElements.label: React.DetailedHTMLProps<React.LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>label> {onSave: ((e: React.FormEvent<HTMLFormElement>) => void) | undefinedonSave && <React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button React.ButtonHTMLAttributes<HTMLButtonElement>.type?: "button" | "reset" | "submit" | undefinedtype="submit">Submit</React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button>} </React.JSX.IntrinsicElements.form: React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>form> ); }

Writing the edit form

To make the edit form, simply pass the BubbleTeaOrder. Changes are automatically saved as you type.

// EditOrder.tsx
export function 
function EditOrder(props: {
    id: string;
}): React.JSX.Element | undefined
EditOrder
(
props: {
    id: string;
}
props
: { id: stringid: string }) {
const
const order: ({
    readonly name: string;
} & CoMap) | null | undefined
order
=
useCoState<co.Map<{
    name: z.z.ZodString;
}, unknown, Account | Group>, true>(Schema: co.Map<{
    name: z.z.ZodString;
}, unknown, Account | Group>, id: string | undefined, options?: {
    ...;
} | undefined): ({
    ...;
} & CoMap) | ... 1 more ... | undefined
React hook for subscribing to CoValues and handling loading states. This hook provides a convenient way to subscribe to CoValues and automatically handles the subscription lifecycle (subscribe on mount, unsubscribe on unmount). It also supports deep loading of nested CoValues through resolve queries.
@returnsThe loaded CoValue, or `undefined` if loading, or `null` if not found/not accessible@example```tsx // Deep loading with resolve queries const Project = co.map({ name: z.string(), tasks: co.list(Task), owner: TeamMember, }); function ProjectView({ projectId }: { projectId: string }) { const project = useCoState(Project, projectId, { resolve: { tasks: { $each: true }, owner: true, }, }); if (!project) { return project === null ? "Project not found or not accessible" : "Loading project..."; } return ( <div> <h1>{project.name}</h1> <p>Owner: {project.owner.name}</p> <ul> {project.tasks.map((task) => ( <li key={task.id}>{task.title}</li> ))} </ul> </div> ); } ```@example```tsx // Using with optional references and error handling const Task = co.map({ title: z.string(), assignee: co.optional(TeamMember), subtasks: co.list(Task), }); function TaskDetail({ taskId }: { taskId: string }) { const task = useCoState(Task, taskId, { resolve: { assignee: true, subtasks: { $each: { $onError: null } }, }, }); if (!task) { return task === null ? "Task not found or not accessible" : "Loading task..."; } return ( <div> <h2>{task.title}</h2> {task.assignee && <p>Assigned to: {task.assignee.name}</p>} <ul> {task.subtasks.map((subtask, index) => ( subtask ? <li key={subtask.id}>{subtask.title}</li> : <li key={index}>Inaccessible subtask</li> ))} </ul> </div> ); } ``` For more examples, see the [subscription and deep loading](https://jazz.tools/docs/react/using-covalues/subscription-and-loading) documentation.
useCoState
(
const BubbleTeaOrder: co.Map<{
    name: z.z.ZodString;
}, unknown, Account | Group>
BubbleTeaOrder
,
props: {
    id: string;
}
props
.id: stringid);
if (!
const order: ({
    readonly name: string;
} & CoMap) | null | undefined
order
) return;
return <
function OrderForm({ order, }: {
    order: PartialBubbleTeaOrder;
}): React.JSX.Element
OrderForm
order: {
    readonly name: string | undefined;
} & CoMap
order
={
const order: {
    readonly name: string;
} & CoMap
order
} />;
}

Writing the create form

For the create form, we need to:

  1. Create a partial order.
  2. Edit the partial order.
  3. Convert the partial order to a "real" order on submit.

Here's how that looks like:

// CreateOrder.tsx
export function 
function CreateOrder(props: {
    id: string;
}): React.JSX.Element | undefined
CreateOrder
(
props: {
    id: string;
}
props
: { id: stringid: string }) {
const
const orders: CoList<({
    readonly name: string;
} & CoMap) | null> | undefined
orders
=
useAccountWithSelector<co.Account<{
    root: co.Map<{
        orders: co.List<co.Map<{
            name: z.z.ZodString;
        }, unknown, Account | Group>>;
    }, unknown, Account | Group>;
    profile: co.Profile<BaseProfileShape>;
}>, CoList<...> | undefined, {
    ...;
}>(AccountSchema: co.Account<...> | undefined, options: {
    ...;
}): CoList<...> | undefined
React hook for accessing the current user's account with selective data extraction and custom equality checking. This hook extends `useAccount` by allowing you to select only specific parts of the account data through a selector function, which helps reduce unnecessary re-renders by narrowing down the returned data. Additionally, you can provide a custom equality function to further optimize performance by controlling when the component should re-render based on the selected data. The hook automatically handles the subscription lifecycle and supports deep loading of nested CoValues through resolve queries, just like `useAccount`.
@returnsThe result of the selector function applied to the loaded account data@example```tsx // Select only specific fields to reduce re-renders const MyAppAccount = co.account({ profile: co.profile(), root: co.map({ name: z.string(), email: z.string(), lastLogin: z.date(), }), }); function UserProfile({ accountId }: { accountId: string }) { // Only re-render when the profile name changes, not other fields const profileName = useAccountWithSelector( MyAppAccount, { resolve: { profile: true, root: true, }, select: (account) => account?.profile?.name ?? "Loading...", } ); return <h1>{profileName}</h1>; } ``` For more examples, see the [subscription and deep loading](https://jazz.tools/docs/react/using-covalues/subscription-and-loading) documentation.
useAccountWithSelector
(
const JazzAccount: co.Account<{
    root: co.Map<{
        orders: co.List<co.Map<{
            name: z.z.ZodString;
        }, unknown, Account | Group>>;
    }, unknown, Account | Group>;
    profile: co.Profile<BaseProfileShape>;
}>
JazzAccount
, {
resolve?: RefsToResolve<{
    readonly root: ({
        readonly orders: CoList<({
            readonly name: string;
        } & CoMap) | null> | null;
    } & CoMap) | null;
    readonly profile: ({
        readonly name: string;
        readonly inbox: string | undefined;
        readonly inboxInvite: string | undefined;
    } & CoMap) | null;
} & Account, 10, []> | undefined
Resolve query to specify which nested CoValues to load from the account
resolve
: {
root?: RefsToResolve<{
    readonly orders: CoList<({
        readonly name: string;
    } & CoMap) | null> | null;
} & CoMap, 10, [0]> | undefined
root
: {
orders?: RefsToResolve<CoList<({
    readonly name: string;
} & CoMap) | null>, 10, [0, 0]> | undefined
orders
: true } },
select: (account: CoMapLikeLoaded<{
    readonly root: ({
        readonly orders: CoList<({
            readonly name: string;
        } & CoMap) | null> | null;
    } & CoMap) | null;
    readonly profile: ({
        readonly name: string;
        readonly inbox: string | undefined;
        readonly inboxInvite: string | undefined;
    } & CoMap) | null;
} & Account, {
    ...;
}, 10, []> | null | undefined) => CoList<...> | undefined
Select which value to return from the account data
select
: (
account: CoMapLikeLoaded<{
    readonly root: ({
        readonly orders: CoList<({
            readonly name: string;
        } & CoMap) | null> | null;
    } & CoMap) | null;
    readonly profile: ({
        readonly name: string;
        readonly inbox: string | undefined;
        readonly inboxInvite: string | undefined;
    } & CoMap) | null;
} & Account, {
    ...;
}, 10, []> | null | undefined
account
) =>
account: CoMapLikeLoaded<{
    readonly root: ({
        readonly orders: CoList<({
            readonly name: string;
        } & CoMap) | null> | null;
    } & CoMap) | null;
    readonly profile: ({
        readonly name: string;
        readonly inbox: string | undefined;
        readonly inboxInvite: string | undefined;
    } & CoMap) | null;
} & Account, {
    ...;
}, 10, []> | null | undefined
account
?.
Account.root: {
    readonly orders: CoList<({
        readonly name: string;
    } & CoMap) | null>;
} & {
    readonly orders: CoList<({
        readonly name: string;
    } & CoMap) | null> | null;
} & CoMap
root
.
orders: CoList<({
    readonly name: string;
} & CoMap) | null> | undefined
orders
,
}); const
const newOrder: ({
    readonly name: string | undefined;
} & CoMap) | null | undefined
newOrder
=
useCoState<co.Map<{
    name: z.ZodOptional<z.z.ZodString>;
}, unknown, Account | Group>, true>(Schema: co.Map<{
    name: z.ZodOptional<z.z.ZodString>;
}, unknown, Account | Group>, id: string | undefined, options?: {
    ...;
} | undefined): ({
    ...;
} & CoMap) | ... 1 more ... | undefined
React hook for subscribing to CoValues and handling loading states. This hook provides a convenient way to subscribe to CoValues and automatically handles the subscription lifecycle (subscribe on mount, unsubscribe on unmount). It also supports deep loading of nested CoValues through resolve queries.
@returnsThe loaded CoValue, or `undefined` if loading, or `null` if not found/not accessible@example```tsx // Deep loading with resolve queries const Project = co.map({ name: z.string(), tasks: co.list(Task), owner: TeamMember, }); function ProjectView({ projectId }: { projectId: string }) { const project = useCoState(Project, projectId, { resolve: { tasks: { $each: true }, owner: true, }, }); if (!project) { return project === null ? "Project not found or not accessible" : "Loading project..."; } return ( <div> <h1>{project.name}</h1> <p>Owner: {project.owner.name}</p> <ul> {project.tasks.map((task) => ( <li key={task.id}>{task.title}</li> ))} </ul> </div> ); } ```@example```tsx // Using with optional references and error handling const Task = co.map({ title: z.string(), assignee: co.optional(TeamMember), subtasks: co.list(Task), }); function TaskDetail({ taskId }: { taskId: string }) { const task = useCoState(Task, taskId, { resolve: { assignee: true, subtasks: { $each: { $onError: null } }, }, }); if (!task) { return task === null ? "Task not found or not accessible" : "Loading task..."; } return ( <div> <h2>{task.title}</h2> {task.assignee && <p>Assigned to: {task.assignee.name}</p>} <ul> {task.subtasks.map((subtask, index) => ( subtask ? <li key={subtask.id}>{subtask.title}</li> : <li key={index}>Inaccessible subtask</li> ))} </ul> </div> ); } ``` For more examples, see the [subscription and deep loading](https://jazz.tools/docs/react/using-covalues/subscription-and-loading) documentation.
useCoState
(
const PartialBubbleTeaOrder: co.Map<{
    name: z.ZodOptional<z.z.ZodString>;
}, unknown, Account | Group>
PartialBubbleTeaOrder
,
props: {
    id: string;
}
props
.id: stringid);
if (!
const newOrder: ({
    readonly name: string | undefined;
} & CoMap) | null | undefined
newOrder
|| !
const orders: CoList<({
    readonly name: string;
} & CoMap) | null> | undefined
orders
) return;
const const handleSave: (e: React.FormEvent<HTMLFormElement>) => voidhandleSave = (e: React.FormEvent<HTMLFormElement>e: React.interface React.FormEvent<T = Element>FormEvent<HTMLFormElement>) => { e: React.FormEvent<HTMLFormElement>e.React.BaseSyntheticEvent<Event, EventTarget & HTMLFormElement, EventTarget>.preventDefault(): voidpreventDefault(); // Convert to real order and add to the list // Note: the name field is marked as required in the form, so we can assume that has been set in this case // In a more complex form, you would need to validate the partial value before storing it
const orders: CoList<({
    readonly name: string;
} & CoMap) | null>
orders
.
CoList<({ readonly name: string; } & CoMap) | null>.$jazz: CoListJazzApi<CoList<({
    readonly name: string;
} & CoMap) | null>>
$jazz
.
CoListJazzApi<CoList<({ readonly name: string; } & CoMap) | null>>.push(...items: (({
    readonly name: string;
} & CoMap) | CoMapInit<{
    readonly name: string;
} & CoMap> | null)[]): number
Appends new elements to the end of an array, and returns the new length of the array.
@paramitems New elements to add to the array.@categoryContent
push
(
const newOrder: {
    readonly name: string | undefined;
} & CoMap
newOrder
as
type BubbleTeaOrder = {
    readonly name: string;
} & CoMap
BubbleTeaOrder
);
}; return ( <
function OrderForm({ order, onSave, }: {
    order: BubbleTeaOrder | PartialBubbleTeaOrder;
    onSave?: (e: React.FormEvent<HTMLFormElement>) => void;
}): React.JSX.Element
OrderForm
order: ({
    readonly name: string;
} & CoMap) | ({
    readonly name: string | undefined;
} & CoMap)
order
={
const newOrder: {
    readonly name: string | undefined;
} & CoMap
newOrder
} onSave?: ((e: React.FormEvent<HTMLFormElement>) => void) | undefinedonSave={const handleSave: (e: React.FormEvent<HTMLFormElement>) => voidhandleSave} />
); }

Editing with a save button

If you need a save button for editing (rather than automatic saving), you can use Jazz's branching feature. The example app shows how to create a private branch for editing that can be merged back when the user saves:

import { useCoState } from "jazz-tools/react";
import { Group } from "jazz-tools";
import { useState, useMemo } from "react";

export function EditOrderWithSave(props: { id: string }) {
  // Create a new group for the branch, so that every time we open the edit page, 
  // we create a new private branch
  const owner = useMemo(() => Group.create(), []);

  const order = useCoState(BubbleTeaOrder, props.id, {
    resolve: {
      addOns: { $each: true },
      instructions: true,
    },
    unstable_branch: {
      name: "edit-order",
      owner,
    },
  });

  function handleSave(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    if (!order) return;

    // Merge the branch back to the original
    order.$jazz.unstable_merge();
    // Navigate away or show success message
  }

  function handleCancel() {
    // Navigate away without saving - the branch will be discarded
  }

  if (!order) return;

  return <OrderForm order={order} onSave={handleSave} onCancel={handleCancel} />;
}

This approach creates a private branch using unstable_branch with a unique owner group. The user can edit the branch without affecting the original data, and changes are only persisted when they click save via unstable_merge().

Important: Version control is currently unstable and we may ship breaking changes in patch releases.

Handling different types of data

Forms can be more complex than just a single string field, so we've put together an example app that shows you how to handle single-select, multi-select, date, boolean inputs, and rich text.

See the full example here.