Hot Reloading Done Right!

When I first learned how to program, compile and execute my code, I ran into a frustrating problem: hot reloading. The idea of seeing changes immediately after saving is crucial in web development, as you make frequent changes and want to see the results as quickly as possible. It's also essential in game development, where you tweak variables and want to observe the effects instantly. While it's possible to use the compile command and run the executable with an IDE, I don't use IDEs in my workflow.

I use Neovim, by the way.

In my quest to find a solution that meets my development needs, I discovered several CLI tools that enable hot reloading for any program, any compiled programming language, and in any code editor. All you need is the build command, the directory where the executable resides, and some simple CLI tools. I'll do my best to explain each tool and how they work together.

Before we dive in, I want to clarify that there are many ways to solve this problem. My solution might not satisfy your needs, but it works on my machine, and I haven't seen a similar setup online, so I'm sharing it.

CLI Tools

I prefer tools over programs because I don't want to install a complex program with weird configs just to add hot reloading to my app. Some programs didn’t even work on my machine.

I'm using macOS, a Unix-based system. Things might differ on other platforms, but the process is pretty much the same on Linux (apart from tool installation).

  • GNU Make: A build automation tool that gets its knowledge from a Makefile. You can define sub-commands like init, build, or clean, and run them with make (e.g., make build).
  • find: A powerful file search utility that helps us locate the files we want to watch for changes. It's more flexible than ls.
  • entr: A simple utility for running arbitrary commands when files change. This is the magic ingredient for hot reloading.

Makefile

After installing the tools, we can start writing the Makefile.

  1. Create the file with touch Makefile.
  2. Open the file in your editor (I use Neovim). Syntax highlighting is optional but helpful.
  3. Depending on your project and programming language, you’ll need the compile command and the path to the executable. For this tutorial, I'll use C and the gcc compiler.

The C Program (for demonstration):

#include 

int main() {
    printf("Hello from @MoreThanCoder\n");
}

To compile this, I’ll use gcc -o main main.c, which creates an executable named main in the current directory.

Basic Makefile:

build:
    gcc -o main main.c

.PHONY: build

The .PHONY line ensures that make build is always executed, even if a file or directory named build exists.

Add a run Command:

build:
    gcc -o main main.c

run: build
    @./main

.PHONY: build run

The run command depends on build, so it will automatically trigger make build and then execute the program. The @ symbol suppresses the command output.

Add the Watch Command:

build:
    gcc -o main main.c

run: build
    @./main

watch:
    @find . -name "*.c" | entr -r make run

.PHONY: build run watch

The watch command uses find to search for all .c files and pipes the output to entr. The -r flag tells entr to restart the program when files change. When you run make watch, your program will rebuild and execute automatically upon saving changes.

Bonus for Web Devs

If you're a full-stack web developer, dealing with server restarts and browser refreshes can be painful. Here's a simple solution:

async function isAlive() {
    try {
        const res = await fetch('/ping', { cache: 'no-store' });
        return res.ok;
    } catch (error) {
        return false;
    }
}

async function monitor() {
    let restarted = false;
    while (true) {
        const alive = await isAlive();
        if (!alive) {
            if (!restarted) {
                console.debug("Server is down, waiting for recovery...");
                restarted = true;
            }
        } else if (restarted) {
            console.debug("Server is back alive, reloading...");
            location.reload();
            break;
        } else {
            console.debug("Server is alive.");
        }
        // Adjust the timeout based on your server restarting time
        await new Promise(r => setTimeout(r, 400));
    }
}

monitor();

This script pings a /ping route on your server. If the server goes down and comes back up, the script reloads the page. Add this to your HTML in development mode for a seamless experience.

Summary

We learned how to create a custom file watcher with hot reloading using make, entr, and basic Unix tools. If you found this helpful, support me with likes and subs to my YouTube channel.

Follow me on Telegram or GitHub.

Have a good day!