Skip to content
Twilight React Playground
ثيم رائدaren

Swapping a component: the registry

Beginner8 min

Replace the engine's product card everywhere by registering your own component under product:card.

Hook slots add content next to engine components. Sometimes you want to replace one instead: your own product card in every grid, slider and product list, without copying any of those pages.

For a few parts the engine does not import the component directly. It looks it up by name in the registry, a shared address book of name → component. ProductCard looks up product:card. Store your own component under that name, and every product card on the storefront becomes yours.

Your card next to the engine's

A real product from the store, drawn twice: once by the engine's ProductCard, once by a small card of your own. Here both are rendered directly; the registry is what would put yours in place of the engine's everywhere.

Loading a product…

app/components/MyProductCard.tsx
// app/components/MyProductCard.tsx
import type { ProductCardProps } from '@salla.sa/twilight-theme-engine/components/product';
import { useMoney } from '@salla.sa/twilight-theme-engine/hooks/useMoney';

export function MyProductCard({ product }: ProductCardProps) {
  const { format } = useMoney();
  const price = product.is_on_sale && product.sale_price ? product.sale_price : product.price;

  return (
    <article style={{ display: 'grid', gap: 8, padding: 12, border: '2px solid', borderRadius: 16 }}>
      <img
        src={product.image?.url}
        alt={product.image?.alt || product.name}
        loading="lazy"
        style={{ width: '100%', aspectRatio: '1 / 1', objectFit: 'cover', borderRadius: 12 }}
      />
      <strong>{product.name}</strong>
      <span style={{ fontSize: 18 }}>{format(price)}</span>
    </article>
  );
}
app/router.tsx
// app/router.tsx (excerpt)
import { registry } from '@salla.sa/twilight-theme-engine';
import { createRouter } from '@salla.sa/twilight-theme-engine/tanstack';
import { MyProductCard } from './components/MyProductCard';
import { routeTree } from './routeTree.gen';

// At module scope, before any page renders. For product:card, both calls:
registry.register('product:card', MyProductCard);
registry.override('product:card', MyProductCard);

export function getRouter() {
  return createRouter(routeTree);
}

Why two calls

register stores a component under a name. override stores one too, but keeps whatever was there before as the original. ProductCard uses the registry only when an original exists, so product:card needs both calls. The lab below makes the same calls on a playground key and shows what ProductCard would do.

Registry calls on a playground key, and what ProductCard would do if the same calls were made on product:card.Try this: switch to "override only": the key resolves to MyCard, but getOriginal is null, so ProductCard keeps its default.
Storefront canvas · ar · RTL
Runs in the browser…
Controls
What a theme writes
import { registry } from '@salla.sa/twilight-theme-engine';
import { MyProductCard } from './components/MyProductCard';

// app/router.tsx: at module scope, before createRouter()
registry.register('product:card', MyProductCard);
registry.override('product:card', MyProductCard);

// Where the engine renders <ProductCard product={…} /> (grids, sliders, listings),
// MyProductCard receives the same props, but only after both calls.

The registry for your own parts

app/components/PromoArea.tsx
// Your own swappable parts use the same registry, with a name of your own.
import { Component, registry } from '@salla.sa/twilight-theme-engine';
import { SeasonalPromo } from './SeasonalPromo';

registry.register('my-theme:promo', SeasonalPromo);

export function PromoArea() {
  // Renders whatever is registered under the name, or the fallback.
  return <Component name="my-theme:promo" title="Summer sale" fallback={null} />;
}
In engine terms
  • registry is one ComponentRegistry instance exported from @salla.sa/twilight-theme-engine, with register, override, resolve, getOriginal, has, list, listByPrefix, remove and clear (src/components/ComponentRegistry.ts).
  • ProductCard resolves product:card in a useMemo with no dependencies, only when registry.getOriginal('product:card') !== null, and passes every prop through (src/components/product/ProductCard.tsx).
  • ProductGallery is looser: it renders whatever product:gallery resolves to, unless that is ProductGallery itself, so register alone is enough there.
  • Home-page blocks are registry entries too: registerHomeComponents() from @salla.sa/twilight-theme-engine/routes/home registers each block under home:<path>.
  • The registry is a module-level object: on the server one copy is shared by every request, so register once at startup, never per request or per render.
  • Reference: registry, Component, registerComponents, ProductCard, registerHomeComponents.
Check yourself

A theme calls only registry.override('product:card', MyCard). What do product grids show?