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} />