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 withuseis 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:
| Hook | Gives you |
|---|---|
useStore | The store: name, settings, and a refresh() |
useTheme | color, font, settings and isRTL |
useMoney | format(amount) for prices |
useProduct | A product whose price and stock follow the chosen options |
useWishlist | has, add, remove, toggle, ids, loading |
useUser | The signed-in customer |
useAsyncFn | Wrap an async action with a loading flag |
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):
| Area | Names |
|---|---|
| Page | body:start, body:end, head:start, head:end |
| Header and footer | header:start, header:end, footer:start, footer:end, copyright |
| Product | product:single.description.start, product:single.description, product:single.description.end |
| Cart | cart: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:
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.