06 Long lists
A list with thousands of rows does not need thousands of DOM nodes. Keep only
the visible window in the DOM and let the browser scroll a tall spacer. Core
gothic does not ship a virtual list component; the window math lives in the
separate virtual module so your own view stays in control.
go get github.com/metruzanca/gothic/virtualThe window
virtual.WindowFor maps a scroll offset to a slice of rows:
import "github.com/metruzanca/gothic/virtual"
win := virtual.WindowFor(msg.Top, 36, 432, len(m.Items))
m.Start = win.Start
m.Count = win.Count
m.Top = win.Top
m.Bottom = win.BottomscrollTopis the pixel offset of the scroll container.rowHeightis the fixed height of one row.viewportHeightis the height of the visible area.totalis the number of rows.
Window has four fields:
Startis the first visible index.Countis how many rows to render.Topis the spacer height before the window.Bottomis the spacer height after the window.
The helper snaps to a row boundary and clamps to the end of the list. The same rules run on the server and in the browser.
Render the window
Put the state on the model:
type Model struct {
Rows []Row
Window virtual.Window
}
type Scrolled struct{ Top int }
func NewModel(rows []Row) Model {
m := Model{Rows: rows}
m.Window = virtual.WindowFor(0, 36, 432, len(rows))
return m
}
func (m Model) Update(msg gothic.Msg) (gothic.Model, gothic.Cmd) {
switch msg := msg.(type) {
case Scrolled:
m.Window = virtual.WindowFor(msg.Top, 36, 432, len(m.Rows))
}
return m, nil
}Render a scroll container, a top spacer, the visible slice, and a bottom spacer:
<div class="viewport" style={ "height:" + dom.Px(432) } onscroll={ Scrolled{Top: event.target.scrollTop} }>
<div style={ "height:" + dom.Px(model.Window.Top) }></div>
for _, row := range model.Rows[model.Window.Start : model.Window.Start+model.Window.Count] {
<div class="row" key={ row.ID }>{ row.Text }</div>
}
<div style={ "height:" + dom.Px(model.Window.Bottom) }></div>
</div>dom.Px(n) formats an integer pixel length on the server and lowers to a px
helper in the browser.
The keyed list keeps rows stable while the window moves. Without a key, the client can only append and remove from the end.
Variable row heights
Use virtual.VariableWindow when rows can have different heights:
win := virtual.VariableWindow(heights, estimate, scrollTop, viewportHeight, total)heights maps a row index to its measured height. Missing or zero entries use
estimate, which defaults to 40px.
virtual.WindowForRows selects fixed or variable mode from the row height:
win := virtual.WindowForRows(heights, 0, 40, scrollTop, viewportHeight, total)Variable mode needs measured heights. Measure rows after they render and store
the result in the model, or use browser.ClientHeight for a one-off
measurement. The example uses fixed rows.
Full example
virtual/example is a 5,000 row app with two lists. It shows:
- a window computed in
NewModelso SSR matches the first paint, - a scroll message for each list,
- a keyed slice of the visible rows,
- top and bottom spacers,
- a second list with a different row height and markup.
Run it:
cd virtual/example
go run .Limits
- the full item list still lives in the client model,
- rows outside the window are not kept alive,
- the example uses fixed row heights and fixed viewport heights,
- scroll throttling is not included.
What next
You have the full guide. The last chapter explains what the compiler emits and how the runtime works: