npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@novasamatech/product-react-renderer

v0.6.8

Published

React wrapper for custom renderer format from product-sdk

Readme

@novasamatech/product-react-renderer

A custom React reconciler for rendering native UI widgets inside Polkadot host applications. Use it together with @novasamatech/product-sdk to render interactive widget trees in response to custom chat messages.

How it works

When the host app displays a custom chat message, it calls your script to produce a widget tree — a structured description of the UI to render natively (buttons, text, columns, etc.). This package implements a custom React reconciler that maps React components to that widget tree format, so you can use React features like useState, useEffect, and component composition to build your UI.

React component tree
      ↓  (React reconciler)
Widget tree (CustomRendererNode)
      ↓  (SCALE encoding)
Native Desktop/Mobile UI

Installation

npm install @novasamatech/product-react-renderer react --save -E

Setup

Configure your tsconfig.json to use React JSX:

{
  "compilerOptions": {
    "jsx": "react-jsx"
  }
}

registerChatMessageRenderer

The primary entry point for rendering custom chat messages. Pass a mapPayload function that decodes the raw bytes sent by the host, and a renderFn that returns the React element tree. The return value is a callback you pass directly to chat.onCustomMessageRenderingRequest().

Static message

import { registerChatMessageRenderer, Text } from '@novasamatech/product-react-renderer';

chat.onCustomMessageRenderingRequest(
  registerChatMessageRenderer(
    () => undefined,
    () => <Text style="headline">Hello from the product!</Text>,
  ),
);

Decoding a payload

mapPayload converts the raw Uint8Array the host sends before your renderFn sees it. A common pattern is JSON:

import { registerChatMessageRenderer, Column, Text } from '@novasamatech/product-react-renderer';

type BalancePayload = { token: string; amount: string };

chat.onCustomMessageRenderingRequest(
  registerChatMessageRenderer(
    raw => JSON.parse(new TextDecoder().decode(raw)) as BalancePayload,
    ({ payload }) => (
      <Column>
        <Text style="headline">{payload.amount}</Text>
        <Text color="textSecondary">{payload.token}</Text>
      </Column>
    ),
  ),
);

Interactive messages

Use standard React hooks for local state. Library automatically wires up callbacks to user interactions on Host side.

import { useState } from 'react';
import { registerChatMessageRenderer, Column, Text, Button } from '@novasamatech/product-react-renderer';

function VoteWidget() {
  const [votes, setVotes] = useState(0);
  return (
    <Column horizontalAlignment="center" padding={16}>
      <Text style="headline">Votes: {votes}</Text>
      <Button text="Vote" variant="primary" onClick={() => setVotes(v => v + 1)} />
    </Column>
  );
}

chat.onCustomMessageRenderingRequest(
  registerChatMessageRenderer(
    () => undefined,
    () => <VoteWidget />,
  ),
);

Using messageId and messageType

Both are forwarded to renderFn so you can adapt the UI per message:

import { registerChatMessageRenderer, Text } from '@novasamatech/product-react-renderer';

chat.onCustomMessageRenderingRequest(
  registerChatMessageRenderer(
    () => undefined,
    ({ messageId, messageType }) => (
      <Text color="textSecondary">
        [{messageType}] {messageId}
      </Text>
    ),
  ),
);

Components

All components accept the shared layout props in addition to their own props.

<Text>

| Prop | Type | Description | |------------|-------------------|------------------------------| | style | TypographyStyle | Font style | | color | ColorToken | Text color | | children | ReactNode | Text content or nested nodes |

TypographyStyle: titleXL · headline · bodyM · bodyS · caption

<Text style="headline" color="textPrimary">Balance: 42 DOT</Text>

<Button>

| Prop | Type | Description | |-----------|-----------------|-------------------------| | text | string | Label (required) | | onClick | () => void | Tap handler (required) | | variant | ButtonVariant | Visual style | | enabled | boolean | Defaults to true | | loading | boolean | Shows loading indicator |

ButtonVariant: primary · secondary · text

<Button text="Send" variant="primary" onClick={handleSend} />

<TextField>

| Prop | Type | Description | |-----------------|---------------------------|---------------------------| | value | string | Current value (required) | | onValueChange | (value: string) => void | Change handler (required) | | placeholder | string | Placeholder text | | label | string | Field label | | enabled | boolean | Defaults to true |

<TextField value={query} placeholder="Search…" onValueChange={setQuery} />

onValueChange receives the decoded string value each time the user edits the field.

import { useState } from 'react';
import {
  registerChatMessageRenderer,
  Column,
  Text,
  TextField,
  Button,
} from '@novasamatech/product-react-renderer';

function SearchForm() {
  const [query, setQuery] = useState('');

  function handleSubmit() {
    // send the query somewhere
  }

  return (
    <Column padding={16}>
      <TextField value={query} placeholder="Search…" onValueChange={setQuery} />
      <Button text="Search" variant="primary" onClick={handleSubmit} />
    </Column>
  );
}

chat.onCustomMessageRenderingRequest(
  registerChatMessageRenderer(
    () => undefined,
    () => <SearchForm />,
  ),
);

<Column>

Stacks children vertically.

| Prop | Type | Description | |-----------------------|-----------------------|----------------------| | horizontalAlignment | HorizontalAlignment | Cross-axis alignment | | verticalArrangement | Arrangement | Main-axis spacing |

HorizontalAlignment: start · center · end Arrangement: start · end · center · spaceBetween · spaceAround · spaceEvenly

<Column horizontalAlignment="center" verticalArrangement="spaceBetween" padding={16}>
  <Text style="headline">Title</Text>
  <Button text="OK" onClick={handleOk} />
</Column>

<Row>

Stacks children horizontally.

| Prop | Type | Description | |-------------------------|---------------------|----------------------| | verticalAlignment | VerticalAlignment | Cross-axis alignment | | horizontalArrangement | Arrangement | Main-axis spacing |

VerticalAlignment: top · center · bottom

<Row verticalAlignment="center" horizontalArrangement="spaceBetween">
  <Text>Label</Text>
  <Text color="textSecondary">Value</Text>
</Row>

<Box>

Single-child container with optional content alignment.

| Prop | Type | Description | |--------------------|--------------------|-----------------------------| | contentAlignment | ContentAlignment | Alignment of the child node |

ContentAlignment: topStart · topCenter · topEnd · centerStart · center · centerEnd · bottomStart · bottomCenter · bottomEnd

<Box contentAlignment="center" background="backgroundSecondary" padding={8}>
  <Text>Centered</Text>
</Box>

<Spacer>

Flexible space element. Use fillMaxWidth / fillMaxHeight or explicit width / height.

<Row>
  <Text>Left</Text>
  <Spacer fillMaxWidth />
  <Text>Right</Text>
</Row>

Layout props

Every component accepts these props to control sizing, spacing, and appearance.

Spacing

| Prop | Type | Description | |-----------|-----------|---------------| | padding | Padding | Inner spacing | | margin | Padding | Outer spacing |

Padding is a single number (all sides) or [top, bottom, start, end] for individual sides.

Sizing

| Prop | Type | Description | |-----------------|-----------|---------------------------------| | width | number | Fixed width | | height | number | Fixed height | | minWidth | number | Minimum width | | minHeight | number | Minimum height | | fillMaxWidth | boolean | Expand to fill available width | | fillMaxHeight | boolean | Expand to fill available height |

Background

background accepts either a ColorToken string or a BackgroundStyle object:

// Plain color
<Box background="backgroundSecondary" />

// Color + shape
<Box background={{ color: 'backgroundSecondary', shape: { tag: 'Rounded', value: 8 } }} />
<Box background={{ color: 'backgroundTertiary', shape: { tag: 'Circle' } }} />

Border

<Box border={{ width: 1, color: 'textTertiary' }} />
// With a rounded corner
<Box border={{ width: 1, color: 'success', shape: { tag: 'Rounded', value: 4 } }} />

Color tokens

| Token | Description | |------------------------|-----------------------------| | textPrimary | Primary text | | textSecondary | Secondary / supporting text | | textTertiary | Tertiary / hint text | | backgroundPrimary | Primary surface | | backgroundSecondary | Secondary surface | | backgroundTertiary | Tertiary surface | | success | Positive / success state | | warning | Warning state | | error | Error / destructive state |


createRenderer

The low-level primitive that registerChatMessageRenderer is built on. Use it directly when you need to manage the renderer lifecycle yourself or integrate it into a custom pipeline outside of the chat system.

createRenderer returns an object with two methods:

| Method | Description | |---------------|------------------------------------------| | mount(node) | Mount or update the element tree | | unmount() | Tear down the tree and release resources |

Basic usage

import { createRenderer, Column, Text, Button } from '@novasamatech/product-react-renderer';

const renderer = createRenderer({
  // Called after every commit with the serialized widget tree.
  onRender(node) {
    send(node);
  },

  // Subscribe to events from the host.
  // Return an unsubscribe function.
  subscribeActions: (callback) => {
    return actionsSubscription.subscribe((actionId, payload) => {
      callback(actionId, payload);
    });
  },
});

// Mount the initial tree.
renderer.mount(
  <Column>
    <Text style="headline">Hello</Text>
    <Button text="OK" onClick={() => console.log('clicked')} />
  </Column>,
);

// Unmount when done — cleans up the React tree and unsubscribes from actions.
renderer.unmount();

Re-mounting with new content

mount can be called multiple times to update the tree. React reconciles the difference, preserving component state where the component type is the same.

// First render
renderer.mount(<Text style="headline">Loading…</Text>);

// Later — update in place
renderer.mount(<Text style="headline">Done!</Text>);

Manual integration with onCustomMessageRenderingRequest

This is what registerChatMessageRenderer does internally. Writing it manually gives you full control over the teardown sequence:

import { createRenderer, Text } from '@novasamatech/product-react-renderer';

chat.onCustomMessageRenderingRequest(({ messageId, messageType, payload, subscribeActions }, render) => {
  const renderer = createRenderer({ onRender: render, subscribeActions });

  renderer.mount(<Text style="headline">{messageType}</Text>);

  // Return the cleanup callback.
  return () => {
    renderer.unmount();
  };
});