Components
Components are Go values that implement gox.Comp by having a Main() gox.Elem method.
There is no virtual DOM in Doors. A component is static by default: Main() renders once and stays. Re-rendering is explicit and local. Bind and Effect re-render dynamic fragments, subscriptions can update Doors, and direct Door methods replace only the Door region. The rest of the tree is untouched.
Model
Use a struct component when the UI owns state, dependencies, or several methods:
func NewCounter() gox.Comp {
return counter{count: doors.NewSource(0)}
}
type counter struct {
count doors.Source[int]
}
elem (c counter) Main() {
<button
(doors.AClick{
On: func(ctx context.Context, _ doors.RequestPointer) bool {
c.count.Mutate(ctx, func(i int) int { return i + 1 })
return false
},
})>
Add
</button>
~(c.count.Bind(c.countView))
}
elem (c counter) countView(i int) {
<span>~(i)</span>
}
The counter value is created when it is placed in the tree. Main() renders once. The button stays mounted. Only the Bind fragment re-renders when count changes.
Use constructors to keep initialization private. Without NewCounter, callers would need to know that count must be initialized with doors.NewSource(0).
Rendering
Three tools cover most dynamic component rendering:
| Tool | Use |
|---|---|
Bind |
One source or beam drives one fragment. |
Effect |
One small container reads multiple related values inline. |
Door |
A handler, subscription, or background task explicitly replaces a region. |
Bind
Use Bind when the relationship is direct: this value renders this fragment.
<>
~(c.count.Bind(c.countView))
</>
The callback receives the value and Doors handles the subscription and Door updates.
Effect
Use Effect when one small fragment needs to read several related values together:
<>
~>(new(doors.Door)) <section>
~~
days, _ := d.days.Effect(ctx)
units, ok := d.units.Effect(ctx)
~~
~(if ok {
~(WeatherChart(days, units))
})
</section>
</>
The same pattern can be written in expression style when it is clearer to return one value:
<>
~>(new(doors.Door)) ~({
days, _ := d.days.Effect(ctx)
units, ok := d.units.Effect(ctx)
if !ok {
return nil
}
return <section>
~(WeatherChart(days, units))
</section>
})
</>
Keep Effect boundaries small. The whole container re-renders when any value read with Effect changes.
It is enough to check the last ok. Effect fails only when the context is already canceled.
Door
Use a Door field when a component needs explicit updates from handlers, subscriptions, or background work:
type panel struct {
body doors.Door
}
elem (p *panel) Main() {
<button
(doors.AClick{
On: func(ctx context.Context, _ doors.RequestPointer) bool {
p.body.Inner(ctx, "Updated")
return false
},
})>
Update
</button>
~>(p.body) <div>Initial</div>
}
See Door for the low-level API.
Fields
Keep component fields tied to ownership and reuse. Use an elem function for markup that does not need state or methods. Use a struct component when a value must persist after Main() returns: state handles, Door handles, dependencies called by handlers, or configuration shared by methods.
Model data with concrete types. If a component needs a content slot, make that slot explicit and document what callers may pass.
When a repeated block has one template shape plus data, make one component per item:
type ContentBlock struct {
Title string
Description string
Items []string
}
elem (b ContentBlock) Main() {
<section>
<h2>~(b.Title)</h2>
<p>~(b.Description)</p>
<ul>
~(for _, item := range b.Items {
<li>~(item)</li>
})
</ul>
</section>
}
For many items, prefer an explicit for loop that renders one component per item. It keeps wrappers, classes, IDs, and per-item attributes local to each item.
State
Be explicit about who owns each source or beam:
- Local state: create it in the constructor and store it on the component.
- Shared state: accept a
SourceorBeamfrom the parent. - Derived state: derive it from a parent source in the constructor.
Do not add a doors.Source or doors.Beam field and leave initialization implicit. A component with source fields should have a constructor or parent wiring that makes those fields non-nil before Bind, Effect, Sub, Update, or Mutate uses them.
func LocationSelector(apply func(context.Context, Place)) gox.Comp {
selected := doors.NewSource(Place{})
return locationSelector{
selected: selected,
apply: apply,
}
}
Use derived sources and beams to keep updates narrow. One parent route or settings source can feed multiple small fragments without making the whole component re-render.
Lifecycle
When a dynamic parent unmounts, Doors cancels everything inside it:
Bind,Effect, andSubsubscriptions- hook handlers
- mounted Doors
- scoped background work started with
doors.Go(...)
Components can be reused across route switches when the same Go value is passed to .Comp(...). Wrap construction in an element when you want fresh state each time the route enters:
<>
~(path.Route(
doors.RouteMatch(func(p Path) bool { return p.Section == SectionSelector }).
Comp(<>
~(LocationSelector(applyPlace))
</>),
doors.RouteDefaultComp[Path](Dashboard{}),
))
</>
Without the wrapper, the component value can be reused and its fields can keep their previous state.
Rules
- Components are static unless you put dynamic fragments inside them.
- Prefer
Bindfor direct value-to-fragment rendering. - Prefer small
Effectcontainers for related values read together. - Use
Doorwhen code must explicitly replace a region. - Initialize every
Source,Beam, andDoorfield deliberately. - Keep state ownership clear: local, shared, or derived.
- Wrap component construction in
<>...</>when route switches need fresh component state.