diff --git a/docs/docs/03-syntax-and-usage/18-render-once.md b/docs/docs/03-syntax-and-usage/18-render-once.md
new file mode 100644
index 000000000..d2c42cda5
--- /dev/null
+++ b/docs/docs/03-syntax-and-usage/18-render-once.md
@@ -0,0 +1,102 @@
+# Render once
+
+If you need to render something to the page once per page, you can create a `*OnceHandler` with `templ.NewOnceHandler()` and use its `Once()` method.
+
+The `*OnceHandler.Once()` method ensures that the content is only rendered once per distinct context passed to the component's `Render` method, even if the component is rendered multiple times.
+
+## Example
+
+The `hello` JavaScript function is only rendered once, even though the `hello` component is rendered twice.
+
+:::warning
+Dont write `@templ.NewOnceHandle().Once()` - this creates a new `*OnceHandler` each time the `Once` method is called, and will result in the content being rendered multiple times.
+:::
+
+```templ title="component.templ"
+package once
+
+var helloHandle = templ.NewOnceHandle()
+
+templ hello(label, name string) {
+ @helloHandle.Once() {
+
+ }
+
+}
+
+templ page() {
+ @hello("Hello User", "user")
+ @hello("Hello World", "world")
+}
+```
+
+```html title="Output"
+
+
+
+```
+
+:::tip
+Note the use of the `data-name` attribute to pass the `name` value from server-side Go code to the client-side JavaScript code.
+
+The value of `name` is collected by the `onclick` handler, and passed to the `hello` function.
+
+To pass complex data structures, consider using a `data-` attribute to pass a JSON string using the `templ.JSONString` function, or use the `templ.JSONScript` function to create a templ component that creates a `
+ }
+}
+```
+
+You can then use the `JQuery` component in other packages, and the jQuery library will only be included once in the rendered HTML.
+
+```templ title="main.templ"
+package main
+
+import "deps"
+
+templ page() {
+
+
+ @deps.JQuery()
+
+
+
Hello, World!
+ @button()
+
+
+}
+
+templ button() {
+ @deps.JQuery()
+
+}
+```
diff --git a/generator/test-once/expected.html b/generator/test-once/expected.html
new file mode 100644
index 000000000..2103bd24a
--- /dev/null
+++ b/generator/test-once/expected.html
@@ -0,0 +1,7 @@
+
+
+
diff --git a/generator/test-once/render_test.go b/generator/test-once/render_test.go
new file mode 100644
index 000000000..3d1f2376b
--- /dev/null
+++ b/generator/test-once/render_test.go
@@ -0,0 +1,23 @@
+package once
+
+import (
+ _ "embed"
+ "testing"
+
+ "github.com/a-h/templ/generator/htmldiff"
+)
+
+//go:embed expected.html
+var expected string
+
+func Test(t *testing.T) {
+ component := render()
+
+ diff, err := htmldiff.Diff(component, expected)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if diff != "" {
+ t.Error(diff)
+ }
+}
diff --git a/generator/test-once/template.templ b/generator/test-once/template.templ
new file mode 100644
index 000000000..7fd03e882
--- /dev/null
+++ b/generator/test-once/template.templ
@@ -0,0 +1,19 @@
+package once
+
+var helloHandle = templ.NewOnceHandle()
+
+templ hello(label, name string) {
+ @helloHandle.Once() {
+
+ }
+
+}
+
+templ render() {
+ @hello("Hello User", "user")
+ @hello("Hello World", "world")
+}
diff --git a/generator/test-once/template_templ.go b/generator/test-once/template_templ.go
new file mode 100644
index 000000000..1d76dd490
--- /dev/null
+++ b/generator/test-once/template_templ.go
@@ -0,0 +1,109 @@
+// Code generated by templ - DO NOT EDIT.
+
+package once
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import "context"
+import "io"
+import "bytes"
+
+var helloHandle = templ.NewOnceHandle()
+
+func hello(label, name string) templ.Component {
+ return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+ if !templ_7745c5c3_IsBuffer {
+ templ_7745c5c3_Buffer = templ.GetBuffer()
+ defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Var2 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+ if !templ_7745c5c3_IsBuffer {
+ templ_7745c5c3_Buffer = templ.GetBuffer()
+ defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if !templ_7745c5c3_IsBuffer {
+ _, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
+ }
+ return templ_7745c5c3_Err
+ })
+ templ_7745c5c3_Err = helloHandle.Once().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if !templ_7745c5c3_IsBuffer {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+ }
+ return templ_7745c5c3_Err
+ })
+}
+
+func render() templ.Component {
+ return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+ if !templ_7745c5c3_IsBuffer {
+ templ_7745c5c3_Buffer = templ.GetBuffer()
+ defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var5 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var5 == nil {
+ templ_7745c5c3_Var5 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = hello("Hello User", "user").Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = hello("Hello World", "world").Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if !templ_7745c5c3_IsBuffer {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+ }
+ return templ_7745c5c3_Err
+ })
+}
diff --git a/once.go b/once.go
new file mode 100644
index 000000000..a69d0b20e
--- /dev/null
+++ b/once.go
@@ -0,0 +1,43 @@
+package templ
+
+import (
+ "context"
+ "io"
+ "sync/atomic"
+)
+
+// onceHandleIndex is used to identify unique once handles in a program run.
+var onceHandleIndex int64
+
+// NewOnceHandle creates a OnceHandle used to ensure that the children of its
+// `Once` method are only rendered once per context.
+func NewOnceHandle() *OnceHandle {
+ return &OnceHandle{
+ id: atomic.AddInt64(&onceHandleIndex, 1),
+ }
+}
+
+// OnceHandle is used to ensure that the children of its `Once` method are are only
+// rendered once per context.
+type OnceHandle struct {
+ // id is used to identify which instance of the OnceHandle is being used.
+ // The OnceHandle can't be an empty struct, because:
+ //
+ // | Two distinct zero-size variables may
+ // | have the same address in memory
+ //
+ // https://go.dev/ref/spec#Size_and_alignment_guarantees
+ id int64
+}
+
+// Once returns a component that renders its children once per context.
+func (o *OnceHandle) Once() Component {
+ return ComponentFunc(func(ctx context.Context, w io.Writer) (err error) {
+ _, v := getContext(ctx)
+ if v.getHasBeenRendered(o) {
+ return nil
+ }
+ v.setHasBeenRendered(o)
+ return GetChildren(ctx).Render(ctx, w)
+ })
+}
diff --git a/once_test.go b/once_test.go
new file mode 100644
index 000000000..c2681be30
--- /dev/null
+++ b/once_test.go
@@ -0,0 +1,106 @@
+package templ_test
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "testing"
+
+ "github.com/a-h/templ"
+ "github.com/google/go-cmp/cmp"
+)
+
+type onceHandleTest struct {
+ ctx context.Context
+ expected string
+}
+
+func TestOnceHandle(t *testing.T) {
+ withHello := templ.WithChildren(context.Background(), templ.Raw("hello"))
+ tests := []struct {
+ name string
+ tests []onceHandleTest
+ }{
+ {
+ name: "renders nothing without children",
+ tests: []onceHandleTest{
+ {
+ ctx: context.Background(),
+ expected: "",
+ },
+ },
+ },
+ {
+ name: "children are rendered",
+ tests: []onceHandleTest{
+ {
+ ctx: templ.WithChildren(context.Background(), templ.Raw("hello")),
+ expected: "hello",
+ },
+ },
+ },
+ {
+ name: "children are rendered once per context",
+ tests: []onceHandleTest{
+ {
+ ctx: withHello,
+ expected: "hello",
+ },
+ {
+ ctx: withHello,
+ expected: "",
+ },
+ },
+ },
+ {
+ name: "different contexts have different once state",
+ tests: []onceHandleTest{
+ {
+ ctx: templ.WithChildren(context.Background(), templ.Raw("hello")),
+ expected: "hello",
+ },
+ {
+ ctx: templ.WithChildren(context.Background(), templ.Raw("hello2")),
+ expected: "hello2",
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := templ.NewOnceHandle().Once()
+ for i, test := range tt.tests {
+ t.Run(fmt.Sprintf("render %d/%d", i+1, len(tt.tests)), func(t *testing.T) {
+ html, err := templ.ToGoHTML(test.ctx, c)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if diff := cmp.Diff(test.expected, string(html)); diff != "" {
+ t.Errorf("unexpected diff:\n%v", diff)
+ }
+ })
+ }
+ })
+ }
+ t.Run("each new handle manages different state", func(t *testing.T) {
+ ctx := templ.WithChildren(context.Background(), templ.Raw("hello"))
+ h1 := templ.NewOnceHandle()
+ c1 := h1.Once()
+ h2 := templ.NewOnceHandle()
+ c2 := h2.Once()
+ c3 := h2.Once()
+ var w strings.Builder
+ if err := c1.Render(ctx, &w); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := c2.Render(ctx, &w); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := c3.Render(ctx, &w); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if diff := cmp.Diff("hellohello", w.String()); diff != "" {
+ t.Errorf("unexpected diff:\n%v", diff)
+ }
+ })
+}
diff --git a/runtime.go b/runtime.go
index ef9df246a..1a18e9041 100644
--- a/runtime.go
+++ b/runtime.go
@@ -593,9 +593,25 @@ type contextKeyType int
const contextKey = contextKeyType(0)
type contextValue struct {
- ss map[string]struct{}
- children *Component
- nonce string
+ ss map[string]struct{}
+ onceHandles map[*OnceHandle]struct{}
+ children *Component
+ nonce string
+}
+
+func (v *contextValue) setHasBeenRendered(h *OnceHandle) {
+ if v.onceHandles == nil {
+ v.onceHandles = map[*OnceHandle]struct{}{}
+ }
+ v.onceHandles[h] = struct{}{}
+}
+
+func (v *contextValue) getHasBeenRendered(h *OnceHandle) (ok bool) {
+ if v.onceHandles == nil {
+ v.onceHandles = map[*OnceHandle]struct{}{}
+ }
+ _, ok = v.onceHandles[h]
+ return
}
func (v *contextValue) addScript(s string) {