React
Block rendering, variant, and tracking exports for React.
These exports turn published content into React UI and wire up event tracking. The rendering and variant helpers come from the @createcms/core/react barrel; the tracking helpers come from the @createcms/core/react/tracking subpath (they are not re-exported by the barrel).
Block rendering stays on @createcms/core/react. The headless editor primitive is documented separately: Editor, Editor canvas, and Editor cms adapter.
Rendering
createBlocksMap
Builds a typed map from block type to component. Each component's properties are typed from the collection definition.
function createBlocksMap(collection, components): BlocksMap;const pageBlocks = createBlocksMap(collections.pages, {
hero: ({ properties }) => <h1>{properties.headline}</h1>,
});The returned BlocksMap also carries the collection definition it was built from (typed), so a single object can drive both rendering and an editor (components + schema/placement/grouping) — no separate collection handoff. BlocksRenderer only reads the component map; the bundled collection is there for editor tooling.
BlocksRenderer
The low-level renderer. Renders a block tree with a BlocksMap built by createBlocksMap. Use it when you want to build the map once (to share with editor tooling) and render it in several places.
function BlocksRenderer(props: {
blocks: BlocksMap;
tree: BlockTreeNode;
diff?: BlocksDiffOptions;
edit?: 'preview';
}): ReactNode;The optional diff prop turns on diff-aware rendering for an annotated tree from getDiff({ view: 'tree' }): changed blocks get wrapped in a <div data-diff="added | deleted | modified | moved"> element (or your own wrap callback). See Review changes visually.
The optional edit prop selects how the edit prop of every block component is filled: absent → NO_EDIT (no anchors in the HTML); 'preview' → data-editor-block / data-editor-field anchors on every block of the tree (referenced trees never get anchors), so an HTML preview can map a click back to a block and a field. edit.active is false in both cases.
createContentRenderer
The convenience factory: pass a collection and a partial component map (components only for the block types you render) and get back a ready ({ tree }) => ReactNode component in one step — no separate createBlocksMap call. This is the recommended entry point. Both paths resolve inline references; this one differs by accepting a partial map.
function createContentRenderer(collection, components): (props: { tree: BlockTreeNode; diff?: BlocksDiffOptions; edit?: 'preview' }) => ReactNode;BlockComponentProps
The props every block component receives from the renderer map.
type BlockComponentProps = {
properties: InferBlockProperties; // always an object; pass M = 'raw' to type against the editor's raw store values
children: ReactNode;
blockId: string;
node: BlockTreeNode;
edit: EditProps;
};edit carries the editor anchors as plain data — no functions, so the object crosses the server → client component boundary. Outside an editor it is NO_EDIT; with edit="preview" on the renderer it carries real anchors. edit.field has one entry per property key from the block DEFINITION (all-optional blocks still have them), and each entry is OPTIONAL, so {...edit.field.x} is a no-op outside an editor.
type EditAttrs = {
'data-editor-block'?: string;
'data-editor-field'?: string;
};
type EditProps = {
active: boolean; // true only inside an interactive editor canvas
block: EditAttrs;
field: { [key: string]: EditAttrs | undefined };
};import { NO_EDIT, type BlockProps } from '@createcms/core/react/blocks';
export function Hero({ properties, edit }: BlockProps<typeof pages, 'hero'>) {
return (
<section {...edit.block}>
<h1 {...edit.field.headline}>{properties.headline}</h1>
</section>
);
}
// Rendering a block by hand (tests, stories): <Hero {...props} edit={NO_EDIT} />getBlockDiff
Typed accessor for the per-node annotation of a diff tree (getDiff({ view: 'tree' })). Returns null on unchanged nodes and regular (non-diff) trees. Server-safe when imported from @createcms/core/react/blocks — the @createcms/core/react barrel is a 'use client' boundary.
function getBlockDiff(node: BlockTreeNode): BlockDiffAnnotation | null;diffSegmentsToHtml
Renders the word-level textDiff segments of a richText property change as an HTML string with <ins>/<del> markers, for the same dangerouslySetInnerHTML path you already use for rich text. Style the markers via ins[data-diff-text] / del[data-diff-text]. Tags are never wrapped and deleted tags are dropped, so a formatting-only change (say, bolding a word) renders as the new HTML with no inline highlight. Server-safe when imported from @createcms/core/react/blocks — the @createcms/core/react barrel is a 'use client' boundary.
function diffSegmentsToHtml(segments: TextDiffSegment[]): string;pickVariant
Server-side variant picker. Returns the tree of the variant matching branchId (or the control), with A/B metadata stripped.
function pickVariant(
variants: readonly { branchId: string; tree: BlockTreeNode }[],
branchId: string | null,
controlBranchId?: string,
): BlockTreeNode | null;extractBlockEvents
Extracts per-block-type event declarations from a collection's blocks.
function extractBlockEvents(
blocks: Record<string, AnyBlockDefinition> | undefined,
): Record<string, Record<string, EventDeclaration>>;useStore
Subscribes a component to a nanostores atom.
function useStore<T>(store: ReadableAtom<T>): T;Tracking
Imported from @createcms/core/react/tracking (not the @createcms/core/react barrel). See Events & tracking for the model.
createTrackedBlocks
Builds a typed event firer for a collection. useTrackedBlock(blockType) returns a fire narrowed to that block's declared events, so an unknown event name or wrong-typed param is a compile error.
function createTrackedBlocks(collection): {
useTrackedBlock(blockType): { fire: BlockEventFire };
};TrackingRuntimeProvider
Supplies the dispatch function and ambient A/B context to every BlockTracker below it.
function TrackingRuntimeProvider(props: { runtime: TrackingRuntime; children: ReactNode }): ReactNode;BlockTracker
Scopes a block's tracking identity (type, id, trackingId, events) to its children. The renderer adds this around functional blocks automatically.
function BlockTracker(props: BlockTrackingCtx & { children: ReactNode }): ReactNode;useBlockTrackerRaw
The untyped escape hatch. Returns fire and fireInteraction to dispatch events from inside a tracked block (expectedBlockType is a dev-time label, not a type-narrowing selector). Prefer createTrackedBlocks for compile-time checking.
function useBlockTrackerRaw(expectedBlockType?: string): {
fire: (name: string, params?: Record<string, string | number | boolean>) => void;
fireInteraction: (
name: string,
interactionId: string,
params?: Record<string, string | number | boolean>,
) => void;
};TrackedForm
A <form> wrapper that fires an attempt event and a success event around a form action.
function TrackedForm(props: {
attempt: string;
success: string;
action: (formData: FormData) => void | Promise<void>;
children: ReactNode;
}): ReactNode;Realtime
Imported from the @createcms/core/react/realtime subpath (which pulls in the optional @upstash/realtime peer — kept out of the main @createcms/core/react barrel). Both hooks ride one shared connection; see Realtime.
RealtimeProvider
Wraps your app in one shared realtime connection that every CMS realtime hook (useNotifications, the A/B useLiveResults) rides. Derives the endpoint + credentials from baseURL.
function RealtimeProvider(props: {
children: ReactNode;
baseURL: string; // same value passed to createCMSClient
maxReconnectAttempts?: number;
}): ReactNode;useNotifications
Subscribes the current user to their live notification stream over the RealtimeProvider connection and seeds from the notifications.list poll. De-dupes by id; the provider replays anything missed across a reconnect. Realtime-only — requires a mounted RealtimeProvider and notifications enabled (it type-requires client.notifications). client is your typed createCMSClient instance (no cast needed). userId is optional: pass it straight from your auth session (session?.user?.id) — while it's undefined the hook stays poll-only and connects once it resolves (the CMS has no current-user endpoint, so your app supplies the id).
function useNotifications<TActorUser>(
client: CMSClient,
options: {
userId?: string;
limit?: number;
withUser?: true;
},
): {
notifications: NotificationListItem<TActorUser>[];
unreadCount: number;
isLive: boolean;
refresh: () => void;
};TActorUser is inferred from your typed client — a partial of your user table's row. With withUser: true, each notification's actorUser (the responsible user) is typed off your config, on both the seed poll and live pushes (the wire event carries actorUser, so the actor shows immediately — no second poll).
const { notifications } = useNotifications(cmsClient, {
userId: session?.user?.id,
withUser: true,
});
// notifications[0].actorUser?.name — typed, present live and on pollEach item carries type, actorId, resourceType, resourceId, collection, and a free-form meta for deep-linking. See createNotificationRouter to turn those into typed links.
createNotificationRouter
Imported from @createcms/core/react (pure — no realtime peer, so it works even without realtime). Maps each notification type to a resolver that builds a deep link (and optional display data) from the item's fields. Each resolver receives meta narrowed to that type; the required fallback keeps routing total for custom and any future type.
function createNotificationRouter<TCMS>(routes: {
[type]?: (n: TypedNotification<type, TCMS>) => NotificationRoute;
fallback: (n: NotificationListItem) => NotificationRoute;
}): { resolve: (n: NotificationListItem) => NotificationRoute };
type NotificationRoute = {
href?: string | null; // null = non-navigable
label?: string;
icon?: string;
group?: string;
};Pass your CMS type — createNotificationRouter<typeof cms>(…) — to pick up plugin-contributed notification types (and your actorUser shape) with full meta typing.
import { createNotificationRouter } from '@createcms/core/react';
import type { cms } from './cms';
export const notificationRouter = createNotificationRouter<typeof cms>({
mention: (n) => ({ href: `/threads/${n.meta.threadId}#${n.meta.messageId}`, icon: 'at-sign' }),
mergeRequestOpened: (n) => ({ href: `/merge-requests/${n.meta.mergeRequestId}` }),
published: (n) => ({ href: `/${n.collection}/${n.resourceId}`, icon: 'rocket' }),
// a type from a plugin's `notificationTypes` — `meta` typed off typeof cms:
abTestWinner: (n) => ({ href: `/experiments/${n.meta.testId}` }),
fallback: (n) => ({ href: n.resourceId ? `/${n.collection}/${n.resourceId}` : null }),
});
// at render: const { href, icon } = notificationRouter.resolve(notification);The built-in core types (mention, comment, mergeRequest*, approval*, published, …) come with typed meta out of the box; plugins add their own types via the notificationTypes seam, inferred through typeof cms. To type a custom (or other app-only) notification raised without a plugin, augment NotificationMetaMap:
declare module '@createcms/core/react' {
interface NotificationMetaMap {
custom: { kind: 'invoice'; invoiceId: string };
}
}