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

Adding content without editing a page: hook slots

Beginner9 min

Engine pages leave named slots; register a function for a name and what it returns appears on every page with that slot.

Say you want a delivery note under the "Add to cart" button of every product. The product page belongs to the engine, and copying it just to add one line means maintaining a whole page forever.

Engine pages leave named, empty places for exactly this: hook slots. product:single.form.end is the place right after the add-to-cart form. Your theme registers a small function under that name, and whatever the function returns appears there, on every page that has the slot.

A slot and its handler, live

The dashed box is a HookSlot with a name of the playground's own. The function registered for it, the handler, receives the slot's context (here, badge) and the engine's context (twilight: store, theme, language).

A HookSlot with one handler. The handler reads the slot context (badge) and the engine context (twilight).Try this: clear the badge text: the handler returns null, so the slot is empty, yet the fallback stays hidden because a handler is registered.
Storefront canvas · ar · RTL
No handler for this slot
Controls
Handler registered
What a theme writes
import { HookSlot } from '@salla.sa/twilight-theme-engine/hooks/HookSlot';

export function PromoArea() {
  return (
    <HookSlot
      name="playground:hook-slot"
      context={{ badge: 'Free delivery' }}
      fallback={<em>{'No handler for this slot'}</em>}
    />
  );
}

Where the engine leaves slots

Pick an area to see its slot names and where each one sits.

Rendered by TwilightProvider (src/providers/TwilightProvider.tsx)
Slot nameWhere it appears
head:start · head · head:endAt the top of the page body, despite the names: not inside the head element.
body:startBefore the layout.
body:endAfter the page, at the end of the body.

Registering handlers in a theme

Keep every registration in one file and run it once, when the theme starts. Registration adds a handler; it never replaces one, so running the file twice shows everything twice.

app/hooks/index.tsx
// app/hooks/index.tsx
import { hookRegistry, HookName, type HookContext } from '@salla.sa/twilight-theme-engine/hooks';

let registered = false;

export function registerThemeHooks() {
  if (registered) return; // registering twice renders everything twice
  registered = true;

  // Appears after the add-to-cart form, on every product page.
  // HookContext types the argument: the slot's context plus `twilight`.
  hookRegistry.register(HookName.PRODUCT_FORM_END, ({ twilight }: HookContext) => (
    <p className="text-sm">Delivered by {twilight.store.name} in 2 to 4 days.</p>
  ));

  // Priority decides the order when several handlers share a slot: higher first (default 50).
  hookRegistry.register(
    HookName.CART_SUMMARY_START,
    () => <p className="text-sm">Free delivery on orders over 200 SAR</p>,
    80
  );
}
app/router.tsx
// app/router.tsx (excerpt): register once, before the router is created
import { registerThemeHooks } from './hooks';

registerThemeHooks();
app/components/LookbookPage.tsx
// Your own components can offer slots too. Use a name of your own.
import { HookSlot } from '@salla.sa/twilight-theme-engine/hooks/HookSlot';

export function LookbookPage({ season }: { season: string }) {
  return (
    <main>
      <h1>Lookbook</h1>
      <HookSlot name="my-theme:lookbook.start" context={{ season }} fallback={null} />
    </main>
  );
}
In engine terms
  • hookRegistry.register(name, handler, priority = 50) appends and sorts handlers by priority, highest first, then notifies subscribers. clear(name) removes all of a slot's handlers; defineHooks({...}) registers many at once (src/hooks/HookRegistry.ts).
  • <HookSlot name context? fallback? ssr? /> subscribes to the registry, so a handler registered after the slot appeared shows up at once. It renders each handler with { ...context, twilight: useTwilight() }. fallback shows only while no handler is registered: handlers that all return null leave the slot empty, fallback included (src/hooks/HookSlot.tsx).
  • Next to your handlers it renders Salla's salla-hook element, where installed apps inject their own markup: only after hydration, unless ssr is set.
  • A handler runs each time its slot renders, on the server as well as in the browser. When the content needs state or effects, return a component of your own from it.
  • Import hookRegistry, HookName and HookSlot from @salla.sa/twilight-theme-engine/hooks (or the /hooks/HookRegistry and /hooks/HookSlot subpaths).
  • Reference: HookSlot, hookRegistry, HookName; the recipe Add content to every page with a hook slot.
Check yourself

You register a handler for product:single.form.end. Where does its content appear?