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.

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