Tulip Logo IconTulip
Blocks

Rendering Blocks

Render stored blocks with typed component maps and slug dispatch.

RenderBlocks renders stored block rows by matching block.slug to a component map.

<RenderBlocks data={blocks} components={previewComponents} />

It does not fetch data, mutate data, create layout wrappers, or know what a block means. The app provides all of that.

Component Maps

Use InferBlockComponents with satisfies so TypeScript checks that every slug in the registry has a component.

const previewComponents = {
  hero: HeroPreview,
  summary: SummaryPreview,
  totals: TotalsPreview,
} satisfies InferBlockComponents<typeof pageBlocks>;

Each component receives a typed block prop.

import type { BlockComponentProps } from "@tulip-systems/blocks";

export function HeroPreview({ block }: BlockComponentProps<typeof heroBlock>) {
  return <section>{block.data.titlePart1}</section>;
}

Multiple Render Modes

Use separate maps for separate presentation modes.

const inlineComponents = {
  hero: HeroInlineBlock,
  summary: SummaryInlineBlock,
} satisfies InferBlockComponents<typeof pageBlocks>;

const previewComponents = {
  hero: HeroPreview,
  summary: SummaryPreview,
} satisfies InferBlockComponents<typeof pageBlocks>;

Then render each surface with the same data but different components.

<RenderBlocks data={blocks} components={inlineComponents} />
<RenderBlocks data={blocks} components={previewComponents} />

Fallbacks

If you want to handle unknown or intentionally omitted components, pass a fallback.

<RenderBlocks
  data={blocks}
  components={previewComponents}
  fallback={({ block }) => <div>Unknown block: {block.slug}</div>}
/>

In most application code, prefer a complete component map with satisfies so missing block renderers fail during development.

Keys

RenderBlocks defaults keys to block.id. If your storage shape needs another key, pass getKey.

<RenderBlocks data={blocks} components={components} getKey={(block) => block.id} />

On this page