Components and Setup

Use vango.Setup for stateful components, keep render pure, and follow the allocation and lifecycle rules that keep Vango correct.

#setup#components#lifecycle

Components and Setup

The canonical stateful component shape in Vango is vango.Setup.

Use vango.Setup for stateful components

go
1type CounterProps struct {
2	Initial  int
3	Children vango.Slot
4}
5
6func Counter(p CounterProps) vango.Component {
7	return vango.Setup(p, func(s vango.SetupCtx[CounterProps]) vango.RenderFn {
8		props := s.Props()
9		count := setup.Signal(&s, props.Peek().Initial)
10
11		return func() *vango.VNode {
12			p := props.Get()
13			return Div(
14				Button(OnClick(count.Dec), Text("-")),
15				Span(Textf("%d", count.Get())),
16				Button(OnClick(count.Inc), Text("+")),
17				p.Children,
18			)
19		}
20	})
21}

Rules:

  • use a named props struct

  • use vango.NoProps when there are no props

  • Setup returns exactly one render closure

  • stateful component functions should normally return vango.Component

Props

Inside Setup, use props := s.Props().

  • props.Get() is a tracked read for render

  • props.Peek() is an untracked read for initialization

Use Get() in render and Peek() for one-time setup decisions like initial signal values.

Props are runtime-only. They do not persist, and props updates do not rerun Setup.

Slots

Children are explicit.

go
1type CardProps struct {
2	Title    string
3	Children vango.Slot
4}

Render slots directly with p.Children. For multiple slots, add more vango.Slot fields to the props struct.

Do not invent ad hoc []any child APIs for stateful components.

Stateless helpers are encouraged

Not every reusable view needs to be a component.

go
1func Badge(label string) *vango.VNode {
2	return Span(
3		Class("inline-flex rounded px-2 py-1 text-xs"),
4		Text(label),
5	)
6}

Use a plain function when there is no reactive allocation and the helper is just deterministic view composition.

Allocation rules

Reactive allocation in Setup must be unconditional.

Bad:

go
1if props.Peek().Admin {
2	_ = setup.Signal(&s, true)
3}

Bad:

go
1return func() *vango.VNode {
2	_ = setup.Signal(&s, 0)
3	return Div()
4}

Good:

go
1adminFlag := setup.Signal(&s, false)
2
3return func() *vango.VNode {
4	if !props.Get().Admin {
5		return Div()
6	}
7	return Div(Textf("%t", adminFlag.Get()))
8}

The rule is: allocate first, branch later.

Lifecycle methods

SetupCtx gives you:

  • s.Ctx()

  • s.OnMount(...)

  • s.Effect(...)

  • s.OnChange(...)

  • s.OnPersistError(...)

OnMount

Use OnMount for one-time post-commit setup such as subscriptions or small integrations.

Do not use it for blocking I/O, long-running goroutines, or signal allocation.

Effect

Use Effect for bounded reactive side effects after commit.

Keep effects cheap and non-blocking. Prefer framework helpers like Timeout, Interval, Subscribe, or GoLatest instead of raw goroutines.

OnChange

Use OnChange when one value changing should synchronously orchestrate another state transition.

Common uses:

  • reset dependent state

  • clear local errors on key changes

  • coordinate post-success UI cleanup

The standard checklist

  • use vango.Setup

  • allocate all state in Setup

  • keep render pure

  • no blocking I/O in Setup or render

  • no manual goroutines in Setup, render, or lifecycle callbacks

The next layer is how state and async work flow through a component. Read State and Async Work next.