Shared state
provide/inject in core, with the shape of Vue and the mechanics of the WICG context protocol.
The functions
| Function | Does |
|---|---|
createContext(description?) | a typed key |
signal(initialValue) | a reactive value |
provideContext(el, key, value) | provide on an element |
provideRootContext(key, create) | provide page-wide, created at most once |
injectContext(el, key) | a promise for the nearest value |
injectContextSync(el, key) | the nearest value, or undefined |
subscribeContext(el, key, cb) | every answer, as providers come and go |
createGroup() | a membership set with a reactive members list |
From inside a component, $provide(), $inject() and $injectSync() are the same three, with the element filled in. The @provide and @inject decorators are field sugar over them.
The model
- The injection key is typed, so strings cannot collide.
- The scope is the subtree, and the nearest provider wins.
- The value is provided as it is. Nothing is wrapped, so the type of the key is the contract from end to end. A reactive value is a provided
Signal. A command surface is a provided object.
class Slider extends Base {
static config = { name: 'Slider' };
api = this.$provide(SliderContext, {
state: signal({ index: 0, total: 0 }), // what changes
goNext: () => {}, // what a control can command
});
}Two ways to ask
| Form | Resolves | When nothing provides |
|---|---|---|
$inject(key) | a promise, awaited in mounted() | it never settles: a missing provider means "not yet". |
$injectSync(key) | the value, synchronously | undefined: the caller falls back or does nothing. |
The pending request of the async form is unmount-scoped. A new mount runs mounted() again and asks again. The @inject field decorator asks once, at construction; a consumer that can wait through several cycles calls $inject() from mounted().
The mechanics
The consumer dispatches a bubbling, module-private js-toolkit:context:request event carrying a key, a callback and a subscription marker. It is deliberately not part of public EVENTS.
The nearest mounted provider answers and stops propagation. provideContext() replays the requests that have no first answer yet, which is what makes mount order irrelevant. injectContext() and $inject() are one-shot.
Duplicate bundles share the basic provider and pending-request state through the context shared-runtime slot at schema revision 2. The optional owner map, weak index and listener flag use the context-subscription slot at revision 1 — and context.ts and the Base graph import none of that optional state.
Scopes
$provide() in a field initializer is instance-scoped: it is never released and dies with the element. A component whose declaration is withdrawn keeps providing until its element goes.
A root provider cannot be disposed and it outlives the instance that asked first, because it is page state. withGroup is not ported.
Read next
Shared state walks the whole pattern, from a coordinator's provided API to a member joining the nearest group.