windo/Concepts/Contexts
Contexts

Contexts

Real components rarely render in a vacuum — they read a theme, a locale, a feature flag, a current user. A context is windo's way of supplying that surrounding world: a named bundle of live values, the UI to edit them, and an optional React provider to wrap the components that need it. Declare it once in your config and every windo can read from it; opt a windo in and the provider mounts around it.

What a context is for

A context answers a single question: what does this component need from outside itself? That splits cleanly into two jobs, and a context can do either or both.

  • Ambient values — light or dark, en or fr, a flag that's on or off. These are values your component (or your other windo functions) read to decide how to render. They come with editable controls in the chrome, so you can flip them live.
  • A provider — when the component reaches for a value through React context (useTheme(), an i18n provider, a query client), windo needs to actually mount that provider in the iframe. The context carries that wrapper, and components opt into it.

You define a context with defineContext, register it under a name in your config, and from then on it's part of the environment every windo runs inside.

Defining a context

defineContext takes one object. Every field is optional — what you supply decides which of the two jobs the context does.

TSXwindo.config.ts
import { defineContext } from '@westopp/windo'
import { ThemeProvider } from './app/ThemeProvider'

const theme = defineContext({
label: 'Theme',
description: 'Colour scheme the component renders under.',
controls: {
  mode: { type: 'enum', options: ['light', 'dark'], default: 'light' },
  density: { type: 'enum', options: ['cosy', 'compact'], default: 'cosy' },
},
provider: ({ children, values }) => (
  <ThemeProvider mode={values.mode} density={values.density}>
    {children}
  </ThemeProvider>
),
resolve: (values) => ({ ...values, isDark: values.mode === 'dark' }),
})

Reading the fields in order:

  • label and description name the context in the chrome's Context tab — label is the heading, description the supporting line. Both are optional; the registration key is used when label is absent.
  • controls is a map of control key to spec. Each spec declares a type (enum, boolean, string, or number), a default, and the metadata to render its toggle — options for an enum, min / max / step for a number, an optional label. The control map's defaults are what the context starts with; the Context tab edits them from there.
  • provider is a React component windo mounts inside the iframe. It receives children (the component being previewed), the live values from the controls, and the render ctx. Use it to wrap the component in whatever real provider it expects.
  • resolve shapes what consumers actually read. By default a context exposes its raw control values; resolve lets you derive something richer — combine values, compute a flag, return a whole client object — from (values, ctx). Its return is what lands on ctx.contexts[name].

Registering contexts

A context isn't live until your config knows about it. The contexts map in defineWindoConfig is the registry — each key is the name the context is addressed by everywhere else: in a windo's uses, in ctx.contexts, and as the section heading in the chrome when no label is set.

TSXwindo.config.ts
import { defineContext, defineWindoConfig } from '@westopp/windo'

const result = defineWindoConfig({
title: 'Acme UI',
groups: [
  { name: 'Actions', slug: 'actions' },
  { name: 'Data display', slug: 'data-display' },
],
contexts: {
  theme: defineContext({
    label: 'Theme',
    controls: { mode: { type: 'enum', options: ['light', 'dark'], default: 'light' } },
    provider: ({ children, values }) => <ThemeProvider mode={values.mode}>{children}</ThemeProvider>,
  }),
  locale: defineContext({
    label: 'Locale',
    controls: { lang: { type: 'enum', options: ['en', 'fr', 'de'], default: 'en' } },
  }),
},
})

export const windo = result.windo
export const config = result.config

Here theme carries a provider and locale is ambient-only. Both appear in the Context tab the moment they're registered — you don't opt a windo in to see a context's controls, only to mount its provider.

Ambient values vs. opting in

This is the distinction that trips people up, so it's worth stating plainly. Registering a context makes its values ambient — every windo can read them off ctx.contexts, no opt-in required. What requires opting in is the context's provider: windo only mounts a wrapper around windos that explicitly ask for it.

A component reads an ambient value through ctx.contexts[name]:

TSXbanner.windo.tsx
export default windo<BannerProps>(() => ({
title: 'Banner',
group: 'data-display',
defaultProps: { message: 'Heads up' },
// No `uses` — but the locale values are still readable.
component: (props, ctx) => {
  const { lang } = ctx.contexts.locale as { lang: string }
  return <Banner {...props} lang={lang} />
},
}))

A component opts into a provider with the uses field — a list of context names. windo mounts each named context's provider around this windo (and only this windo), in the order listed.

TSXcard.windo.tsx
export default windo<CardProps>(() => ({
title: 'Card',
group: 'data-display',
// The Theme provider wraps this windo, so `useTheme()` inside Card works.
uses: ['theme'],
defaultProps: { title: 'Atlas redesign' },
component: (props) => <Card {...props} />,
}))

Editing context values live

Each registered context that contributes controls gets a section in the chrome's Context tab. The controls render from the metadata you gave each spec — an enum becomes a select of its options, a boolean a switch, a number a stepper bounded by min / max / step, a string a text field — and they start at each control's default.

Editing a control pushes the new value into the iframe. Both surfaces update at once: the provider re-renders with the fresh values, and ctx.contexts[name] reflects the new resolve output. So flipping theme.mode from light to dark re-runs the ThemeProvider and updates anything reading ctx.contexts.theme.isDark — no reload, no remount of your component tree beyond the provider.

Wrapping a single windo

Contexts are the shared, reusable layer. Sometimes one windo needs a wrapper that nothing else does — a router stub for a link component, a form store for a single field demo, a mock query client scoped to one example. Reaching for a full context is overkill.

The per-windo providers field is the local escape hatch: a single React component that wraps just this windo, in addition to anything uses brings in. It receives children and the render ctx, so it can read live state and environment.

TSXnav-link.windo.tsx
import { MemoryRouter } from 'react-router-dom'

export default windo<NavLinkProps>(() => ({
title: 'Nav link',
group: 'navigation',
// Shared theme provider for everyone…
uses: ['theme'],
// …plus a router that only this windo needs.
providers: ({ children }) => <MemoryRouter>{children}</MemoryRouter>,
defaultProps: { to: '/projects', label: 'Projects' },
component: (props) => <NavLink {...props} />,
}))

providers and uses compose: the uses providers mount first, then the local providers wraps inside them, then your component. Use a registered context when more than one windo shares a wrapper; reach for providers when the wrapper belongs to exactly one example.

Where next

  • Variants — pre-baked prop sets that render in the gallery and apply on click.
  • State & actions — the per-windo state machine and the render-time rule that governs providers too.