Hooks and slots

Read store data with engine hooks, and inject your own content into the page with hook slots.

Read this when you need store data inside a component, or want to add content to an engine page without replacing it.

Two different things share the word "hook" here, so keep them apart.

  • React hooks (useStore, useTheme, …) are functions a component calls to read data. Any function whose name starts with use is a React hook.
  • Hook slots are named spots in the engine's pages where you can inject content without replacing the page.

Engine hooks

Each engine hook has its own import path, so you only bundle what you use. The most common:

HookGives you
useStoreThe store: name, settings, and a refresh()
useThemecolor, font, settings and isRTL
useMoneyformat(amount) for prices
useProductA product whose price and stock follow the chosen options
useWishlisthas, add, remove, toggle, ids, loading
useUserThe signed-in customer
useAsyncFnWrap an async action with a loading flag
app/components/product/ProductCard.tsx
import { useWishlist } from '@salla.sa/twilight-theme-engine/hooks/useWishlist';

function WishlistButton({ productId }: { productId: number }) {
  const wishlist = useWishlist();
  return <button onClick={() => wishlist.toggle(productId)}>♥</button>;
}

Each hook has a page in the Hooks tab: useStore, useTheme, useMoney, useProduct, useWishlist and useAsyncFn.

Hooks only work inside a component, and only below TwilightProvider, which the root route already sets up.

Hook slots

The engine puts a HookSlot at useful places in its pages. A slot renders whatever you registered under its name, and nothing if you registered nothing. Some of the names (from the HookName enum):

AreaNames
Pagebody:start, body:end, head:start, head:end
Header and footerheader:start, header:end, footer:start, footer:end, copyright
Productproduct:single.description.start, product:single.description, product:single.description.end
Cartcart:items.start, cart:items.end, cart:summary.start, cart:summary.end

A slot exists only where a page renders it. The header and footer slots live inside the engine's own header and footer, so if your layout or a forked page replaces them, render the slot there yourself.

Register content in a slot

hookRegistry.register(name, handler, priority) adds a handler. The handler receives a context and returns markup, or null to show nothing. Higher priority runs first, and the default is 50. The starter theme does this once at startup:

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

hookRegistry.register(
  HookName.BODY_END,
  (context: HookContext) => {
    const { twilight } = context;
    if (twilight?.theme?.settings?.enable_add_product_toast === false) return null;
    return <AddProductToast />;
  },
  50
);

The context always has twilight (store, theme, currency and more). Some slots add more: the product description slot also passes product.

Call your registration function from app/router.tsx so it runs on both the server and the browser. The slot recipe walks through it.

Register from a component

useHook(name, handler, priority) registers while a component is mounted:

import { useHook } from '@salla.sa/twilight-theme-engine/hooks';

function PromoRegistration() {
  useHook('header:start', () => <PromoBar />);
  return null;
}

When the component unmounts, useHook removes only the handler it registered. For something the whole theme needs, register once at startup with hookRegistry.register.

Add a slot to your own page

Put a HookSlot where you want others (or later you) to inject content. fallback shows when nothing is registered:

import { HookSlot } from '@salla.sa/twilight-theme-engine/hooks';

function FaqPage({ page }: { page: { id: string } }) {
  return (
    <>
      <h1>FAQ</h1>
      <HookSlot name="my-page:after-title" context={{ page }} fallback={null} />
    </>
  );
}

See HookSlot and useHook for every prop.

Next: Styling, to give your theme its look.

On this page