Tulip Logo IconTulip
Blocks

Creating Blocks

Build create catalogs with create component maps and composable create primitives.

Create flows use two layers:

  • RenderCreateBlocks renders available block create components in registry order.
  • CreateBlock* primitives compose the dialog, trigger, fields, cancel, and submit controls for one block type.

Create Component Maps

Create components are keyed by slug, just like preview and inline renderers.

const createComponents = {
  hero: HeroCreateBlock,
  summary: SummaryCreateBlock,
  totals: TotalsCreateBlock,
} satisfies InferBlockCreateComponents<typeof pageBlocks>;

Render them with one shared mutation and variable mapper.

<RenderCreateBlocks
  registry={pageBlocks}
  components={createComponents}
  variables={(block) => ({ pageId, block })}
  mutation={createBlockMutation}
/>

RenderCreateBlocks uses registry.definitions for ordering because there are no stored rows yet.

Create Component Props

Use BlockCreateComponentProps to avoid repeating mutation prop shapes.

type HeroCreateInput = {
  slug: typeof heroBlock.slug;
  data: z.input<typeof heroBlock.schema>;
};

type HeroCreateBlockProps<
  TData = unknown,
  TError = DefaultError,
  TVariables = undefined,
  TOnMutateResult = unknown,
> = BlockCreateComponentProps<HeroCreateInput, TData, TError, TVariables, TOnMutateResult>;

A Block Create Form

Each create component owns its form schema, defaults, labels, and field layout.

export function HeroCreateBlock({ variables, mutation }: HeroCreateBlockProps) {
  const form = useForm<HeroCreateValues>({
    resolver: standardSchemaResolver(heroBlock.schema),
    defaultValues: {
      titlePart1: "Make space",
      titlePart2: "for growth",
    },
  });

  return (
    <CreateBlock>
      <CreateBlockTrigger>
        <CreateBlockTriggerContent>
          <CreateBlockTriggerTitle>{heroBlock.label}</CreateBlockTriggerTitle>
          <CreateBlockTriggerDescription>{heroBlock.description}</CreateBlockTriggerDescription>
        </CreateBlockTriggerContent>
      </CreateBlockTrigger>

      <Form {...form}>
        <CreateBlockContent
          variables={(data) => variables({ slug: heroBlock.slug, data })}
          mutation={mutation}
        >
          <CreateBlockHeader>
            <CreateBlockTitle>Add hero block</CreateBlockTitle>
          </CreateBlockHeader>

          <CreateBlockFields>{/* app-owned form fields */}</CreateBlockFields>

          <CreateBlockFooter>
            <CreateBlockCancel>Cancel</CreateBlockCancel>
            <CreateBlockSubmit>Create block</CreateBlockSubmit>
          </CreateBlockFooter>
        </CreateBlockContent>
      </Form>
    </CreateBlock>
  );
}

What the Package Handles

  • dialog open state
  • form submission through TanStack Query mutation options
  • resetting the nearest React Hook Form after success or cancel
  • disabling submit while pending
  • consistent trigger, field, footer, and action layout primitives

What the App Handles

  • field components
  • default values
  • localization
  • toasts
  • query invalidation metadata
  • the server mutation implementation

On this page