Cookbook
Structural Patterns
Core architectural building blocks enable extensions to bootstrap, render surfaces, manage state across UI slots, and respond to loading and context changes. Understanding how these patterns work together helps you structure reliable, maintainable extensions.
Bootstrap — Entry Point
Set up your extension entry point in src/index.tsx. The createExtension factory bootstraps the sandboxed runtime and registers all surfaces with the framework.
import { createExtension } from '@stackable-labs/sdk-extension-react'
import { Header } from './surfaces/Header'
import { Content } from './surfaces/Content'
import { Footer } from './surfaces/Footer'
const Extension = () => (
<>
<Header />
<Content />
<Footer />
</>
)
// NOTE: extensionId is optional — used when connected to a registered extension
createExtension(() => <Extension />, { extensionId: 'my-extension' })
Surfaces — Declaring UI Slots
Each surface renders into a specific layout slot in the embedding application. The id prop must match a target declared in your manifest.json.
Header
import { Surface, ui } from '@stackable-labs/sdk-extension-react'
export function Header() {
return (
<Surface id="slot.header">
<ui.Text className="text-sm font-medium">Header content</ui.Text>
</Surface>
)
}
Content
import { Surface, ui } from '@stackable-labs/sdk-extension-react'
export function Content() {
return (
<Surface id="slot.content">
<ui.Card>
<ui.CardContent>
<ui.Text>Extension content</ui.Text>
</ui.CardContent>
</ui.Card>
</Surface>
)
}
Footer
import { Surface, ui } from '@stackable-labs/sdk-extension-react'
export function Footer() {
return (
<Surface id="slot.footer">
<ui.Text className="text-xs">Powered by My Extension</ui.Text>
</Surface>
)
}
Footer Links
import { Surface, ui } from '@stackable-labs/sdk-extension-react'
export function FooterLinks() {
return (
<Surface id="slot.footer-links">
<ui.FooterLink href="https://example.com">My Extension</ui.FooterLink>
</Surface>
)
}
Store — Cross-Surface State & Navigation
Extensions use createStore for shared state across surfaces. The store replaces traditional URL routing — there are no URLs inside the sandbox. Use discriminated unions for view state to get exhaustive type checking.
import { createStore, useStore, Surface, ui } from '@stackable-labs/sdk-extension-react'
// store.ts
type ViewState = { type: 'list' } | { type: 'detail'; id: string }
interface AppState {
viewState: ViewState
}
export const appStore = createStore<AppState>({
viewState: { type: 'list' },
})
// Content.tsx
export function Content(): React.ReactElement {
const viewState = useStore(appStore, (s) => s.viewState)
const goToDetail = (id: string) => appStore.set({ viewState: { type: 'detail', id } })
const goBack = () => appStore.set({ viewState: { type: 'list' } })
return (
<Surface id="slot.content">
{(viewState.type === 'list') && (
<ui.Button onClick={() => goToDetail('abc123')}>View Detail</ui.Button>
)}
{(viewState.type === 'detail') && (
<ui.Stack direction="column" gap="2">
<ui.Text>Viewing: {viewState.id}</ui.Text>
<ui.Button onClick={goBack}>Back</ui.Button>
</ui.Stack>
)}
</Surface>
)
}
Loading States & Guards
Handle loading states and missing context gracefully. Always show a skeleton while context loads, and guard against missing data before rendering.
import { useContextData, Surface, ui } from '@stackable-labs/sdk-extension-react'
export function Content(): React.ReactElement {
const { loading, customerId } = useContextData()
if (loading) {
return (
<Surface id="slot.content">
<ui.Skeleton />
</Surface>
)
}
if (!customerId) {
return (
<Surface id="slot.content">
<ui.Stack direction="column" gap="1" className="p-4 items-center">
<ui.Icon name="user" size="sm" />
<ui.Text className="text-xs text-muted-foreground">No customer selected</ui.Text>
</ui.Stack>
</Surface>
)
}
return (
<Surface id="slot.content">
<ui.Text className="text-sm">Customer: {customerId}</ui.Text>
</Surface>
)
}
Surface Context
Read host-pushed context for a specific surface using the lower-level useSurfaceContext hook.
import { useSurfaceContext, Surface, ui } from '@stackable-labs/sdk-extension-react'
import type { ContextData } from '@stackable-labs/sdk-extension-contracts'
export function Header(): React.ReactElement {
// Lower-level hook — reads host-pushed context for this specific surface
const context = useSurfaceContext() as ContextData
return (
<Surface id="slot.header">
<ui.Text className="text-xs">{context.customerId ?? 'No customer'}</ui.Text>
</Surface>
)
}