Context shares state with nested components without prop drilling; compound components (Dialog, Accordion, Tabs) are built on it.
const Ctx = createContext<T>(defaultValue?: T)
<Ctx.Provider value={value}>{children}</Ctx.Provider>
const value = useContext(Ctx) // T | undefinedcreateContext(defaultValue?)creates a context (both functions come from@barefootjs/client).Providermakesvalueavailable to every descendant; it compiles to aprovideContext()call that runs synchronously before the children initialize.useContextreturns the nearest provided value, else the default passed tocreateContext, elseundefined. It never throws, so guard an optional context with?..
A context value can hold signal getters; reading one inside createEffect makes the effect reactive.
#Example: Accordion
The root provides the state; sub-components read it in their ref callbacks, once the DOM exists to walk up to the provider.
"use client"
import { createSignal, createContext, useContext, createEffect } from '@barefootjs/client'
interface AccordionContextValue {
activeItem: () => string | null
toggle: (id: string) => void
}
const AccordionContext = createContext<AccordionContextValue>()
export 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>
)
}
export function AccordionTrigger(props: { itemId: string; children?: Child }) {
const handleMount = (el: HTMLButtonElement) => {
const ctx = useContext(AccordionContext)
el.addEventListener('click', () => ctx.toggle(props.itemId))
createEffect(() => {
el.setAttribute('aria-expanded', String(ctx.activeItem() === props.itemId))
})
}
// Nothing is open at SSR, so render the initial state; the effect keeps it in sync.
return <button aria-expanded="false" ref={handleMount}>{props.children}</button>
}
export function AccordionContent(props: { itemId: string; children?: Child }) {
const handleMount = (el: HTMLElement) => {
const ctx = useContext(AccordionContext)
createEffect(() => {
el.hidden = ctx.activeItem() !== props.itemId
})
}
return <div ref={handleMount}>{props.children}</div>
}#Context is same-file only
createContext() and every component that provides or reads it must live in the same file. Importing a context from another file compiles, but the consumer never finds the provider's value.
#Sharing state across files
Each .tsx file compiles to its own client bundle, and a context's identity is a Symbol created when that bundle loads, so a consumer in one file never matches a provider in another. Module-level state is per bundle the same way. Three ways around it:
| Option | When |
|---|---|
| Put the components in one file | Tightly coupled, always used together |
| Dispatch a custom DOM event | Separate files sharing a DOM ancestor |
| Let the server pass props | State originates on the server (database, session, URL) |
A custom event needs no shared import; the receiver updates its own signal:
"use client"
import { createSignal, onMount, onCleanup } from '@barefootjs/client'
export function Elapsed() {
const [elapsedMs, setElapsedMs] = createSignal(0)
onMount(() => {
const onTimeUpdate = (e: Event) => setElapsedMs((e as CustomEvent).detail.elapsedMs)
document.addEventListener('playback:timeupdate', onTimeUpdate)
onCleanup(() => document.removeEventListener('playback:timeupdate', onTimeUpdate))
})
return <span>{Math.floor(elapsedMs() / 1000)}s</span>
}The sender calls el.dispatchEvent(new CustomEvent('playback:timeupdate', { bubbles: true, detail: { elapsedMs } })). Server-passed props give each component its initial value only; later updates still go through events.