LiveView for Go

github.com/malcolmston/liveview

Phoenix LiveView-style reactive server-rendered UI for Go.

 GitHubports phoenixframework/phoenix_live_view

A from-scratch, standard-library-only Go take on Phoenix LiveView: server-held state drives the UI, the browser sends events, and the server ships back a minimal diff describing only what changed. The core trick is LiveView's static/dynamic split — a template is compiled once into the literal fragments that never change and the interpolated values that do, so unchanged HTML never travels twice. You implement the View interface (Mount / HandleEvent / Render), keep per-connection state in a Socket with per-key change tracking, and let a Session run the mount → render → diff cycle; a tiny net/http Handler adds an optional HTTP + JSON transport on top. No cgo, no third-party dependencies — the import path and package are both liveview, and diffs marshal to the same compact JSON shape Phoenix LiveView uses.

Install

shell
$ go get github.com/malcolmston/liveview

Quick start

main.go
import "github.com/malcolmston/liveview"

var tmpl = liveview.MustParse(
	`<div><h1>{{ label }}</h1><span class="value">{{ count }}</div>`)

type Counter struct{}

func (Counter) Mount(_ map[string]any, s *liveview.Socket) error {
	s.Assign("count", 0)
	s.Assign("label", "Clicks")
	return nil
}

func (Counter) HandleEvent(e string, _ map[string]any, s *liveview.Socket) error {
	if e == "inc" {
		s.Assign("count", s.GetInt("count")+1)
	}
	return nil
}

func (Counter) Render(a map[string]any) *liveview.Rendered { return tmpl.Render(a) }

Features

  • The View lifecycle — Mount seeds state, HandleEvent reacts to a client event, Render returns a static/dynamic tree
  • Server-held state in a SocketAssign/AssignAll writes, GetInt/GetString/Get reads, with per-key Changed tracking
  • Tiny {{ name }} templates compiled once via MustParse / Parse into a fixed static/dynamic Template
  • A Rendered tree, not a string — Statics + Dynamics with the len(Statics) == len(Dynamics)+1 invariant, and HTML() to materialise it
  • Minimal-patch diff engine — DiffRendered emits only the changed dynamic slots (recursing into nested components), FullDiff sends the first frame with statics under "s"
  • Auto HTML-escaping by default, with Safe to opt trusted markup out of escaping
  • A per-connection Session (NewSession) that drives mount → render → diff; Session.Event returns just the Diff
  • An optional net/http transport — NewHandler serves the initial HTML page and accepts JSON events, so the state → render → diff core stays independent of the wire
  • Zero dependencies — pure Go standard library, nothing to audit but the toolchain
LiveView