Skip to Content
WidgetYour Own Renderer

Write your own widget renderer

@evanion/widget 0.1.0 and its patch releases up to 0.1.3, the version on npm. Values on this page were produced by running the source at f9b1d93, a later commit than the 0.1.0 tag, and are not re-executed.

If you are using a framework without a published renderer, you can implement your own. To ensure your renderer provides a consistent developer experience, you should synchronize its warnings with the @evanion/widget core.

The Problem: Inconsistent Warning Logs

When a renderer encounters a widget type it doesn’t have a component for, it must skip that item. If every renderer in your ecosystem has its own way of warning about this, your logs become a mess of inconsistent messages.

Both @evanion/react-widget and @evanion/astro-widget solve this by using ERROR_MESSAGES from the core. This ensures that regardless of the framework, the same unknown type produces the same log line.

import { } from '@evanion/widget'; const = .('listting', 'hive'); ; // -> 'Unknown widget type "listting" for widget ID "hive". Skipping render.'

Using warnOnce

Renderers run during the render loop. Without protection, a single unknown widget would trigger a warning on every single re-render, flooding your console.

@evanion/widget provides warnOnce, which prints a message the first time it is encountered in a process and then silences it. It also automatically suppresses all warnings when NODE_ENV is set to production.

Implementing the Render Loop

A basic renderer iterates over the widget items, looks up the type in a registry, and renders the corresponding component. If the type is missing, it uses warnOnce to report the issue.

import { , , } from '@evanion/widget'; import type { , } from '@evanion/widget'; type = <string, { ?: string }>; const : <(: { ?: string }) => string> = { : () => `Listing: ${.}`, }; function (: []): string[] { return .(() => { // An own key only: a CMS type of `constructor` is unknown. const = ...(, .) ? [.] : ; if ( === ) { (.(., .)); return []; } return [(.)]; }); } // The set of printed messages lives as long as the process, so a renderer's // tests clear it before each case. (); const : [] = [ { : 'wingspan', : 'listing', : { : 'Wingspan' } }, { : 'hive', : 'listting', : { : 'Hive' } }, ]; (); // -> ['Listing: Wingspan'] (); // -> ['Listing: Wingspan']

Key implementation details:

  • Registry Lookup: Use Object.prototype.hasOwnProperty to check for the type. Avoiding item.type in registry prevents the renderer from accidentally trying to render inherited object properties (like toString).
  • Warning Reset: In your tests, call resetWarnings before each case to ensure that one test’s warning doesn’t silence the next.
  • Production Silence: In production, warnOnce prints nothing. This is why you must run validateItems as a separate quality gate (as shown in Getting started) to catch content errors before they hit production.

Extending the Renderer for Complex Layouts

To build a production-ready renderer, you need to handle four additional responsibilities:

  1. Children: Decide how to handle nested items. You can either draw them inside the parent (like @evanion/react-widget) or pass them as data to the parent’s component (like @evanion/astro-widget).
  2. Meta: Pass the meta property to the item chrome (chrome.item), never to the widget’s props.
  3. Structural Warnings: Use warnOnce to report structural failures:
    • MALFORMED_ITEMS: The items provided are not an array.
    • MALFORMED_ITEM(id, type): An entry is not an object or lacks a string type.
    • MALFORMED_CHILDREN(id): The children field is present but is not an array.
  4. Separation of Concerns: Do not call validateItems inside the renderer. The renderer’s job is to draw what it can and warn about what it can’t. The validation check should run where the payload first enters your system.

Every widget core export

The API reference lists every export of @evanion/widget with its signature and the shape of each ERROR_MESSAGES key.

Last updated on