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.

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>
  )
}
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>
  )
}
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>
  )
}

Auto-generated from Stackable Extension SDK. Questions/Issues? developers@stackablelabs.com

Previous
Recipes
Structural Patterns | Stackable Labs :. Dev Documentation