Contents

Custom Props and Click Handlers

Maya's homepage hero has a CTA Button. She wants that click to open her booking drawer. The composer cannot know about the drawer — that code lives in her app. Copy the Component id from the Modify drawer, then pass it on RuleCMSWidget as a key in componentProps. The Button keeps everything she styled. The click becomes hers.

What you can do

She already embeds the Development homepage hero from Development Integration. The CTA on that hero is a Button she dropped in the composer. Clicking it should open her booking drawer.

The composer cannot know about the drawer. That code lives in her app. She should not have to rebuild the Button in React just to attach a handler.

componentProps is the handshake. She copies the Component id from the Modify drawer and passes a small map on RuleCMSWidget. The Button keeps the label, the preset, and the column styles. The click becomes hers.

Two everyday jobs:

  • Open something only her app owns. The booking drawer. A checkout. A login modal.
  • Keep the author's Button. She restyles the CTA next week. The id does not change. The handler stays wired.

She builds with the ten built-in cards — Text, Image, Video, Icon, Button, Divider, Embed, List, Accordion, Custom — or a library she registered. The built-in that documents onClick today is Button. Passing a prop a component does not read is harmless. A library card can receive these props too — see Component Libraries. A Custom slot that should render a host component is Mount Your Own Components. Use this page's column id for handlers on that slot; do not mix the two jobs.

How it thinks

The id is the address. componentProps is a map. Each key is a Component id from the Modify drawer. Whatever she puts in that entry is handed to that one instance while it renders. RuleCMS does not interpret the keys inside the entry. The component does.

A Component id stays put when she edits the label, the preset, or the column styles. Delete the Button and add another one, and that is a new component with a new id. Re-copy it.

A key that matches nothing is silent in production. The page looks fine. Outside production, the browser console names the unmatched keys so she can catch a typo while she is wiring:

RuleCMSWidget "widget-…": componentProps "b721c4e2-…" matches no component in this widget, so those props were not passed to anything. Keys are column ids from the widget config — the composer's Modify drawer shows the id of the selected component.

Usual causes: she copied the id from a different widget, she deleted and re-added the Button, or a stray character landed in the constant.

Here is the case that surprises people. A collection can sit on the hero more than once — two product cards, same inner Button. Every copy shares the same inner ids. A bare Component id from inside the collection reaches every copy. That is often what she wants. When it is not, she prefixes the Embedding id of that collection, with a slash: embedding-id/inner-id. Embedding id is on the collection itself, not a child. Click the collection in the composer. The drawer labels its id Embedding id. A narrowed path wins over a plain id, so she can track every copy and still give one of them the booking drawer.

She can pass values, not just functions. A component only reacts to the props it documents. Content the author should control belongs in the composer, not here.

The composer never runs her handler. Authors editing the hero never trigger her application code.

Wire Maya's homepage hero CTA

She does this after the hero is on her local site from Development Integration: a dev. token and the draft widget-… key from Integrate. The Default Widget is Production-only. She created this hero in Development.

  1. Open the homepage hero in the composer.
  2. Click the CTA Button. The Modify drawer opens.
  3. At the top of the drawer, open the Component id accordion — it starts closed so it does not compete with the settings she is editing. Click Copy.
  4. In her app, pass that id as a key in componentProps on RuleCMSWidget. Put onClick in the entry.
'use client';

import { useCallback } from 'react';
import { RuleCMSWidgetProvider, RuleCMSWidget } from '@rulecms/widget-react';
import * as sourceComponents from '@rulecms/source-components-react';

const token = process.env.NEXT_PUBLIC_RULECMS_DEV_TOKEN; // must start with "dev."
const widgetKey = 'widget-…';
const HERO_CTA_ID = 'b721c4e2-8f0a-4c31-9a77-1d5e3f0b2c44';

export function HomepageHero() {
  const openBookingDrawer = useCallback(() => {
    // her booking drawer
  }, []);

  return (
    <RuleCMSWidgetProvider
      token={token}
      libraries={{ default: sourceComponents }}
    >
      <RuleCMSWidget
        publishedKey={widgetKey}
        componentProps={{ [HERO_CTA_ID]: { onClick: openBookingDrawer } }}
      />
    </RuleCMSWidgetProvider>
  );
}

Define the handler with useCallback, or keep the componentProps object outside render. A fresh object on every render makes the widget re-render along with it.

That is a safe stop. Save. Refresh. Click the CTA. The booking drawer opens. The Button still looks the way she styled it in the composer.

When the Button also has a Link

If she also set a Link on the Button — the setting on Button — the handler runs, then the browser follows the link. To keep the click in-app, cancel the navigation:

const onClick = useCallback((event) => {
  event.preventDefault();
  openBookingDrawer();
}, [openBookingDrawer]);

When she disabled the Button

A disabled Button never calls the handler. Disabled is the author's decision and it wins. There is no way to click through it from her code.

When two copies of a collection share the Button

Prefix the inner id with the collection's Embedding id. The narrowed path wins over the plain id:

componentProps={{
  // every copy of the collection
  'inner-id': { onClick: trackClick },
  // only the copy embedded at this spot
  'embedding-id/inner-id': { onClick: openBookingDrawer },
}}

When you need the HTML on the server

RuleCMSWidgetServer cannot take componentProps. It is a React Server Component, and a function cannot be passed from the server to the browser.

To keep server-rendered HTML and still get a working handler, fetch the widget on the server and render it from a client component:

// page.tsx — Server Component
import { fetchRuleCMSWidget } from '@rulecms/widget-react/server';
import { HomepageHero } from './HomepageHero';

export default async function Page() {
  const initialData = await fetchRuleCMSWidget({
    publishedKey: widgetKey,
    token,
  });
  return <HomepageHero publishedKey={widgetKey} initialData={initialData} />;
}
// HomepageHero.tsx
'use client';

import { RuleCMSWidget } from '@rulecms/widget-react';

export function HomepageHero({ publishedKey, initialData }) {
  return (
    <RuleCMSWidget
      mode="pre-fetched"
      publishedKey={publishedKey}
      initialData={initialData}
      libraries={{ default: sourceComponents }}
      componentProps={{ [HERO_CTA_ID]: { onClick: openBookingDrawer } }}
    />
  );
}

The handler starts working once the page has loaded her JavaScript. A click in the moment before that does nothing, as with any React handler.

The HTML Custom Element Embed cannot carry a function either. Its API is HTML attributes. Attach a listener to the surrounding element instead.

What to read next

This page is the walkthrough for extra props by column id. Embed, Button settings, mounts, and libraries live on the pages below.

  • Development IntegrationEmbed the Development hero first: the dev. token and the draft widget-… key from Integrate.
  • ButtonThe settings she controls on the CTA: Link, disabled, the label.
  • Mount Your Own ComponentsA Custom slot that should render a component from her app. Use the same column id here for handlers.
  • Component LibrariesA first-class palette card can read these props too.

How this fits

This page does not replace an embed. Development Integration is how she puts the hero on the page. This page is how the CTA talks to her app.

It is also not a Custom slot. Mount Your Own Components registers a host component by name. This page addresses extra props by Component id. A mounted component can still receive handlers the same way.

The Default Widget is in Production. She built this hero in Development. Promote copies the draft to the next environment; it does not publish. When she publishes, she switches the app to a Staging or Production token — no dev. prefix — and that published key. The Component id does not change. Do not mix a dev. token with a published key, or a published token with a draft widget-… key.