Building Extensions
Store & Navigation
Extensions use createStore for cross-surface state management. The store is shared across all surfaces (header, content, footer), enabling coordinated UI updates from a single state source.
Creating a Store
Define your store in src/store.ts:
import { createStore } from '@stackable-labs/sdk-extension-react'
type ViewState = { type: 'list' } | { type: 'detail'; id: string }
interface AppState {
viewState: ViewState
}
export const appStore = createStore<AppState>({
viewState: { type: 'list' },
})
Reading State in Surfaces
Use useStore with a selector to subscribe to specific slices of state:
import { useStore } from '@stackable-labs/sdk-extension-react'
import { appStore } from '../store'
export const Content = () => {
const viewState = useStore(appStore, (s) => s.viewState)
if (viewState.type === 'list') {
return <ListView onSelect={(id) => appStore.set({ viewState: { type: 'detail', id } })} />
}
return <DetailView id={viewState.id} onBack={() => appStore.set({ viewState: { type: 'list' } })} />
}
Store-Based Navigation
Use discriminated unions for view state instead of string constants or URL routing. The store replaces traditional routing — there are no URLs inside the sandbox.
View State Pattern
// Define all possible views as a discriminated union
type ViewState =
| { type: 'list' }
| { type: 'detail'; id: string }
| { type: 'edit'; id: string }
| { type: 'create' }
// TypeScript narrows the type based on the discriminant
switch (viewState.type) {
case 'list': return <ListView />
case 'detail': return <DetailView id={viewState.id} />
case 'edit': return <EditView id={viewState.id} />
case 'create': return <CreateView />
}
Cross-Surface Coordination
The header can show context based on what the content surface displays:
// Header surface — shows back button when on detail view
export const Header = () => {
const viewState = useStore(appStore, (s) => s.viewState)
return (
<Surface id="slot.header">
<ui.Inline>
{viewState.type !== 'list' && (
<ui.Button
variant="ghost"
onClick={() => appStore.set({ viewState: { type: 'list' } })}
>
Back
</ui.Button>
)}
<ui.Heading level={3}>
{viewState.type === 'list' ? 'All Items' : 'Item Detail'}
</ui.Heading>
</ui.Inline>
</Surface>
)
}
Selector Performance
Use narrow selectors to minimize re-renders. Each useStore call only triggers a re-render when its selected value changes:
// Good — component only re-renders when viewState changes
const viewState = useStore(appStore, (s) => s.viewState)
// Bad — component re-renders on ANY state change
const state = useStore(appStore, (s) => s)
Best Practices
- One store per extension — define in
src/store.ts, import everywhere - Discriminated unions for views — TypeScript can narrow the type and catch missing cases
- Narrow selectors — select only what the component needs
- Use
appStore.set()— update state directly viaappStore.set({ viewState: ... })from any component - No URL routing — the extension runs in a sandboxed iframe with no URL bar