React
`@multitenant/react` — TenantProvider, hooks, SSR/RSC.
React context and hooks for tenant, market, feature flags, theme, experiments, and per-tenant config — fed from server-provided ResolvedTenant plus a client-side TenantRegistry rebuilt from the same static config the server used.
TenantProvider and hooks run in the client tree ('use client' in Next).
Install
npm install @multitenant/react reactPeer: react ≥ 18.
Mental model
| Layer | Responsibility |
|---|---|
| Server (RSC, SSR, loader) | Resolve ResolvedTenant with createTenantRegistry + adapter (getTenantFromHeaders, resolveByRequest, …). |
| Client | TenantProvider + useTenant, useMarket, useTenantConfig, … |
ResolvedTenant is plain JSON — safe across the server→client boundary. TenantRegistry is not serializable: on the client, import the same tenants.config.json (or shared module) and call createTenantRegistry(config) once, then pass registry + tenant into TenantProvider.
Client shell (TenantProvider)
'use client';
import type { ResolvedTenant, TenantsConfig } from '@multitenant/core';
import { createTenantRegistry } from '@multitenant/core';
import { TenantProvider } from '@multitenant/react';
import tenantsConfig from '../tenants.config.json';
const registry = createTenantRegistry(tenantsConfig as TenantsConfig);
export function AppShell({
tenant,
children,
}: {
tenant: ResolvedTenant;
children: React.ReactNode;
}) {
return (
<TenantProvider registry={registry} tenant={tenant} environment={tenant.environment}>
{children}
</TenantProvider>
);
}Server layout passes tenant from requireTenant(headers(), registry) or equivalent — see Next.js — App Router.
Hooks
'use client';
import {
useTenant,
useMarket,
useTenantFlag,
useTenantTheme,
useExperiment,
useTenantConfig,
useTenantContext,
} from '@multitenant/react';
export function TenantBanner() {
const { tenantKey, marketKey, flags } = useTenant();
const market = useMarket();
const beta = useTenantFlag('beta');
const theme = useTenantTheme();
return (
<div>
{tenantKey} · {marketKey} · {market.locale} · {market.currency}
{beta ? ' · beta' : ''}
{theme?.preset ? ` · theme ${theme.preset}` : ''}
</div>
);
}
export function ExperimentSlot() {
const variant = useExperiment('hero_copy');
return <section data-variant={variant}>…</section>;
}
export function TenantSettingsPanel() {
const settings = useTenantConfig<{ supportEmail?: string }>();
return <a href={`mailto:${settings.supportEmail}`}>Support</a>;
}
/** Escape hatch: registry + tenant + environment in one object */
export function DebugTenant() {
const { registry, tenant, environment } = useTenantContext();
return <pre>{JSON.stringify({ environment, tenantKey: tenant.tenantKey }, null, 2)}</pre>;
}useMarket()—NormalizedMarketfromregistry.markets[tenant.marketKey](throws if missing).useTenantFlag(name)— boolean fromtenant.flags[name].useExperiment(key)— string variant fromtenant.experiments[key](throws if missing).useTenantConfig()— merged per-tenant config viagetTenantConfig(registry, tenantKey, environment)from core.
Next.js App Router
Resolve in a Server Component or layout with headers() + requireTenant, then pass tenant into a client shell that mounts TenantProvider. See Next.js — App Router.
Classic SSR
Embed ResolvedTenant in HTML/JSON bootstrap, then on the client use TenantProvider with the same static config import as the server.
See also
@multitenant/core—ResolvedTenant,getTenantConfig,TenantRegistry