Context
Threading a value through five elements that do not care about it is the thing that actually stops people building large trees out of web components.
Alacris implements the W3C community context protocol — the same
context-request event @lit/context uses — so it works between libraries,
not just inside Alacris.
import { createContext, provide, consume } from '@alacris/core/context';
export const ThemeCtx = createContext('theme');Providing
Section titled “Providing”define('app-shell', { setup(_props, host) { const theme = signal('dark'); provide(host, ThemeCtx, theme); // a value, a signal, or any function return html`<slot></slot>`; },});If the value is reactive, subscribers are pushed the new value automatically when it changes.
Consuming
Section titled “Consuming”define('deep-button', { setup(_props, host) { const theme = consume(host, ThemeCtx, 'light'); // a read-only signal return html`<button class=${theme}><slot></slot></button>`; },});consume returns a signal, so it drops straight into a binding and updates on
its own.
import { define, html, signal } from '@alacris/core';import { createContext, provide, consume } from '@alacris/core/context';
// One key object, imported by both the provider and the consumer.const Theme = createContext('demo-theme');
// A leaf, several levels down, that never receives the value as a prop.define('demo-leaf', { styles: ` :host { display: inline-block; padding: .35rem .7rem; border-radius: 6px; border: 1px solid currentColor; font: inherit; font-size: .9rem } :host([data-theme='dark']) { background: #22222a; color: #f2f2f5 } `, setup(_props, host) { const theme = consume(host, Theme, 'light'); // Reflect it so the host can style itself. return html`<span ref=${() => queueMicrotask(() => host.setAttribute('data-theme', theme()))} >theme is ${theme}</span>`; },});
define('demo-middle', { styles: `:host { display: block; padding-left: 1rem; border-left: 2px solid currentColor; opacity: .9 }`, setup: () => html`<p style="margin:.25rem 0">a component that knows nothing about themes</p> <demo-leaf></demo-leaf>`,});
define('demo-provider', { styles: ` :host { display: grid; gap: .6rem; font: inherit } button { font: inherit; padding: .3rem .7rem; border-radius: 6px; cursor: pointer; border: 1px solid currentColor; background: transparent; color: inherit; justify-self: start } `, setup(_props, host) { const theme = signal('light'); // Serve it to every descendant, across shadow boundaries. provide(host, Theme, theme); return html` <button @click=${() => theme(theme() === 'light' ? 'dark' : 'light')}> toggle theme (currently ${theme}) </button> <demo-middle></demo-middle>`; },});The middle component knows nothing about themes. It does not receive one, it does not forward one, and it never re-renders when the theme changes — only the leaf does.
How it resolves
Section titled “How it resolves”The request is a composed, bubbling event, which gives you the semantics you would expect from a scope chain:
- It crosses shadow boundaries, so nesting depth is irrelevant.
- The nearest provider wins; an inner provider shadows an outer one.
- A consumer created before its provider exists holds the fallback and updates as soon as one appears — so script order does not matter.
consume(host, ThemeCtx, 'light');// ^^^^^^^ used until a provider answersCross-library interoperability
Section titled “Cross-library interoperability”Because it is the shared protocol rather than a private mechanism, a Alacris element can consume context provided by a Lit element, and a Lit element can consume one provided by Alacris. Both directions are covered by the test suite.
That matters if you are adopting Alacris incrementally inside an existing component library, or shipping components into an app you do not control.
Cleaning up
Section titled “Cleaning up”provide returns a function that stops providing. Inside a component, prefer
provideTo, which ties it to the enclosing scope so it is released when the
element is removed:
import { provideTo } from '@alacris/core/context';
setup(_props, host) { provideTo(host, ThemeCtx, theme); // released automatically}When not to use it
Section titled “When not to use it”Context is for values that are genuinely ambient — theme, locale, a router, a service client, the current user. It is not a general state container.
The natural combination:
export const StoreCtx = createContext('app-store');
// once, at the topprovideTo(host, StoreCtx, store({ user: null, items: [] }));
// anywhere belowconst state = consume(host, StoreCtx)();state.items.push({ id: 1 }); // path-level updates, from any depth- State that scales — what to put in the context
- Using it from a framework