Solving Infinite Reloads When Using Air and Templ

Need the solution without the story? I gotchu 👊
Introduction
Go is a popular progamming language known for it’s simplicity and broad ecosystem. It is also the language that I am using for Project X. Development has gone well thus far and I am enjoying writing in it. There are many features of the language that make development life much easier and really speed up work.
Air
There are also a number of tools in the Go ecosystem that make it even easier. One of those tools is called Air1. Air is a hot-reloading library that monitors the local filesystem for changes, and then runs a set a commands when it detects those changes. For example, you could execute a test run or kick off a Typescript build. Air includes support for both pre and post-change hooks which makes it very powerful.
In my project, I quickly grew tired of the constant CTRL + C
→ go run cmd/main.go
and so I added the Air library to my (development) toolbox.
Templ
Another library that I found very useful is called Templ2. Templ is a templating engine similar to Laravel’s Blade3 library. Here is a sample Templ component:
go code snippet start
package components
templ Card (card CardData) {
<div class="card">
<h2>{ card.Title }</h2>
<p>{ card.Description }</p>
</div>
}
type CardData struct {
Title string
Description string
}
go code snippet end
When you run templ generate
, this Templ file gets processed and a new file is created next to the Templ file. This is what that file looks like:
go code snippet start
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.747
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func Card(card CardData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer,
...
})
}
type CardData struct {
Title string
Description string
}
go code snippet end
As you can see, this file uses a lot of generated variables and is regenerated each time the templ generate
command is run. To make my life easier, I added the templ generate
command to the pre_cmd
setting in Air:
toml code snippet start
[build]
...
cmd = "go build -o ./tmp/main ./cmd/main.go"
...
pre_cmd = [ "templ generate" ]
...
toml code snippet end
Understanding the Problem
One of the issues that I ran into fairly quickly when using these two tools together was an infinite loop: Air would detect a change in card.templ
, trigger the templ generate
call, which would in turn generate a card_templ.go
file. Air would detect a change in this new file, which would trigger the templ generate
call, which in turn would regenerate a new card_templ.go
file. Round and round it went.
You can see it play out here:
bash code snippet start
building...
running...
Successfully connected and migrated the database!
2024/08/25 20:48:19 Starting server on :8080
components/card.templ has changed
> templ generate
(✓) Complete [ updates=3 duration=20.501708ms ]
building...
running...
Successfully connected and migrated the database!
2024/08/25 20:48:20 Starting server on :8080
components/card_templ.go has changed
> templ generate
(✓) Complete [ updates=3 duration=21.856708ms ]
building...
running...
Successfully connected and migrated the database!
2024/08/25 20:48:21 Starting server on :8080
components/card_templ.go has changed
> templ generate
...
bash code snippet end
Workarounds and Solutions
I ended up searching online for others experiencing this issue. I didn’t find anyone documenting the exact issue, but I was able to find others using both Air and Templ. I quickly noticed two (2) settings in the Air config that looked promising:
toml code snippet start
[build]
...
exclude_file = [ ]
exclude_regex = ["_test.go"]
toml code snippet end
I first tried to add a wildcard to the exclude_file
array, but this didn’t work.
exclude_file = [ "components/**/*_templ.go" ]
I then found this article by @adrianhesketh4 who pointed out that the exclude_regex
option is the one I needed. I updated my Air file to include the setting for the templ.go
files:
toml code snippet start
[build]
...
exclude_regex = ["_test.go", ".*_templ.go"]
toml code snippet end
Success!
Conclusion
While this didn’t end up being a terribly complicated issue, I didn’t find it obvious that this would happen when I started using these two libraries together. Now I know (and you do, too!)