Important
Practices you should understand and follow.
1. Use local context
In all templ
components, handlers and listeners framework provides context.Context
value.
❌ Using context.Background()
or any external context instead of the ctx
provided by the framework in framework-related operations. It panics.
❌ Try to enable interactivity/reactivity with doors in templ-based pages not served by doors. It panics.
✅ Update Door
, read and mutate Beam/SourceBeam
values, use doors.A...
binds using context value in scope inside frameworks space.
✅ Use the closest context in scope. It is used to track component relations, progress of actions, and more.
templ (f *fragment) Render() {
@doors.Run(func(pageCtx context.Context) {
itemId.Sub(pageCtx, func(subCtx context.Context, id int) bool {
✅ f.door.Update(subCtx, card(id))
// ❌ f.door.Update(pageCtx, card(id))
// ❌ f.door.Update(context.Background(), card(id))
return false
})
})
@f.door
}
Context lifecycle is linked to the dynamic container tree. You can use
ctx.Done()
channel in spawned goroutines to trigger clean up on DOM removal.
2. Respect the concurrency model
Rendering operations and Beam
(state) watchers are executed in the framework’s runtime on a goroutine pool.
Beam
propagation begins at the root and proceeds in parallel down each branch, where each dynamic container waits for its subscribers to complete before updating its children.- The
Beam
algorithm ensures that all elements observe the same state value consistently during a render. - The content of each dynamic container (
Door
) is rendered in its own goroutine
Best practices:
- Query data and make requests to external systems during rendering or, less preferably, in
Beam
subscription handlers. - Use manually spawned (or via
@doors.Go
) goroutines, or, less preferably, hook handlers to wait on channels, asynchronous events, or perform any long-running operations.
✅ Best: query data during render:
templ (c *card) Render() {*
// predictable wait time
{{ item := db.get(c.id) }}
/* use item in render */
}
⚠️ Acceptable: query data in subscription
pathBeam.Sub(ctx, func(ctx context.Context, p Path) bool {
item := db.get(c.id)
door.Update(ctx, itemInfo(item))
}
Querying data in the beam watcher will delay data propagation to nested elements; this is acceptable.
✅ Spawn goroutine for long-running tasks:
templ (f *fragment) Render() (
// spawned goroutine
@doors.Go(func(ctx context.Context) {
select {
// after 1 minute update
case <-time.After(1 * time.Minute):
f.door.Update(ctx, doors.Text("Updated after 1 minute!"))
// or cancel if unmounted
case <-ctx.Done():
return
}
})
@f.door {
Initial State
}
)
❌ Bad
- Wait for external systems events during render or in Beam subscription handlers
- Try to synchronize rendering operations with each other by blocking in render functions
❌ Block framework runtime to wait for other runtime-dependent operations to complete, like :
pathBeam.Sub(ctx, func(ctx context.Context, p Path) bool {
// XUpdate returns channel to track when frontend applies update
err, ok := <- door.XUpdate(ctx, itemInfo(item)) // can cause a deadlock!
/* ... */
return false
}
✅ Instead, do this:
pathBeam.Sub(ctx, func(ctx context.Context, p Path) bool {
// spawn independent goroutine
go func() {
// tell the framework that you are safe
blockingCtx := doors.AllowBlocking(ctx)
err, ok := <- door.XUpdate(blockingCtx, itemInfo(item))
/* ... */
}()
return false
}
❌ Block the render like:
templ (f *fragment) Render() {
// blocking receive from pubsub topic
{{ msg, _ := <- f.pubsub.Channel() }}
// print payload
{ msg.Payload }
}
✅ Instead, do this:
templ (f *fragment) Render() {
// initialize door
{{ door := doors.Door{} }}
// spawn goroutine
@doors.Go(func(ctx context.Context) {
select {
// blocking recieve from pubsub topic
case msg, _ := <- f.pubsub.Channel():
n.update(doors.Text(msg.Payload))
// or cancel if unmounted
case <-ctx.Done():
return
}
})
// render door
@door
}
❌ Render-time shared state race:
templ (f *fragment) Render() {
// initialize door
{{ door := doors.Door{} }}
{{ foo := "bar" }}
@f.door { // content of the door is rendered in its own goroutine!
{{ foo = "" }}
}
{ foo } // probably will render "bar"
}
3. Understand the security model.
- For protected pages, verify cookie authentication in the
ServePage
handler - Don’t forget to call
doors.SessionEnd(ctx)
when the user logs out, and manage framework session expiration withdoors.SessionExpire(ctx, duration)
. Otherwise, you might leave private page instances active after authentication has ended. - There is no need to check cookies/headers in the event handlers, because they are already protected
- If user access to certain actions or views can be revoked, you should
- Verify user view permissions during render to ensure that the user can’t access previously available views with dynamic navigation.
- Verify user write permissions in the transactions to ensure that even if the permission is revoked after rendering, you are still safe.
4. Understand the page instance lifecycle.
Every page opened by a user has a representation in the server memory. And so, they need to be cleaned up.
After the page instance is cleaned up, it enters a suspended state on the client side, and upon tab switch on the interaction, the page is automatically reloaded.
There are certain configuration options related to the instance lifecycle:
- Max instances per session. Upon reaching the limit, older and less active instances are suspended and cleaned up.
- Instance time to live. Disconnected instances get cleaned up after a certain time.
- Time after the hidden page is disconnected. If the browser tab is not active, the client disconnects from the server after a certain time and then gets cleaned up after the “instance time to live”.
Instance memory consumption depends highly on page complexity, but I expect it to typically be around 50-150KB and low enough to choose generous, UX friendly values for those settings.
Refer to Configuration for more details.
5. Avoid storing database data in state.
In general, you don’t need to store database query results in fields or Beams
.
✅ Store the ID in the Fragment field.
func newCard(id string) *card {
return &card {
// store id
id: id,
}
}
type card struct {
id string
}
templ (c *card) Render() {
// retrieve db data
{{ item := db.get(c.id) }}
/* use item in render */
}
✅ Store ID in Beam
idBeam := doors.NewBeam(pathBeam, func(p Path) string {
return p.id
})
❌ Store DB entry in the Fragment field like:
func newCard(id string) *card {
// query
item := db.get(c.id)
return &card {
// store item
item: item,
}
}
type card struct {
item db.Item
}
templ (c *card) Render() {
/* use c.item in render */
}
❌ Store db entry in the Beam like:
itemBeam := doors.NewBeam(pathBeam, func(p Path) db.Item {
return db.get(p.id)
})
If you need data only to produce render output - render and forget, so you won’t waste server memory for nothing. However, it’s your decision.
6. Be careful with front-end manipulations via JavaScript
The framework controls parts of the DOM, so separate concerns clearly.