Context shares state with deeply nested children without prop drilling. It is the foundation of compound components (Dialog, Accordion, Tabs).
import { createContext, useContext } from '@barefootjs/client'#`createContext`
Creates a new context with an optional default value.
const MyContext = createContext<T>(defaultValue?: T)Type:
type Context<T> = {
readonly id: symbol
readonly defaultValue: T | undefined
readonly Provider: (props: { value: T; children?: unknown }) => unknown
}#`Context.Provider`
Provides a value to all descendants. Components inside the provider tree read it with useContext.
<MyContext.Provider value={someValue}>
{props.children}
</MyContext.Provider>The compiler transforms this into a provideContext() call. The value is set synchronously before children initialize.
#`useContext`
Reads the current value from a context.
const value = useContext(MyContext)Behavior:
- If a
Providerancestor exists, returns the provided value - If no
Providerexists and a default value was passed tocreateContext, returns the default - Otherwise returns
undefined— guard with optional chaining (store?.value)
#Basic Example
"use client"
import { createContext, useContext } from '@barefootjs/client'
// 1. Create the context
const ThemeContext = createContext<'light' | 'dark'>('light')
// 2. Provider component
export function ThemeProvider(props: { theme: 'light' | 'dark'; children?: Child }) {
return (
<ThemeContext.Provider value={props.theme}>
{props.children}
</ThemeContext.Provider>
)
}
// 3. Consumer component
export function ThemedButton(props: { children?: Child }) {
const handleMount = (el: HTMLButtonElement) => {
const theme = useContext(ThemeContext)
el.className = theme === 'dark' ? 'btn-dark' : 'btn-light'
}
return <button ref={handleMount}>{props.children}</button>
}// Usage
<ThemeProvider theme="dark">
<ThemedButton>Click me</ThemedButton> {/* Gets dark styling */}
</ThemeProvider>#Compound Components
A group of related components sharing internal state. The root provides state; sub-components consume it.
#Example: Accordion
"use client"
import { createSignal, createContext, useContext, createEffect } from '@barefootjs/client'
// Context type
interface AccordionContextValue {
activeItem: () => string | null
toggle: (id: string) => void
}
// Create context
const AccordionContext = createContext<AccordionContextValue>()
// Root component — provides state
function Accordion(props: { children?: Child }) {
const [activeItem, setActiveItem] = createSignal<string | null>(null)
const toggle = (id: string) => {
setActiveItem(prev => prev === id ? null : id)
}
return (
<AccordionContext.Provider value={{ activeItem, toggle }}>
<div data-slot="accordion">{props.children}</div>
</AccordionContext.Provider>
)
}
// Trigger — toggles the active item
function AccordionTrigger(props: { itemId: string; children?: Child }) {
const handleMount = (el: HTMLButtonElement) => {
const ctx = useContext(AccordionContext)
el.addEventListener('click', () => {
ctx.toggle(props.itemId)
})
createEffect(() => {
const isOpen = ctx.activeItem() === props.itemId
el.setAttribute('aria-expanded', String(isOpen))
})
}
return <button ref={handleMount}>{props.children}</button>
}
// Content — shows/hides based on active item
function AccordionContent(props: { itemId: string; children?: Child }) {
const handleMount = (el: HTMLElement) => {
const ctx = useContext(AccordionContext)
createEffect(() => {
const isOpen = ctx.activeItem() === props.itemId
el.hidden = !isOpen
})
}
return <div ref={handleMount}>{props.children}</div>
}Usage:
<Accordion>
<AccordionTrigger itemId="faq-1">What is BarefootJS?</AccordionTrigger>
<AccordionContent itemId="faq-1">
<p>A JSX-to-template compiler with signal-based reactivity.</p>
</AccordionContent>
<AccordionTrigger itemId="faq-2">How does hydration work?</AccordionTrigger>
<AccordionContent itemId="faq-2">
<p>Marker-driven: bf-* attributes tell the client JS where to attach.</p>
</AccordionContent>
</Accordion>#Reactive Context Values
Context values can contain signal getters. Effects that read them re-run when the signal changes:
// Provider passes signal getter
<AccordionContext.Provider value={{ activeItem, toggle }}>// Consumer reads inside createEffect — reactive
const ctx = useContext(AccordionContext)
createEffect(() => {
const isOpen = ctx.activeItem() === props.itemId // Tracks activeItem signal
el.hidden = !isOpen
})ctx.activeItem() inside the effect subscribes to activeItem. When it changes, only affected effects re-run.
#Context Without a Default
Without a default value, useContext throws if no Provider ancestor exists. Recommended for compound components:
const DialogContext = createContext<DialogContextValue>()
// If DialogTrigger is used outside a Dialog, useContext throws
function DialogTrigger(props: { children?: Child }) {
const handleMount = (el: HTMLElement) => {
const ctx = useContext(DialogContext) // Throws if no Dialog ancestor
// ...
}
return <button ref={handleMount}>{props.children}</button>
}This catches composition errors early — the error identifies the missing provider.
#Context With a Default
With a default, useContext always succeeds:
const ThemeContext = createContext<'light' | 'dark'>('light')
// Works even without a ThemeProvider ancestor — returns 'light'
const theme = useContext(ThemeContext)Use for optional contexts with a sensible fallback.