Skip to content

Render once

Use a request-scoped handle when a component can appear many times but its markup should render only once per request.

Copy the helper

Copy this into the package that owns the singleton component:

go
package app

import (
	"context"
	"net/http"
	"sync"
	"sync/atomic"
)

type OnceHandle struct{ id int64 }

var onceIndex atomic.Int64

func NewOnce() *OnceHandle {
	return &OnceHandle{id: onceIndex.Add(1)}
}

type onceState struct {
	mu   sync.Mutex
	seen map[*OnceHandle]struct{}
}

type onceKeyT struct{}

func (h *OnceHandle) firstRender(ctx context.Context) bool {
	state, _ := ctx.Value(onceKeyT{}).(*onceState)
	if state == nil {
		return true
	}

	state.mu.Lock()
	defer state.mu.Unlock()
	if _, seen := state.seen[h]; seen {
		return false
	}
	state.seen[h] = struct{}{}
	return true
}

func withOnceScope(ctx context.Context) context.Context {
	if state, _ := ctx.Value(onceKeyT{}).(*onceState); state != nil {
		return ctx
	}
	return context.WithValue(ctx, onceKeyT{}, &onceState{
		seen: make(map[*OnceHandle]struct{}),
	})
}

func OnceScopeMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := withOnceScope(r.Context())
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

Keep the non-zero-size id field. Go may give zero-size values the same address, which would let distinct pointer-keyed handles collide.

Install the request scope

Wrap the handler or router before rendering a page:

go
handler := OnceScopeMiddleware(mux)

The middleware creates a fresh seen-set for each request. It is idempotent, so wrapping an already-scoped handler reuses the current set.

Add the component

Guard the children with firstRender:

gsx
component Once(handle *OnceHandle) {
	{ if handle.firstRender(ctx) { { children } } }
}

Use it

Declare one package-level handle for each singleton:

gsx
var dialogContainerOnce = NewOnce()

component DialogContainer() {
	<Once handle={dialogContainerOnce}>
		<div id="dialog-root"></div>
	</Once>
}

Every <DialogContainer/> call in one request shares that handle. A different handle renders its own singleton, and the same handle renders again on the next request.

What happens without middleware

Without OnceScopeMiddleware, firstRender returns true every time. Content renders on every call, but nothing panics.