How to write forms with Jazz
This guide shows you a simple and powerful way to implement forms for creating and updating CoValues.
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.
import { co, z } from "jazz-tools"; export const BubbleTeaOrder = co.map({ name: z.string(), }); export type BubbleTeaOrder = co.loaded<typeof BubbleTeaOrder>; export const PartialBubbleTeaOrder = BubbleTeaOrder.partial(); export type PartialBubbleTeaOrder = co.loaded<typeof PartialBubbleTeaOrder>;
Writing the components in React
Let's write the form component that will be used for both create and update.
import { co } from "jazz-tools"; import { BubbleTeaOrder, PartialBubbleTeaOrder } from "./schema"; export function OrderForm({ order, onSave, }: { order: BubbleTeaOrder | PartialBubbleTeaOrder; onSave?: (e: React.FormEvent<HTMLFormElement>) => void; }) { return ( <form onSubmit={onSave || ((e) => e.preventDefault())}> <label> Name <input type="text" value={order.name} onChange={(e) => order.$jazz.set("name", e.target.value)} required /> </label> {onSave && <button type="submit">Submit</button>} </form> ); }
Writing the edit form
To make the edit form, simply pass the BubbleTeaOrder. Changes are automatically saved as you type.
export function EditOrder(props: { id: string }) { const order = useCoState(BubbleTeaOrder, props.id); if (!order.$isLoaded) return; return <OrderForm order={order} />; }
Writing the create form
For the create form, we need to:
- Create a partial order.
- Edit the partial order.
- Convert the partial order to a "real" order on submit.
Here's how that looks like:
export function CreateOrder(props: { id: string }) { const orders = useAccount(JazzAccount, { resolve: { root: { orders: true } }, select: (account) => (account.$isLoaded ? account.root.orders : undefined), }); const newOrder = useCoState(PartialBubbleTeaOrder, props.id); if (!newOrder.$isLoaded || !orders) return; const handleSave = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); // 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 orders.$jazz.push(newOrder as BubbleTeaOrder); }; return <OrderForm order={newOrder} onSave={handleSave} />; }
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 { 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, $onError: "catch" }, 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} />; }
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.