Skip to Content
React WidgetGetting Started

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.

Getting started

Install

npm install @evanion/react-widget
yarn add @evanion/react-widget
pnpm add @evanion/react-widget

React 18 or 19 is the peer dependency. Node 20 or newer. The package is ESM only.

Three pieces

A widget set is a component map, a list of items, and the Widgets component that renders one against the other. Baize’s shelf is two widget types — a listing card and the table-booking strip — and the items the shop’s data gives it.

import { } from '@evanion/react-widget'; const = ({ , }: { : string; : number }) => ( < ="listing"> <>{}</> <>{} kr</> </> ); const = ({ , , }: { : number; : boolean; }) => ( < ="booking"> <>{} tables</> <>{ ? 'tonight' : 'this week'}</> </> ); // Call once, at module scope. const { , } = ({ : { : , : }, : { : ({ }) => < ="shelf">{}</>, }, }); export function () { return ( < ={[ { : 'b1', : 'booking', : { : 4, : true } }, { : 'g1', : 'listing', : { : 'Brass: Birmingham', : 649 }, }, ]} /> ); }

That page can be a Server Component. Nothing in the package forces the widgets into the client bundle; a widget that needs client state carries its own 'use client' and nothing else changes.

Why once at module scope

createWidgets is a factory, not a hook and not a provider. Calling it inside a component builds a new Widgets identity on every render, which remounts the whole region. There is no provider to call it from instead: React’s react-server export condition has no createContext.

The item shape

{ id: string; // stable identity, used as the React key type: keyof typeof components; // which component to render props: ComponentProps<That>; // minus `children` and `ctx` meta?: M; // for chrome.item only children?: Item[]; // only if that component accepts children }

props excludes children and ctx because the renderer supplies both. A widget may declare ctx required without every item having to repeat a value <Widgets> is going to pass anyway, and an item cannot override it.

A widget whose component declares no props at all gets props: Record<string, never> rather than {}. TypeScript assigns any object to {} without an excess-property check, so props: { totally: 'bogus' } on a zero-prop widget would otherwise compile clean — a hole in the guarantee, opening exactly where the component is simplest.

Typing items held in a variable

Written inline in JSX, an item array is already contextually typed. Assigned to a variable it is not: const items = [{ type: 'listing', … }] infers type: string, which cannot narrow to the map’s keys, and the check is silently lost.

defineItems is the identity function that supplies the type:

const { Widgets, defineItems } = createWidgets({ components: { listing: ListingCard, booking: TableBooking }, }); const items = defineItems([ { id: '1', type: 'listing', props: { title: 'Wingspan', price: 549 } }, { id: '2', type: 'nope', props: {} }, // ✗ not a key of the map { id: '3', type: 'booking', props: { tables: 'warm' } }, // ✗ tables is a number ]);

satisfies WidgetItem<typeof components>[] works too, if you would rather not destructure a second name off the factory.

Nesting

Nested items render as the parent component’s children:

const Card = ({ title, children }: PropsWithChildren<{ title: string }>) => ( <section> <h3>{title}</h3> {children} </section> ); const { Widgets } = createWidgets({ components: { card: Card, text: Text } }); <Widgets items={[ { id: 'c1', type: 'card', props: { title: 'My card' }, children: [{ id: 't1', type: 'text', props: { content: 'Nested' } }], }, ]} />;

children is type-gated: permitted only when the mapped component actually accepts children, and typed never otherwise. Nesting under a widget that would drop the child items is a compile error rather than content that silently disappears.

There is no Output component and no outlet prop. A widget renders its nested items by rendering children, like any other React component.

What happens to bad data

The type checker covers items you wrote. For a CMS payload it does not, and the renderer stays defensive: a malformed item or an unknown type is skipped, and a non-array children is dropped and the widget rendered without it, each with a development-only console.warn rather than taking the page down.

Each message is logged once per process, keyed on its own text — which carries the offending item’s id and type, so a second bad item is still reported separately. Nothing is logged when NODE_ENV is production.

Run the loud check yourself at ingestion or in CI:

import { validateItems } from '@evanion/react-widget'; const problems = validateItems(payload, ['listing', 'booking', 'card']); if (problems.length) { console.error(problems); process.exit(1); }

Widgets never calls it. What it reports is the message catalogue: every line validateItems can return and the payload that produces it.

Next

Last updated on