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
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:

  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:

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.

See the full example here.