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.
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.
Markup Encapsulation
A struct component's main job is to own a piece of markup — the tags, classes, and layout — and expose what varies as fields. Callers assemble pages from data-shaped literals; presentation stays in one place.
GoX renders any type, so a slot field is just any: it accepts a string, an element, another component, or a fragment of components.
type Bubble struct {
Content any
Accent bool
}
elem (b Bubble) Main() {
<div class=({
if b.Accent {
return "bubble accent"
}
return "bubble"
})>~(b.Content)</div>
}
The same slot takes plain text or a markup tree:
elem examples() {
~Bubble{Content: "Hello!"}
~Bubble{Accent: true, Content: <>
<h4>Links</h4>
<a href="https://doors.dev">doors.dev</a>
</>}
}
A nil slot renders nothing. Wrap dependent markup in ~(if ...) when it should disappear together with the slot:
type Message struct {
Avatar any
Content any
}
elem (m Message) Main() {
<li class="message">
~(if m.Avatar != nil {
<div class="message-avatar">~(m.Avatar)</div>
})
<div class="message-body">~(m.Content)</div>
</li>
}
Composition then reads as data:
elem conversation() {
<ul class="chat">
~Message{
Avatar: Avatar{Initials: "AZ"},
Content: Bubble{Accent: true, Content: "What's Doors?"},
}
~Message{
Content: Bubble{Content: "A Go framework for server-side interactive apps."},
}
</ul>
}
For repeated blocks, make one component per item and render them with an explicit for loop — wrappers, classes, IDs, and per-item attributes stay 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(...)
Start timing and context semantics of doors.Go(f) are covered in Core Concepts.
Disposable Components
Give Main a value receiver and initialize state inside it — every render then works on a fresh copy of the component value, and the declared value itself is never mutated:
type Counter struct {
count doors.Source[int]
}
elem (c Counter) Main() {
~~
c.count = doors.NewSource(0)
~~
<div>
~(c.count.Bind(func(v int) gox.Elem {
return <span>~(v)</span>
}))
<button (doors.AClick{On: c.increment})>+</button>
</div>
}
func (c *Counter) increment(ctx context.Context, _ doors.RequestPointer) bool {
c.count.Mutate(ctx, func(v int) int { return v + 1 })
return false
}
Three receivers working together:
Mainon a value: each render gets its own copy, so the top~~ ~~block acts as a constructor — noNewCounterneeded.- Handlers and helpers on a pointer: inside
Main,c.incrementbinds to this render's copy — the one whose fields were just initialized. Everything in one mount shares that instance; two mounts share nothing. - The declared value is a prototype:
~Counter{}— or one sharedvar counter Counter— can be dropped anywhere, any number of times, with no stale state and no races through the shared value.
Where it pays off — a component value handed to a route branch is one Go value that renders again every time the route re-enters:
<>
~(path.Route(
doors.RouteMatch(func(p Path) bool {
return p.Section == SectionCounter
}).Comp(Counter{}),
doors.RouteDefaultComp[Path](Dashboard{}),
))
</>
A stateful component constructed outside the render would come back with its previous state; the disposable Counter re-initializes on every entry.
The flip side: state lives for one render of that fragment. Every re-render resets it. State that must survive re-renders needs an owner above the component — a pointer receiver and external construction.
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.