The provider: data every component can reach
Context shares values without passing props; TwilightProvider shares the store, theme and language, and useTwilight reads them.
Almost every component in a store needs the same few facts: which store this is, its colors, the page language. Passing them as props from the top of the page down through every component would be tedious and fragile.
React's answer is context: a value placed once, high in the tree, that any component below can read directly. The component that places it is called a provider.
Context in thirty seconds
The label below sits four components deep. None of the boxes in between passes it anything, yet it follows the value given to the provider at the top.
// Without context: every level passes size along, even those that never use it.
function ProductGrid({ size }) {
return <ProductCard size={size} />;
}
function ProductCard({ size }) {
return <CardFooter size={size} />;
}
function CardFooter({ size }) {
return <PriceLabel size={size} />;
}
import { createContext, useContext } from 'react';
const CardSizeContext = createContext('comfortable');
// Put a value at the top…
export function Catalog() {
return (
<CardSizeContext.Provider value="compact">
<ProductGrid />
</CardSizeContext.Provider>
);
}
// …and read it anywhere below, without passing it through the middle.
function PriceLabel() {
const size = useContext(CardSizeContext);
return <span className={`price price--${size}`}>228.00 SAR</span>;
}
When the provider's value changes, React re-renders the components that read it, wherever they are.
The engine's provider: TwilightProvider
A theme mounts TwilightProvider exactly once, around every page, in app/routes/__root.tsx. useTwilight() reads its whole value; useStore(), useTheme() and useMoney() read parts of it. Below is the value this page was rendered with, from the real demo store.
// app/routes/__root.tsx (excerpt)
<body suppressHydrationWarning>
<TwilightProvider translations={themeTranslations}>
<Outlet /> {/* every page of the theme renders here */}
</TwilightProvider>
<Scripts />
</body>
useTwilight() → store: {…} 24 keys
import { useTwilight } from '@salla.sa/twilight-theme-engine';
export function Example() {
const { store } = useTwilight();
return <span>{store.name}</span>;
}
import { useTwilight } from '@salla.sa/twilight-theme-engine';
import { useStore } from '@salla.sa/twilight-theme-engine/hooks/useStore';
import { useTheme } from '@salla.sa/twilight-theme-engine/hooks/useTheme';
export function StoreLine() {
const { locale, dir } = useTwilight(); // the whole context
const { name } = useStore(); // the store part of it
const { color } = useTheme(); // the theme part of it
return (
<p lang={locale} dir={dir} style={{ color: color.primary }}>
{name}
</p>
);
}
What else it sets up
Sharing data is only part of its job. Around your pages it also:
- starts the Salla SDK in the browser and keeps logins in step with the server;
- sets up translations for
useTranslation(), with your theme's own texts added; - applies the theme to the document: body classes, the merchant's colors and font as CSS variables;
- wraps every page in the layout, the header and footer (
MasterLayout) unless you pass another; - renders the toast notifications, the
body:startandbody:endhook slots, and the snippets of apps the merchant installed; - turns clicks on plain links to the store's own pages, such as links inside Salla's web components or a merchant's content, into in-app navigation without a full page reload.
In engine terms
- Import
TwilightProvideranduseTwilightfrom@salla.sa/twilight-theme-engine. Outside a provider,useTwilight()throws "useTwilight must be used within a TwilightProvider" (src/providers/twilight-context.ts). - The context value is memoized on
isReady,store,theme,currency,config,logandauthToken.settings,locale,routeId,location,i18n,dirandextrasare getters that readgetTwilightContext()when accessed: reading them never subscribes a component to their changes (src/providers/TwilightProvider.tsx). isReadystartstruewhenever the store settings loaded, so it is alreadytruein the server render. It does not mean the SDK is ready: wait forSalla.onReady()or thetheme::readyevent for that.- Useful props:
translations(fromvirtual:twilight/theme-translations),layout(a component, orfalsefor none; defaultMasterLayout),toast,addToCartToastandtoastMobilePosition,client({ framework: 'tanstack', interceptLinks: true }by default) androuteClass. - Reference: TwilightProvider, useTwilight, and getTwilightContext for loaders and head functions, which run outside React.