Skip to Content
React WidgetAPI Reference

Ahead of the release

npm install @evanion/react-widget gives you 0.3.0. These pages document main, which has changes that release does not.

API reference

Every export of @evanion/react-widget.

The item model — AnyWidgetItem, WidgetRegistry, WidgetProblem, WidgetMeta, KnownWidgetTypes, validateItems, defineWidgets, ERROR_MESSAGES, VALIDATION_MESSAGES — comes from @evanion/widget and is re-exported here. The specifier does not change and neither does the install: this package pins the core exactly and depends on it. The widget core’s API reference is where each of those is written down, and the entries below link to it rather than restating it.

createWidgets(config)

function createWidgets< const C extends WidgetRegistry<AnyWidgetComponent>, M = WidgetMeta, >( config: WidgetsConfig<C, M>, ): { // memo()-wrapped Widgets: ComponentType<WidgetsProps<C, M>>; defineItems: (items: WidgetItem<C, M>[]) => WidgetItem<C, M>[]; validateItems: ( items: unknown, required?: Record<string, string[]>, ) => WidgetProblem[]; };

Call once, at module scope.

config

FieldMeaning
componentswidget type → component. Drives inference for the whole set
chrome.wrapperrendered around the whole set. Defaults to <section>
chrome.itemrendered around each widget. Defaults to a <div> with the data-widget-* attributes
chrome.suspense'per-item' (default) or 'none'
chrome.suspenseFallbackrendered while a widget suspends. Defaults to nothing

The two type parameters

C is the component map, inferred from config.components. M is the item meta vocabulary, inferred from config.chrome.item.

Neither is meant to be passed by hand. Naming M explicitly costs the inference of C, and chrome.item is the only thing that reads meta, so a set with no such chrome has nothing to check meta against — there, and for an unannotated chrome, meta stays any object.

<Widgets>

PropTypeMeaning
itemsWidgetItem<C, M>[]the items to render
componentsPartial<C>per-instance overrides, merged over the factory’s map
chromeWidgetsChrome<M>per-instance chrome overrides
ctxRecord<string, unknown>page-level data passed to every widget as a prop

Wrapped in memo(). An items prop that is not an array is skipped with a development warning and renders null.

Chrome is resolved per field, not per object: an instance chrome that sets only wrapper keeps the factory’s item, suspense and suspenseFallback.

ctx follows the props spread, so the renderer’s value wins over anything an item carries under that name — including when the renderer has none and the widget is handed undefined. Items are untrusted input, and WidgetDataProps omits ctx, so a typed item cannot express one at all.

validateItems(items, known, required?)

Re-exported unchanged from @evanion/widget. The core’s entry carries the signature and the arguments, and What it reports carries every message it returns and the payload that produces each one.

Two entry points reach it here. The standalone export takes a plain list of type names, so a webhook handler or a CI script validates a payload without importing React components it will never render. The bound form off createWidgets closes over the factory’s component map, so it takes items and required and no known.

props is not checked against the component. That is WidgetItem<C>’s job and it does it at compile time, which is the half of the check this package adds; validateItems covers structure, which is all it can see given a list of type names.

DefaultWrapper

function DefaultWrapper( props: HTMLProps<HTMLDivElement> & { items?: readonly RenderableWidgetItem[]; }, ): ReactElement;

A <section> carrying whatever props it is handed, less items, which it drops rather than spreading onto the element. <section> rather than <div> because a named section maps to the region landmark role, so a caller who passes aria-label gets a region reachable by landmark navigation and one who does not is no worse off.

DefaultItem

function DefaultItem( props: HTMLProps<HTMLDivElement> & { meta?: Record<string, unknown> }, ): ReactElement;

A <div> carrying data-widget-id and data-widget-type, which CMS click-to-edit overlays, analytics and E2E selectors key off.

meta is dropped rather than forwarded. It is arbitrary consumer data with no meaning to the DOM, and React warns about an unknown attribute on every key of it that reaches an element. Reading meta is what a custom chrome.item is for.

Neither default carries an error boundary or a Suspense boundary. The Suspense boundary lives in the renderer; error boundaries are not in this package at all.

ERROR_MESSAGES

Re-exported from @evanion/widget, and what this renderer passes to its warn-once logger when it skips an item. The core’s entry lists every key and its shape.

Each message names the offending item, which is what makes the message text a usable key: every bad item is reported once per process, and a second bad item is still reported separately.

Types

WidgetItem<C, M>

The checked item union, distributed over the keys of the component map. type narrows props to that component’s props minus children and ctx, meta is typed M, and children is gated on whether the component accepts it.

M does not distribute over keyof C: one chrome reads every item’s meta, so there is one vocabulary per set rather than one per widget type.

WidgetDataProps<C>

Omit<ComponentProps<C>, 'children' | 'ctx'>, except that a component with no remaining props resolves to Record<string, never> rather than {}.

WidgetChildren<C, K, M>

WidgetItem<C, M>[] when C[K] accepts children, and never when it does not. The never is what makes nesting under such a widget a compile error instead of a dropped subtree.

WidgetMeta

Record<string, unknown>, from @evanion/widget. The meta vocabulary of a set whose chrome declares none, and the default for M throughout this page.

WidgetsChrome<M>

interface WidgetsChrome<M = WidgetMeta> { wrapper?: WidgetsWrapperComponent; item?: WidgetItemComponent<M>; suspense?: 'per-item' | 'none'; suspenseFallback?: ReactNode; }

WidgetItemComponent<M>

type WidgetItemComponent<M = WidgetMeta> = ComponentType<{ children?: ReactNode; 'data-widget-id': string; 'data-widget-type': string; meta?: M; }>;

Annotating one of these is what types the whole set’s meta.

WidgetsWrapperComponent

type WidgetsWrapperComponent = ComponentType<{ children?: ReactNode; items?: readonly RenderableWidgetItem[]; }>;

The region wrapper receives the rendered children and the items that produced them, positionally aligned: an item the renderer skipped is dropped from both at once, so index i of one is index i of the other. That is what lets a wrapper group, size, key or window a region by metaChildren.toArray walks the <Suspense> elements the renderer created, not the items, so a wrapper cannot recover an item from its child without this prop.

items is optional, so a wrapper that ignores it declares nothing. One that spreads its props onto a DOM element has to drop it, the way DefaultWrapper does, or React warns about an unknown attribute.

AnyWidgetItem<Type, Props>

The loose item shape, from @evanion/widget, for data built before a component map exists — a CMS payload, a fixture, a network response. It checks nothing against a component, so prefer WidgetItem, which widens to it. Getting started is where the four fields are taught and the core’s entry carries the interface.

RenderableWidgetItem

The type-erased view the renderer uses internally. The checked union is widened to this exactly once, at the boundary between the public props and the render loop: spreading a discriminated union of this size directly onto a component makes TypeScript give up with “union type that is too complex to represent”.

AnyWidgetComponent

ComponentType<any>, for use in a generic constraint. ComponentType<unknown> would reject a component with concrete props, because component props are contravariant. Inference is unaffected — ComponentProps<C[K]> resolves against the concrete component that was passed.

Others

WidgetsConfig<C, M>, WidgetsProps<C, M> and WidgetSuspenseMode are this package’s own. WidgetRegistry, WidgetProblem, KnownWidgetTypes and defineWidgets come from @evanion/widget.

Migrating from 0.2.x

The item shape, the props and the data are unchanged. Three type names moved to @evanion/widget and are re-exported from here under the names the whole family uses.

WasNow
WidgetComponentMapWidgetRegistry<AnyWidgetComponent>
WidgetItemProblemWidgetProblem
WidgetPropsAnyWidgetItem

WidgetProps and WidgetsProps differed by one character and meant different things — the loose item, and the component’s props. AnyWidgetItem is the untyped counterpart to WidgetItem, which is what it always was.

chrome.wrapper now receives an items prop beside its children. It is optional, so an existing wrapper keeps compiling; a wrapper that spreads its props onto a DOM element has to drop it.

Migrating from 0.1.x

0.2.0 removed everything that could not exist under React’s react-server export condition.

RemovedReplacement
'use client'none needed; the package is importable from an RSC
WidgetsProvider, useWidgets, WidgetsConfig.contextcall createWidgets once at module scope
the injected Output prop and <Output/>read children
WidgetOutputPropsnone needed
the default WidgetErrorBoundaryyour own boundary in a custom chrome.item, in your own 'use client' file
DEFAULT_STYLES.LOADINGchrome.suspenseFallback
DEFAULT_STYLES.ERROR, ERROR_MESSAGES.WIDGET_ERROR, ERROR_MESSAGES.WIDGET_FAILED, ERROR_MESSAGES.LOADINGgone with the boundary that used them

renderWidget and NestedWidgetsContext are no longer exported: the nesting mechanism is internal, so changing it is not a breaking release.

Added since: meta and its typing through chrome.item, chrome.suspense, and validateItems.

Last updated on