packages/component/docs/context.md
Context enables components to communicate without direct prop passing.
Use handle.context.set() to provide values and handle.context.get() to consume them:
function ThemeProvider(handle: Handle<{ theme: 'light' | 'dark' }>) {
let theme: 'light' | 'dark' = 'light'
handle.context.set({ theme })
return (props: { children: RemixNode }) => (
<div>
<button
mix={[
on('click', () => {
theme = theme === 'light' ? 'dark' : 'light'
handle.context.set({ theme })
handle.update()
}),
]}
>
Toggle Theme
</button>
{props.children}
</div>
)
}
function ThemedContent(handle: Handle) {
let { theme } = handle.context.get(ThemeProvider)
return () => (
<div mix={[css({ backgroundColor: theme === 'dark' ? '#000' : '#fff' })]}>
Current theme: {theme}
</div>
)
}
Important: handle.context.set() does not cause any updates—it simply stores a value. If you want the component tree to update when context changes, you must call handle.update() after setting the context (as shown above).
For better performance, use TypedEventTarget to avoid updating the entire subtree. This allows descendants to subscribe to specific changes rather than re-rendering on every parent update:
import { TypedEventTarget } from 'remix/component'
class Theme extends TypedEventTarget<{ change: Event }> {
#value: 'light' | 'dark' = 'light'
get value() {
return this.#value
}
setValue(value: 'light' | 'dark') {
this.#value = value
this.dispatchEvent(new Event('change'))
}
}
function ThemeProvider(handle: Handle<Theme>) {
let theme = new Theme()
handle.context.set(theme)
return (props: { children: RemixNode }) => (
<div>
<button
mix={[
on('click', () => {
// No update needed - consumers subscribe to changes
theme.setValue(theme.value === 'light' ? 'dark' : 'light')
}),
]}
>
Toggle Theme
</button>
{props.children}
</div>
)
}
function ThemedContent(handle: Handle) {
let theme = handle.context.get(ThemeProvider)
// Subscribe to granular updates
addEventListeners(theme, handle.signal, {
change() {
handle.update()
},
})
return () => (
<div mix={[css({ backgroundColor: theme.value === 'dark' ? '#000' : '#fff' })]}>
Current theme: {theme.value}
</div>
)
}
Benefits of this pattern:
handle.update() when context changesTypedEventTarget ensures event handlers receive the correct event typesProvide multiple related values through context:
class AppContext extends TypedEventTarget<{ userChange: Event; settingsChange: Event }> {
#user: User | null = null
#settings: Settings = defaultSettings
get user() {
return this.#user
}
get settings() {
return this.#settings
}
setUser(user: User | null) {
this.#user = user
this.dispatchEvent(new Event('userChange'))
}
setSettings(settings: Settings) {
this.#settings = settings
this.dispatchEvent(new Event('settingsChange'))
}
}
function AppProvider(handle: Handle<AppContext>) {
let context = new AppContext()
handle.context.set(context)
return (props: { children: RemixNode }) => props.children
}
// Components can subscribe to only the events they care about
function UserDisplay(handle: Handle) {
let context = handle.context.get(AppProvider)
addEventListeners(context, handle.signal, {
userChange() {
handle.update()
},
})
return () => <div>{context.user?.name ?? 'Not logged in'}</div>
}
handle.context referenceaddEventListeners() for subscribing to EventTargets