Cookbook
Structural Patterns
Focused examples for the foundational patterns every extension uses: bootstrapping, surfaces, store-based navigation, and loading states.
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>
)
}