Skip to main content jdo.sh

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!)

References