Skip to Content
React WidgetAPI Reference

These pages document @evanion/react-widget 0.3.3, the version on npm.

API reference

@evanion/react-widget exports one factory, two default chrome components and their types. It also re-exports the validator, the message constants and the loose item types from @evanion/widget, so one install covers both. The entries from validateItems on are those re-exports.

createWidgets(config)

createWidgets takes a component map and optional chrome, and returns the Widgets component that renders items against them.

function createWidgets< const C extends WidgetRegistry<AnyWidgetComponent>, M = WidgetMeta, >( config: WidgetsConfig<C, M>, ): { Widgets: MemoExoticComponent< (props: WidgetsProps<C, M>) => JSX.Element | null >; defineItems: (items: WidgetItem<C, M>[]) => WidgetItem<C, M>[]; validateItems: ( items: unknown, required?: Record<string, string[]>, ) => WidgetProblem[]; };
Signature — a shape, not a call
ReturnedWhat it does
Widgetsrenders an item array against the map; its props are WidgetsProps
defineItemsreturns the array it is handed, typed WidgetItem<C, M>[], for an array held in a variable
validateItemsvalidateItems with the map’s keys as the known types

C is inferred from config.components and M from config.chrome.item. Pass neither by hand. WidgetRegistry, WidgetMeta and WidgetProblem come from @evanion/widget and have entries below. TypeScript infers no type argument once one is named, so naming M means writing C out as well. Call createWidgets once, at module scope, because each call returns a new Widgets and a call inside a component remounts the region on every render.

const = (: { : string; : number }) => ( <> {.}, {.} kr </> ); // 1. Create a typed widget set from the component map const { , } = ({ : { : }, }); // 2. Define items with compile-time prop checking const = ([ { : 'brass', : 'listing', : { : 'Brass: Birmingham', : 649 }, }, ]); // 3. Render the region const = (< ={} />); // -> '<section><div data-widget-id="brass" data-widget-type="listing"><h3>Brass: Birmingham, 649 kr</h3></div></section>'

WidgetsConfig

WidgetsConfig<C, M> is the argument to createWidgets.

interface WidgetsConfig< C extends WidgetRegistry<AnyWidgetComponent>, M = WidgetMeta, > { components: C; chrome?: WidgetsChrome<M>; }
Signature — a shape, not a call

components is the component map, and its shape drives inference for every item and prop the returned Widgets accepts. chrome is the factory’s WidgetsChrome.

WidgetsProps

WidgetsProps<C, M> is the props of the Widgets component createWidgets returns.

PropTypeMeaning
itemsWidgetItem<C, M>[]the items to render
componentsPartial<C>optional; per-instance components, merged over the factory’s map
chromeWidgetsChrome<M>optional; per-instance chrome, resolved field by field
ctxRecord<string, unknown>optional; page-level data, passed to every widget as its ctx prop

Widgets is wrapped in memo(). An items prop that is not an array renders null and warns in development. Widgets passes ctx after the item’s props, so its value reaches the widget over anything an item carries under that name, undefined included. The compiler does not check ctx against the ctx prop a widget declares. Overriding the map or the chrome for one region shows the per-instance props.

WidgetsChrome

WidgetsChrome<M> is the markup Widgets draws around its region and its widgets, and the Suspense behaviour between them.

interface WidgetsChrome<M = WidgetMeta> { wrapper?: WidgetsWrapperComponent; item?: WidgetItemComponent<M>; suspense?: WidgetSuspenseMode; suspenseFallback?: ReactNode; }
Signature — a shape, not a call
FieldDefaultWhat it is
wrapperDefaultWrapperrendered around the whole region
itemDefaultItemrendered around each widget; its annotation types meta
suspense'per-item'whether each widget gets its own <Suspense>
suspenseFallbacknothingrendered while a widget suspends

WidgetSuspenseMode

WidgetSuspenseMode is 'per-item' | 'none', the values chrome.suspense takes.

type WidgetSuspenseMode = 'per-item' | 'none';
Signature — a shape, not a call

'per-item' gives each widget its own boundary, so one slow widget does not hold up its siblings. 'none' gives the region no boundary, for widgets that never suspend. React’s streaming renderer then writes every item into the shell, the first HTML it sends, and leaves none to a script that moves it into place later. When to disable Suspense carries the measurement.

No mode puts one boundary around the whole region. React would outline that one boundary and send every item in it after the shell, at once. To get one boundary anyway, wrap <Widgets> in <Suspense> and set 'none'.

WidgetItemComponent

WidgetItemComponent<M> is the type of chrome.item, the component rendered around each widget.

type WidgetItemComponent<M = WidgetMeta> = ComponentType<{ children?: ReactNode; 'data-widget-id': string; 'data-widget-type': string; meta?: M; }>;
Signature — a shape, not a call

Annotating chrome.item with it types the meta of every item one createWidgets call accepts: createWidgets infers M from the annotation.

import type { } from '@evanion/react-widget'; type = { : number; ?: number }; const : <> = ({ , , ... }) => ( < {...} ={{ : `${?. ?? 'auto'} / span ${?. ?? 1}`, }} > {} </> ); const { , } = ({ : { : }, : { : }, }); const = ([ { : 'root', : 'listing', : { : 'Root' }, : { : 2, : 2 }, }, // @ts-expect-error GridCell reads `column`, and `col` is not a key of GridMeta { : 'hive', : 'listing', : { : 'Hive' }, : { : 1 } }, ]); const = (< ={} />); // -> '<section><div data-widget-id="root" data-widget-type="listing" style="grid-column:2 / span 2"><h3>Root</h3></div><div data-widget-id="hive" data-widget-type="listing" style="grid-column:auto / span 1"><h3>Hive</h3></div></section>'

WidgetsWrapperComponent

WidgetsWrapperComponent is the type of chrome.wrapper, the component rendered around the whole region.

type WidgetsWrapperComponent = ComponentType<{ children?: ReactNode; items?: readonly RenderableWidgetItem[]; }>;
Signature — a shape, not a call

items and children are aligned by position. An item Widgets skipped is dropped from both, so index i of one is index i of the other, and a wrapper can group, size or window by item. Each child is the chrome.item element for one item. Its own props are data-widget-id, data-widget-type and meta, and React keys it by the item’s id. The item’s props sit on the widget element nested inside it. items hands the wrapper each whole item as data, top-level items only.

If your wrapper spreads its props onto a DOM element, remove items first, the way DefaultWrapper does, or React warns about an unknown attribute.

DefaultWrapper

DefaultWrapper is the default chrome.wrapper: a <section> carrying every prop it is handed except items.

function DefaultWrapper( props: HTMLProps<HTMLDivElement> & { items?: readonly RenderableWidgetItem[]; }, ): JSX.Element;
Signature — a shape, not a call

A <section> with an accessible name maps to the region landmark role, so an aria-label makes the region reachable by landmark navigation.

const { , } = ({ : { : }, : { : () => < {...} ="New in" />, : () => < {...} ="slot" />, }, }); const = ([ { : 'azul', : 'listing', : { : 'Azul' } }, ]); const = (< ={} />); // -> '<section aria-label="New in"><div data-widget-id="azul" data-widget-type="listing" class="slot"><h3>Azul</h3></div></section>'

DefaultItem

DefaultItem is the default chrome.item: a <div> carrying data-widget-id, data-widget-type and every other prop it is handed except meta.

function DefaultItem( props: HTMLProps<HTMLDivElement> & { meta?: Record<string, unknown> }, ): JSX.Element;
Signature — a shape, not a call

CMS click-to-edit overlays, analytics and end-to-end selectors key off the two data-widget-* attributes. meta is dropped because React warns about an unknown attribute on every key of it that reaches the DOM. Neither default carries a Suspense boundary or an error boundary; the Suspense boundary lives in Widgets.

WidgetItem

WidgetItem<C, M> is the checked item, a union with one member per key of the component map.

type WidgetItem< C extends WidgetRegistry<AnyWidgetComponent>, M = WidgetMeta, > = { [K in keyof C & string]: { id: string; type: K; props: WidgetDataProps<C[K]>; meta?: M; children?: WidgetChildren<C, K, M>; }; }[keyof C & string];
Signature — a shape, not a call

type narrows props to that component’s props. M is one type per createWidgets call, because one chrome.item reads every item’s meta. WidgetItem widens to AnyWidgetItem. satisfies WidgetItem<typeof components>[] checks an array the way defineItems does.

const = (: { : string; : number }) => ( <> {.}, {.} kr </> ); const { , } = ({ : { : }, }); const = ([ { : 'root', : 'listing', : { : 'Root', : 499 } }, ]); // @ts-expect-error 'raffle' is not a key of the component map ([{ : 'raffle', : 'raffle', : {} }]); const = (< ={} />); // -> '<section><div data-widget-id="root" data-widget-type="listing"><h3>Root, 499 kr</h3></div></section>'

WidgetDataProps

WidgetDataProps<C> is the props an item carries for component C: its props less children and ctx, which Widgets supplies.

type WidgetDataProps<C extends AnyWidgetComponent> = [ keyof Omit<ComponentProps<C>, 'children' | 'ctx'>, ] extends [never] ? Record<string, never> : Omit<ComponentProps<C>, 'children' | 'ctx'>;
Signature — a shape, not a call

A component with no props left resolves to Record<string, never>, which admits props: {} and rejects any key. Were the type {} instead, the same props would admit every key, because TypeScript assigns any object to {} with no excess-property check.

WidgetChildren

WidgetChildren<C, K, M> is the type of an item’s children: WidgetItem<C, M>[] when component C[K] declares children, and never when it does not.

type WidgetChildren< C extends WidgetRegistry<AnyWidgetComponent>, K extends keyof C, M = WidgetMeta, > = 'children' extends keyof ComponentProps<C[K]> ? WidgetItem<C, M>[] : never;
Signature — a shape, not a call

The never makes nesting under a widget that renders no children a compile error. Without it, Widgets drops that subtree and logs nothing.

([ { : 'azul', : 'listing', : { : 'Azul' }, // @ts-expect-error ListingCard declares no children, so nothing nests under it : [{ : 'hive', : 'listing', : { : 'Hive' } }], }, ]);

RenderableWidgetItem

RenderableWidgetItem is the item with its types erased, which Widgets renders from and chrome.wrapper receives as items.

interface RenderableWidgetItem { id: string; type: string; props: Record<string, unknown>; meta?: Record<string, unknown>; children?: RenderableWidgetItem[]; }
Signature — a shape, not a call

Widgets widens the checked union to this once, between its public props and its render loop. Spreading the union onto a component directly makes TypeScript fail with TS2590, “union type that is too complex to represent”.

AnyWidgetComponent

AnyWidgetComponent is ComponentType<any>, the constraint a component map’s values meet.

type AnyWidgetComponent = ComponentType<any>;
Signature — a shape, not a call

ComponentType<unknown> rejects a component with concrete props, because component props are contravariant. Inference is unaffected: ComponentProps<C[K]> resolves against the component that was passed.

validateItems

validateItems walks a payload and returns every problem it finds, re-exported unchanged from @evanion/widget.

import { } from '@evanion/react-widget'; const : unknown = .( '[{"id":"root","type":"listing","props":{"title":"Root"}},{"id":"raffle","type":"raffle","props":{}}]', ); const = (, ['listing', 'booking']); // -> [{ index: 1, id: 'raffle', type: 'raffle', message: 'unknown widget type' }]

The standalone export takes a list of type names or a component map, so a webhook or a CI script checks a payload without importing a React component. The bound form that createWidgets returns takes no names. Both take an optional required map from a widget type to the props that must be present and non-blank, where blank is undefined, null or a whitespace-only string. Neither checks a prop’s type against a component; WidgetItem does that at compile time. @evanion/widget’s entry carries the signature, and What it reports every message.

defineWidgets

defineWidgets returns the component map it is handed, typed as the literal object, re-exported from @evanion/widget.

import { , } from '@evanion/react-widget'; import type { } from '@evanion/react-widget'; const = (: { : string }) => .; const = ({ : }); // @ts-expect-error 'raffle' is not a key of components const : <typeof >['type'] = 'raffle'; const : unknown = [{ : 'raffle', : 'raffle', : {} }]; const = (, ); // -> [{ index: 0, id: 'raffle', type: 'raffle', message: 'unknown widget type' }]

A map declared on its own serves as the type argument of WidgetItem and as the known types of the standalone validateItems, so the raffle type fails the compile and the runtime check alike. Annotating that map as WidgetRegistry widens its keys to string, and defineWidgets keeps them literal. A map declared with no annotation keeps them literal too, so the call is optional in this package. @evanion/widget’s entry carries the signature.

ERROR_MESSAGES

ERROR_MESSAGES is the text Widgets warns with in development, re-exported from @evanion/widget.

MALFORMED_ITEMS reports an items prop that is not an array, and Widgets renders nothing. MALFORMED_CHILDREN reports nested items that are not an array, and Widgets renders the widget without them. UNKNOWN is the string 'unknown', which a message uses in place of an id or type the item lacks. The rest report an item Widgets skipped. A message about one item names its id, and Widgets logs each distinct message once per process, so a second bad item still gets its own warning. @evanion/widget’s entry lists every key.

VALIDATION_MESSAGES

VALIDATION_MESSAGES is the text validateItems reports, re-exported from @evanion/widget, so you can group or translate problems without matching on prose. @evanion/widget’s entry lists every key.

AnyWidgetItem

AnyWidgetItem is the loose item, 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; WidgetItem widens to it. The widget core’s Overview teaches the item’s fields, and @evanion/widget’s entry carries the interface.

WidgetRegistry

WidgetRegistry<T> is Record<string, T>, from @evanion/widget. This package instantiates it as WidgetRegistry<AnyWidgetComponent>, its component map type. @evanion/widget’s entry carries the rest.

WidgetMeta

WidgetMeta is Record<string, unknown>, from @evanion/widget: the meta type when createWidgets gets a chrome.item that declares none, and the default for M on this page. @evanion/widget’s entry carries the rest.

WidgetProblem

WidgetProblem is one line of the list validateItems returns, from @evanion/widget: an index, the item’s id and type, and a message. @evanion/widget’s entry says what each field holds.

KnownWidgetTypes

KnownWidgetTypes is WidgetRegistry | readonly string[], from @evanion/widget: the known types validateItems takes, as a component map or a plain list of names. @evanion/widget’s entry carries the rest.

Migrating react-widget 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 here under the names the whole family uses.

WasNow
WidgetComponentMapWidgetRegistry<AnyWidgetComponent>
WidgetItemProblemWidgetProblem
WidgetPropsAnyWidgetItem

chrome.wrapper now receives an items prop beside its children. It is optional, so an existing wrapper keeps compiling. If your wrapper spreads its props onto a DOM element, remove items first.

Migrating react-widget from 0.1.x

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

RemovedReplacement
'use client'none needed; the package is importable from a Server Component
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.LOADINGremoved with the boundary that used them

renderWidget and NestedWidgetsContext are no longer exported. The nesting mechanism is internal, so a change to it is not a breaking release.

Last updated on