Overriding an engine component
register, override, resolve: how the registry swaps a component, which names the engine looks up, and the silent no-op.
The engine draws some parts of the store through a registry: a table of names and components. Put your own component under one of those names and the engine draws yours instead, everywhere, without copying a page.
It works for a few names only, and one of them needs two calls. Step through what happens.
1. register stores a component under a name
The registry is a shared address book: a name on one side, a component on the other. Your theme writes to it once, when it starts, in app/router.tsx. register stores a component under a name.
In engine terms
registry (root export, src/components/ComponentRegistry.ts) is a module-level Map of { component, displayName, original? }, shared by every request the server handles, so register at module scope, never per request or per render. register(name, C) stores { component: C } with no original, replacing whatever was there.
Which names the engine resolves
| Name | Looked up by | What your theme calls |
|---|---|---|
product:card | ProductCard, in every grid and slider | register, then override |
product:gallery | ProductGallery, on the product page | register |
home:<path>, home:<path>:<view_style> | HomeComponentRenderer, per home block | registerHomeComponents |
account:layout-pending | The account layout while it loads | register |
| Any other name | Only your own Component or useComponent | register |
import { memo } from 'react';
import { registry } from '@salla.sa/twilight-theme-engine';
import type { ProductCardProps } from '@salla.sa/twilight-theme-engine/components/product';
import { Link } from '@salla.sa/twilight-theme-engine/components/common';
import { useMoney } from '@salla.sa/twilight-theme-engine/hooks/useMoney';
// A full replacement: the engine's card is not available to wrap.
export const MyProductCard = memo(function MyProductCard({ product, className }: ProductCardProps) {
const { format } = useMoney();
return (
<article className={['my-card', className].filter(Boolean).join(' ')}>
<Link to={product.url}>
<img src={product.image?.url} alt={product.name} width={300} height={300} loading="lazy" />
<h3>{product.name}</h3>
</Link>
<p>{format(product.price)}</p>
</article>
);
});
// app/router.tsx, at module scope, in this order:
registry.register('product:card', MyProductCard); // seed the name
registry.override('product:card', MyProductCard); // now getOriginal() is not null
Try the calls
The lab below runs the same calls on a playground name and shows what ProductCard would do with them. Under it, the registry of this tab: the home: names are the playground's home blocks.
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.
Reading the registry…
Why it matters
- Your override does nothing, with no warning:
product:cardgotoverridewithoutregister, or the name is not one the engine looks up (header,footer,product.card). - Cards already on screen keep the old look: each card decides once, when it mounts. Register in
app/router.tsx, beforecreateRouter(). - The page freezes or crashes after overriding the card: your component renders the engine's
ProductCard, which renders your component again. Treat the override as a full replacement. - You only want to add something next to the card or the header: a hook slot is usually the better tool, and it cannot silently do nothing.
Go deeper: registry, Component, useComponent, defineComponent, registerComponents, ProductCard and ProductGallery. Home blocks use the same registry: see Home page blocks.