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, true>
BubbleTeaOrder
= import coco.
map<{
    name: z.z.ZodString;
}>(shape: {
    name: z.z.ZodString;
}): co.Map<{
    name: z.z.ZodString;
}, unknown, Account | Group, true>
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> = SchemaResolveQuery<T>> = R extends true | undefined ? LoadedAndRequired<InstanceOfSchemaCoValuesMaybeLoaded<T>> : [LoadedAndRequired<InstanceOfSchemaCoValuesMaybeLoaded<T>>] extends [...] ? Exclude<...> extends CoValue ? R extends {
    ...;
} ? readonly ((ItemDepth extends true | undefined ? Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((ItemDepth extends true | undefined ? Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((ItemDepth extends true | undefined ? Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((ItemDepth extends true | undefined ? Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((ItemDepth extends true | undefined ? Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((ItemDepth extends true | undefined ? Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((ItemDepth extends true | undefined ? Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((ItemDepth extends true | undefined ? Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((ItemDepth extends true | undefined ? Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly (Exclude<...> | OnErrorResolvedValue ...
export loaded
loaded
<typeof
const BubbleTeaOrder: co.Map<{
    name: z.z.ZodString;
}, unknown, Account | Group, true>
BubbleTeaOrder
>;
export const
const PartialBubbleTeaOrder: co.Map<{
    name: z.ZodOptional<z.z.ZodString>;
}, unknown, Account | Group, true>
PartialBubbleTeaOrder
=
const BubbleTeaOrder: co.Map<{
    name: z.z.ZodString;
}, unknown, Account | Group, true>
BubbleTeaOrder
.
CoMapSchema<{ name: ZodString; }, unknown, Account | Group, true>.partial<"name">(keys?: {
    name: true;
} | undefined): co.Map<{
    name: z.ZodOptional<z.z.ZodString>;
}, unknown, Account | Group, true>
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> = SchemaResolveQuery<T>> = R extends true | undefined ? LoadedAndRequired<InstanceOfSchemaCoValuesMaybeLoaded<T>> : [LoadedAndRequired<InstanceOfSchemaCoValuesMaybeLoaded<T>>] extends [...] ? Exclude<...> extends CoValue ? R extends {
    ...;
} ? readonly ((ItemDepth extends true | undefined ? Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((ItemDepth extends true | undefined ? Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((ItemDepth extends true | undefined ? Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((ItemDepth extends true | undefined ? Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((ItemDepth extends true | undefined ? Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((ItemDepth extends true | undefined ? Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((ItemDepth extends true | undefined ? Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((ItemDepth extends true | undefined ? Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly ((ItemDepth extends true | undefined ? Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends {
    ...;
} ? readonly (Exclude<...> | OnErrorResolvedValue ...
export loaded
loaded
<typeof
const PartialBubbleTeaOrder: co.Map<{
    name: z.ZodOptional<z.z.ZodString>;
}, unknown, Account | Group, true>
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: MaybeLoaded<{
    readonly name: string;
} & CoMap>
order
=
useCoState<co.Map<{
    name: z.z.ZodString;
}, unknown, Account | Group, true>, true, MaybeLoaded<{
    readonly name: string;
} & CoMap>>(Schema: co.Map<{
    name: z.z.ZodString;
}, unknown, Account | Group, true>, id: string | undefined, options?: {
    ...;
} | undefined): MaybeLoaded<...>
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. The {@param options.select} function allows returning only specific parts of the CoValue data, which helps reduce unnecessary re-renders by narrowing down the returned data. Additionally, you can provide a custom {@param options.equalityFn} to further optimize performance by controlling when the component should re-render based on the selected data.
@returnsThe loaded CoValue, or an {@link NotLoaded } value. Use `$isLoaded` to check whether the CoValue is loaded, or use {@link MaybeLoaded.$jazz.loadingState } to get the detailed loading state. If a selector function is provided, returns the result of the selector function.@example```tsx // Select only specific fields to reduce re-renders const Project = co.map({ name: z.string(), description: z.string(), tasks: co.list(Task), lastModified: z.date(), }); function ProjectTitle({ projectId }: { projectId: string }) { // Only re-render when the project name changes, not other fields const projectName = useCoState( Project, projectId, { select: (project) => !project.$isLoading ? project.name : "Loading...", } ); return <h1>{projectName}</h1>; } ```@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.$isLoaded) { switch (project.$jazz.loadingState) { case "unauthorized": return "Project not accessible"; case "unavailable": return "Project not found"; case "loading": return "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: 'catch' } }, }, }); if (!task.$isLoaded) { switch (task.$jazz.loadingState) { case "unauthorized": return "Task not accessible"; case "unavailable": return "Task not found"; case "loading": return "Loading task..."; } } return ( <div> <h2>{task.title}</h2> {task.assignee && <p>Assigned to: {task.assignee.name}</p>} <ul> {task.subtasks.map((subtask, index) => ( subtask.$isLoaded ? <li key={subtask.id}>{subtask.title}</li> : <li key={index}>Inaccessible subtask</li> ))} </ul> </div> ); } ```@example```tsx // Use custom equality function for complex data structures const TaskList = co.list(Task); function TaskCount({ listId }: { listId: string }) { const taskStats = useCoState( TaskList, listId, { resolve: { $each: true }, select: (tasks) => { if (!tasks.$isLoaded) return { total: 0, completed: 0 }; return { total: tasks.length, completed: tasks.filter(task => task.completed).length, }; }, // Custom equality to prevent re-renders when stats haven't changed equalityFn: (a, b) => a.total === b.total && a.completed === b.completed, } ); return ( <div> {taskStats.completed} of {taskStats.total} tasks completed </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, true>
BubbleTeaOrder
,
props: {
    id: string;
}
props
.id: stringid);
if (!
const order: MaybeLoaded<{
    readonly name: string;
} & CoMap>
order
.$isLoaded: boolean
Whether the CoValue is loaded. Can be used to distinguish between loaded and {@link NotLoaded } CoValues. For more information about the CoValue's loading state, use {@link $jazz.loadingState } .
$isLoaded
) 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<MaybeLoaded<{
    readonly name: string;
} & CoMap>> | undefined
orders
=
useAccount<co.Account<{
    root: co.Map<{
        orders: co.List<co.Map<{
            name: z.z.ZodString;
        }, unknown, Account | Group, true>, true>;
    }, unknown, Account | Group, true>;
    profile: co.Profile<...>;
}, true>, {
    ...;
}, CoList<...> | undefined>(AccountSchema?: co.Account<...> | undefined, options?: {
    ...;
} | undefined): CoList<...> | undefined
React hook for accessing the current user's account. This hook provides access to the current user's account profile and root data. It automatically handles the subscription lifecycle and supports deep loading of nested CoValues through resolve queries. The {@param options.select} function allows returning only specific parts of the account data, which helps reduce unnecessary re-renders by narrowing down the returned data. Additionally, you can provide a custom {@param options.equalityFn} to further optimize performance by controlling when the component should re-render based on the selected data.
@returnsThe account data, or an {@link NotLoaded } value. Use `$isLoaded` to check whether the CoValue is loaded, or use {@link MaybeLoaded.$jazz.loadingState } to get the detailed loading state. If a selector function is provided, returns the result of the selector function.@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 = useAccount( MyAppAccount, { resolve: { profile: true, root: true, }, select: (account) => account.$isLoaded ? account.profile.name : "Loading...", } ); return <h1>{profileName}</h1>; } ```@example```tsx // Deep loading with resolve queries function ProjectListWithDetails() { const me = useAccount(MyAppAccount, { resolve: { profile: true, root: { myProjects: { $each: { tasks: true, }, }, }, }, }); if (!me.$isLoaded) { switch (me.$jazz.loadingState) { case "unauthorized": return "Account not accessible"; case "unavailable": return "Account not found"; case "loading": return "Loading account..."; } } return ( <div> <h1>{me.profile.name}'s projects</h1> <ul> {me.root.myProjects.map((project) => ( <li key={project.id}> {project.name} ({project.tasks.length} tasks) </li> ))} </ul> </div> ); } ```
useAccount
(
const JazzAccount: co.Account<{
    root: co.Map<{
        orders: co.List<co.Map<{
            name: z.z.ZodString;
        }, unknown, Account | Group, true>, true>;
    }, unknown, Account | Group, true>;
    profile: co.Profile<...>;
}, true>
JazzAccount
, {
resolve?: RefsToResolve<{
    readonly root: MaybeLoaded<{
        readonly orders: MaybeLoaded<CoList<MaybeLoaded<{
            readonly name: string;
        } & CoMap>>>;
    } & CoMap>;
    readonly profile: MaybeLoaded<...>;
} & Account, 10, []> | undefined
Resolve query to specify which nested CoValues to load from the account
resolve
: {
root?: RefsToResolve<{
    readonly orders: MaybeLoaded<CoList<MaybeLoaded<{
        readonly name: string;
    } & CoMap>>>;
} & CoMap, 10, [0]> | undefined
root
: {
orders?: RefsToResolve<CoList<MaybeLoaded<{
    readonly name: string;
} & CoMap>>, 10, [0, 0]> | undefined
orders
: true } },
select?: ((account: MaybeLoaded<CoMapLikeLoaded<{
    readonly root: MaybeLoaded<{
        readonly orders: MaybeLoaded<CoList<MaybeLoaded<{
            readonly name: string;
        } & CoMap>>>;
    } & CoMap>;
    readonly profile: MaybeLoaded<...>;
} & Account, {
    ...;
}, 10, []>>) => CoList<...> | undefined) | undefined
Select which value to return from the account data
select
: (
account: MaybeLoaded<CoMapLikeLoaded<{
    readonly root: MaybeLoaded<{
        readonly orders: MaybeLoaded<CoList<MaybeLoaded<{
            readonly name: string;
        } & CoMap>>>;
    } & CoMap>;
    readonly profile: MaybeLoaded<...>;
} & Account, {
    ...;
}, 10, []>>
account
) =>
account: MaybeLoaded<CoMapLikeLoaded<{
    readonly root: MaybeLoaded<{
        readonly orders: MaybeLoaded<CoList<MaybeLoaded<{
            readonly name: string;
        } & CoMap>>>;
    } & CoMap>;
    readonly profile: MaybeLoaded<...>;
} & Account, {
    ...;
}, 10, []>>
account
.$isLoaded: boolean
Whether the CoValue is loaded. Can be used to distinguish between loaded and {@link NotLoaded } CoValues. For more information about the CoValue's loading state, use {@link $jazz.loadingState } .
$isLoaded
?
account: CoMapLikeLoaded<{
    readonly root: MaybeLoaded<{
        readonly orders: MaybeLoaded<CoList<MaybeLoaded<{
            readonly name: string;
        } & CoMap>>>;
    } & CoMap>;
    readonly profile: MaybeLoaded<...>;
} & Account, {
    ...;
}, 10, []>
account
.
Account.root: {
    readonly orders: CoList<MaybeLoaded<{
        readonly name: string;
    } & CoMap>>;
} & {
    readonly orders: MaybeLoaded<CoList<MaybeLoaded<{
        readonly name: string;
    } & CoMap>>>;
} & CoMap
root
.
orders: CoList<MaybeLoaded<{
    readonly name: string;
} & CoMap>>
orders
: var undefinedundefined,
}); const
const newOrder: MaybeLoaded<{
    readonly name: string | undefined;
} & CoMap>
newOrder
=
useCoState<co.Map<{
    name: z.ZodOptional<z.z.ZodString>;
}, unknown, Account | Group, true>, true, MaybeLoaded<{
    readonly name: string | undefined;
} & CoMap>>(Schema: co.Map<...>, id: string | undefined, options?: {
    ...;
} | undefined): MaybeLoaded<...>
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. The {@param options.select} function allows returning only specific parts of the CoValue data, which helps reduce unnecessary re-renders by narrowing down the returned data. Additionally, you can provide a custom {@param options.equalityFn} to further optimize performance by controlling when the component should re-render based on the selected data.
@returnsThe loaded CoValue, or an {@link NotLoaded } value. Use `$isLoaded` to check whether the CoValue is loaded, or use {@link MaybeLoaded.$jazz.loadingState } to get the detailed loading state. If a selector function is provided, returns the result of the selector function.@example```tsx // Select only specific fields to reduce re-renders const Project = co.map({ name: z.string(), description: z.string(), tasks: co.list(Task), lastModified: z.date(), }); function ProjectTitle({ projectId }: { projectId: string }) { // Only re-render when the project name changes, not other fields const projectName = useCoState( Project, projectId, { select: (project) => !project.$isLoading ? project.name : "Loading...", } ); return <h1>{projectName}</h1>; } ```@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.$isLoaded) { switch (project.$jazz.loadingState) { case "unauthorized": return "Project not accessible"; case "unavailable": return "Project not found"; case "loading": return "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: 'catch' } }, }, }); if (!task.$isLoaded) { switch (task.$jazz.loadingState) { case "unauthorized": return "Task not accessible"; case "unavailable": return "Task not found"; case "loading": return "Loading task..."; } } return ( <div> <h2>{task.title}</h2> {task.assignee && <p>Assigned to: {task.assignee.name}</p>} <ul> {task.subtasks.map((subtask, index) => ( subtask.$isLoaded ? <li key={subtask.id}>{subtask.title}</li> : <li key={index}>Inaccessible subtask</li> ))} </ul> </div> ); } ```@example```tsx // Use custom equality function for complex data structures const TaskList = co.list(Task); function TaskCount({ listId }: { listId: string }) { const taskStats = useCoState( TaskList, listId, { resolve: { $each: true }, select: (tasks) => { if (!tasks.$isLoaded) return { total: 0, completed: 0 }; return { total: tasks.length, completed: tasks.filter(task => task.completed).length, }; }, // Custom equality to prevent re-renders when stats haven't changed equalityFn: (a, b) => a.total === b.total && a.completed === b.completed, } ); return ( <div> {taskStats.completed} of {taskStats.total} tasks completed </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, true>
PartialBubbleTeaOrder
,
props: {
    id: string;
}
props
.id: stringid);
if (!
const newOrder: MaybeLoaded<{
    readonly name: string | undefined;
} & CoMap>
newOrder
.$isLoaded: boolean
Whether the CoValue is loaded. Can be used to distinguish between loaded and {@link NotLoaded } CoValues. For more information about the CoValue's loading state, use {@link $jazz.loadingState } .
$isLoaded
|| !
const orders: CoList<MaybeLoaded<{
    readonly name: string;
} & CoMap>> | 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<MaybeLoaded<{
    readonly name: string;
} & CoMap>>
orders
.
CoList<MaybeLoaded<{ readonly name: string; } & CoMap>>.$jazz: CoListJazzApi<CoList<MaybeLoaded<{
    readonly name: string;
} & CoMap>>>
$jazz
.
CoListJazzApi<CoList<MaybeLoaded<{ readonly name: string; } & CoMap>>>.push(...items: (({
    readonly name: string;
} & CoMap) | NotLoaded<{
    readonly name: string;
} & CoMap> | CoMapInit<{
    readonly name: string;
} & CoMap>)[]): 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.$isLoaded) 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.$isLoaded) 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.