Context
Every mounted component exposes a public context through instance.context. The public context is the controlled API used by component code: methods, computed functions, watchers, lifecycle hooks, and function templates.
Internally, Udodi also maintains an internal context used by the runtime and template VM. The internal context is not the public component API.
The two surfaces are connected by a Proxy membrane:
Component
│
┌─────────────┴─────────────┐
│ │
▼ ▼
Internal context Public context membrane
│ │
│ ├── state
│ ├── computed
│ ├── methods
│ ├── props
│ ├── refs
│ ├── ud
│ ├── name
│ ├── cleanup
│ │
▼ ▼
Runtime / VM / bindDOM Component application codecreateComponent() creates both contexts. mount() uses the internal context for runtime DOM binding and exposes the public context as the mounted instance's context.
Public Context
The public context is the component-facing API.
When a component is mounted:
const instance = render(Component(), "#app");
instance.context;instance.context is the public context membrane.
This means component code and external code holding a mounted instance interact with the same controlled context surface.
instance.context.count = 10;
console.log(instance.context.count);State writes through instance.context follow the same public-context rules as writes made through this inside a method.
Internal Context
The internal context is the runtime-facing context created by createComponent().
It contains the concrete runtime wiring needed by Udodi's VM and DOM binding system, including:
- component name
- standard-library helpers
refs- framework namespace
- reactive state access
- computed accessors
- methods
- props
- runtime lifecycle wiring
The internal context is passed to runtime systems such as bindDOM().
The user's application code does not access the internal context.
| Context | Purpose |
|---|---|
| Public context | Component/application API |
| Internal context | Runtime and VM implementation surface |
How Context Is Exposed
The same public context is made available through several component APIs.
| Location | Context access |
|---|---|
| methods | this |
| computed | First argument ctx |
| watch handlers | this |
onMount | Second argument ctx |
onUnmount | Second argument ctx |
| Function templates | Template ctx |
| Mounted instance | instance.context |
For example:
const Counter = createComponent({
name: "Counter",
state() {
return {
count: 0,
};
},
computed: {
doubled(ctx) {
return ctx.count * 2;
},
},
methods: {
increment() {
this.count++;
},
},
watch: {
countChange: {
deps: ["count"],
handler(newValues, oldValues) {
console.log(this.count);
},
},
},
onMount(root, ctx) {
console.log(ctx.count);
},
onUnmount(root, ctx) {
console.log(ctx.count);
},
template: (ctx) => html`
<button @on="click=increment">
<span @text="count"></span>
/
<span @text="doubled"></span>
</button>
`,
});All of these component-facing APIs operate on the public context.
Mounted Instance Context
render() returns a mounted instance containing:
const instance = render(Component(), "#app");
instance.name;
instance.context;
instance.unmount();The important distinction is:
instance.context
│
▼
public context membrane
│
▼
internal reactive state / component definitionsinstance.context is not a free-form object. Its Proxy traps control what can be read and written.
This means:
instance.context.count = 10;is a valid state write, while:
instance.context.someUnknownKey = true;is rejected.
What the Public Context Exposes
The public context provides the following root-level surfaces:
| Kind | Access |
|---|---|
| Component name | ctx.name |
| State | Read and write registered state keys |
| Computed | Read computed properties |
| Methods | Call methods |
| Props | Read props |
refs | Read DOM references |
ud | Read framework-owned namespace |
cleanup | Register unmount cleanup |
The public membrane controls these values rather than exposing the internal context directly.
Reading from Context
Root property resolution follows the component's registered namespaces.
Conceptually:
context key
│
├── special runtime key
│ ├── refs
│ ├── ud
│ ├── name
│ └── cleanup
│
├── state key
│ └── reactive state store
│
├── computed key
│ └── computed value
│
├── method key
│ └── bound method
│
├── prop key
│ └── prop value / reactive prop
│
└── unknown key
└── undefinedThe exact precedence is implemented by the public context membrane.
Unknown properties are not dynamically appended to the component context.
State
Component state is the normal writable portion of the public context.
const Counter = createComponent({
state() {
return {
count: 0,
label: "Count",
};
},
methods: {
increment() {
this.count++;
},
},
});The state keys are available directly:
this.count;
this.label;and can be written:
this.count = 10;
this.label = "Total";The assignment goes through the component's reactive state store and therefore participates in the component's normal reactivity and interceptor processing.
Shallow state reactivity
Component state is reactive at the top level.
For:
state() {
return {
user: {
label: "Ada",
},
};
}the root key user is tracked as a state property.
Nested objects are not automatically made deeply reactive by the component state layer. Mutating:
this.user.label = "Grace";does not itself constitute a root-state assignment.
When a nested mutation needs to be surfaced to the component's reactivity system, use the appropriate reactivity mechanism, such as touch().
Computed Values
Computed values are exposed as readable properties on the public context.
computed: {
doubled(ctx) {
return ctx.count * 2;
},
},The value is then read as:
this.doubled;or:
ctx.doubled;A computed property is not a writable state key:
this.doubled = 10; // throwsComputed functions receive the public context as their first argument:
computed: {
total(ctx) {
return ctx.price * ctx.quantity;
},
},This keeps computed functions on the same controlled context surface as methods and lifecycle hooks.
Methods
Methods are exposed as callable root properties.
methods: {
increment() {
this.count++;
},
reset() {
this.count = 0;
},
},Inside a method, this is the public context:
this.increment();
this.count++;Methods cannot be replaced through the public context:
this.increment = null; // throwsThe runtime binds methods to the public context so method code does not receive the unrestricted internal context as this.
Props
Props share the component's root namespace with state, computed properties, and methods.
A regular prop is exposed as its value.
const User = createComponent({
methods: {
printName() {
console.log(this.userName);
},
},
});Reactive props can retain a live connection when created with bindProp().
Child({
data: bindProp(() => parent.data),
});Without bindProp(), a normal prop is a value passed to the child rather than an automatically connected reactive reference.
Props are readable through the public context but are not writable through normal public-context assignment. Public root assignment is reserved for registered state keys.
See Props.
refs
refs contains DOM references registered by the template runtime.
<input @ref="query" />The reference is then available through:
this.refs.query;For example:
methods: {
focusQuery() {
this.refs.query?.focus();
},
},Use refs for imperative DOM operations such as:
- focus
- scroll
- measurement
- direct browser API access
Prefer declarative bindings where possible.
refs is a runtime-managed object and is not part of the component's state namespace.
ud
ud is Udodi's framework-owned namespace.
It is used internally by framework features and directives. For example, framework-managed form state can be exposed under the namespace:
this.ud.forms;The public context exposes ud through a readonly membrane.
Therefore:
this.ud = {};and mutations through the readonly surface are not valid application operations.
Treat ud as runtime-owned.
Application state should live in:
- component state
- props
- the Udodi Store
rather than inside ud.
Standard Library Helpers
The internal context includes Udodi's standard-library helpers.
These helpers can be resolved by the runtime as context names when they are not shadowed by component-defined keys.
Common helpers include:
| Helper | Purpose |
|---|---|
trim(value) | Trim a string; nullish values become "" |
get(collection, key) | Read from supported collections |
upper | Upper-case conversion |
lower | Lower-case conversion |
capitalise(value) | Capitalize words |
size(collection) | Get the size of a supported collection |
negate / n | Boolean negation |
They are intended for lightweight formatting and value manipulation, particularly in template resolution.
Component root keys take precedence where they overlap with available helper names.
cleanup
Mounted components expose a cleanup registrar through the public context.
onMount(root, ctx) {
const onResize = () => {
// ...
};
window.addEventListener("resize", onResize);
ctx.cleanup(() => {
window.removeEventListener("resize", onResize);
});
},The cleanup hook is injected by the mounting system after the component is mounted.
Cleanup callbacks run during unmount along with the component's other runtime cleanup.
cleanup is therefore lifecycle infrastructure rather than ordinary component state.
Before mount, the cleanup hook has not yet been injected.
See Lifecycle.
touch() and Context
touch() can accept a component's public context.
import { touch } from "udodi";
methods: {
rename(next) {
this.user.label = next;
touch(this, "user");
},
},This allows a nested mutation to explicitly notify the reactive system that the corresponding root state key should be considered changed.
The public context can therefore be used directly with touch(); application code does not need to access the component's internal runtime context.
See Using touch().
Watchers
Watchers declare dependencies on root state keys.
watch: {
userChanged: {
deps: ["user"],
handler(newValues, oldValues) {
console.log(this.user);
},
},
},Watcher handlers use the public context as this.
The watcher system tracks declared dependencies and invokes the handler when the relevant values change.
Because component state is shallow at the root, nested mutations are not independently represented as root dependency changes. Use touch() when a nested mutation needs to trigger the corresponding root-level dependency.
See Watchers.
Lifecycle Hooks
Lifecycle hooks receive the public context.
onMount
onMount(root, ctx) {
console.log(ctx.name);
console.log(ctx.count);
ctx.cleanup(() => {
// teardown
});
},onUnmount
onUnmount(root, ctx) {
console.log(ctx.name);
},The runtime invokes the component lifecycle machinery and supplies the public context to the user-defined hooks.
See Lifecycle.
Templates and Context
Function templates receive the public context:
template: (ctx) => html`
<button @on="click=increment">
<span @text="count"></span>
<span @text="doubled"></span>
</button>
`,This means the function template can access the same component-facing properties as methods and lifecycle hooks:
ctx.count;
ctx.doubled;
ctx.increment;
ctx.refs;
ctx.name;The template runtime itself operates against the internal context when binding and evaluating the compiled template.
This distinction lets the template system access runtime metadata and wiring without making that implementation surface the public component API.
Public Context Writes
The public context has a restrictive set operation.
Valid state writes
this.count = 1;
this.count++;
this.user = nextUser;These are valid when count and user are registered state keys.
Invalid computed writes
this.doubled = 10;Computed values are read-only.
Invalid method writes
this.increment = null;Methods cannot be replaced through the public context.
Invalid prop writes
this.title = "New";If title is a prop rather than state, the public context does not treat it as a writable root state key.
Invalid arbitrary writes
this.extra = true;The public context is not an extensible application object. Unknown root keys cannot be appended.
Invalid reserved writes
this.name = "Other";
this.ud = {};Reserved and framework-owned properties cannot be overwritten.
Reserved Keywords
The following names are reserved by the component model:
name
state
computed
interceptors
methods
watch
template
onMount
onUnmount
refs
style
udThese names cannot be registered as component state, computed properties, methods, or props.
For example:
createComponent({
state() {
return {
name: "Invalid",
};
},
});throws a collision error.
The same rule prevents component definitions from accidentally replacing framework-level component properties.
Namespace Collision Model
The component root namespace is shared by:
state | computed | methods | propsA root key must have exactly one owner.
This is invalid:
createComponent({
state() {
return {
title: "Hello",
};
},
computed: {
title(ctx) {
return ctx.title;
},
},
});The component definition throws a namespace-collision error.
Props are also checked against the existing component namespace when an instance is created.
The invariant is:
Every root-level component key has exactly one meaning.
That makes template resolution and component code predictable:
ctx.title;cannot ambiguously mean state in one place and computed data in another.
Runtime Context vs Public Context
The distinction matters most at the DOM/runtime boundary.
mount() uses the internal context when it connects the component to the DOM runtime.
Conceptually:
mount()
│
├── internal context
│ │
│ └── bindDOM / VM
│
└── public context
│
└── instance.contextTherefore:
instance.contextis the public context, while the context supplied internally to runtime DOM binding is the internal context.
Application code does not depend on the internal runtime context.
Context Lifecycle
The context exists as part of the component instance, while some runtime facilities are injected during mounting.
The lifecycle is conceptually:
createComponent()
│
├── create state
├── create internal context
├── create public membrane
├── register computed
├── register methods
├── register watchers
└── return component
│
▼
mount()
│
├── bind runtime DOM using internal context
├── inject cleanup registrar
└── return mounted instance
│
└── instance.context
│
▼
public contextThe public context is therefore the stable component-facing surface, while mount-time facilities such as cleanup become available as the component enters the mounted lifecycle.
Context and Reactivity
The context is not itself the source of reactivity.
The reactive state store is.
The public context provides the controlled access path to that store:
this.count
│
▼
public context membrane
│
▼
state accessor
│
▼
reactive state storeA state assignment therefore participates in the normal component reactivity system:
this.count = 10;For nested state:
this.user.label = "Grace";the component's state model remains shallow. If the nested mutation needs to be surfaced explicitly, use touch() or replace the root value.
For example:
this.user = {
...this.user,
label: "Grace",
};or:
this.user.label = "Grace";
touch(this, "user");Context API Summary
| Property | Read | Write | Notes |
|---|---|---|---|
name | Yes | No | Component name |
| State keys | Yes | Yes | Normal writable context |
| Computed keys | Yes | No | Evaluated computed values |
| Method keys | Yes | No | Callable methods |
| Prop keys | Yes | No | Values or reactive prop bindings |
refs | Yes | Runtime-managed | DOM references |
ud | Yes | No | Readonly framework namespace |
cleanup | Yes, after mount | No | Cleanup registrar |
| Unknown keys | undefined | No | Cannot append to context |
Minimal Example
import { createComponent, html, mount } from "udodi";
const Counter = createComponent({
name: "Counter",
state() {
return {
count: 0,
};
},
computed: {
doubled(ctx) {
return ctx.count * 2;
},
},
methods: {
increment() {
this.count++;
},
},
onMount(root, ctx) {
console.log(ctx.name);
console.log(ctx.count);
console.log(ctx.doubled);
ctx.cleanup(() => {
// teardown
});
},
template: (ctx) => html`
<button @on="click=increment">
<span @text="count"></span>
/
<span @text="doubled"></span>
</button>
`,
});
const instance = render(Counter(), "#app");
console.log(instance.name);
console.log(instance.context.count);
instance.context.count = 10;The important distinction is:
this.count; // inside methods
ctx.count; // computed, lifecycle, function templates
instance.context.count; // mounted instanceall address the public context.
Design Invariants
The component context maintains these invariants:
- The public context is a Proxy membrane.
instance.contextis the public context.- The internal context is used by runtime systems such as the VM and DOM binding.
- State keys are the normal writable root properties.
- Computed values are readable but not writable.
- Methods are callable but not replaceable through the public context.
- Props share the root namespace with state, computed values, and methods.
- Root namespace collisions are rejected.
- Reserved framework names cannot be registered as component keys.
udis framework-owned and readonly through the public context.cleanupbecomes available through the public context after mount injects the cleanup registrar.- Component state is shallowly reactive at the top level.
- Unknown root properties cannot be appended to the public context.
- Component-facing callbacks receive the public context rather than the unrestricted internal context.
- The internal context remains a runtime implementation surface and should not be treated as application API.