DEV Community

Cover image for Understanding Astro integrations and the hooks lifecycle
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Understanding Astro integrations and the hooks lifecycle

Written by Ohans Emmanuel✏️

With just a few lines of code, Astro integrations let you add new functionality and behavior to an Astro project. However, to effectively write Astro integrations, we must understand the Astro hooks lifecycle.

These hooks are how we plug into Astro’s internal build process and specify custom behavior. This article serves as a guide to every hook, including where they should be used and how to create a custom integration that logs all relevant hooks in a sample Astro build process.

Jump ahead:

What are Astro integrations?

By definition, integrations extend your Astro project with new functionality and behavior. Most of the integrations you build will be to support a particular feature, such as the official sitemap integration that generates a sitemap when you build your Astro project.

Once you sufficiently understand how to build feature integrations, you‘ll be able to transfer your knowledge to building libraries or renderer integrations, such as the official React, Preact, Vue, or Tailwind integrations for Astro.

The Astro hooks lifecycle

In a general sense, “lifecycle” refers to the series of changes in the life of an organism. For example, a butterfly starts as an egg, then turns into a larva, a pupa, and ultimately a full-blown adult butterfly. Until human cloning becomes available, there’s a decent chance you also started as an infant, then became a toddler, went through puberty, and finally entered adulthood. At least, I hope so!

In software, “lifecycle” refers to the different stages of a given process. With Astro hooks, we explicitly refer to the stages Astro goes through while building your application pages.

This process covers resolving the Astro configuration setup, spinning up a local server, and bundling your pages statically or rendering them on the server side in production.

To productively develop custom integrations, we’ll need to know where in the lifecycle we want to effect a change or reaction. Hooks are functions that are called at various stages of the build; to interact with the build process, we can use the following ten hooks:

  • astro:config:setup
  • astro:config:done
  • astro:server:setup
  • astro:server:start
  • astro:server:done
  • astro:build:start
  • astro:build:setup
  • astro:build:generated
  • astro:build:ssr
  • astro:build:done

Ten seems like a lot to remember, but you don’t have to memorize these. Instead, we’ll focus on understanding how they work; you can always refer to the official reference when needed.

How to order and trigger hooks

One of the first questions I asked myself when I started tinkering with Astro integrations was, “When exactly are these triggered, and is there some order of execution to them?”

Let’s consider the following diagram, which depicts the order in which the hooks are executed: The order in which hooks are executed in development and production modes Two hooks kick off the process:

  1. astro:config:setup
  2. astro:config:done

These hooks are always executed, regardless of the Astro build process. Let’s breakdown when these are executed and how to leverage them in custom integrations.

astro:config:setup

Think about being the first one at the pub, before it opens. You can cause a ruckus before anyone else even shows up!

Similarly, this hook is executed when Astro is initialized. This happens before the Astro project or Vite config is resolved. The astro:config:setup hook is where we swoop in to extend the project configuration. In this hook, we can update the Astro config, apply Vite plugins, add component renderers, and inject scripts long before Astro starts building the application.

astro:config:done

Like a perfect pint of beer, we patiently wait to grab the glass until after it’s been poured. Similarly, this hook indicates when the Astro config has been resolved. When this hook triggers, it means every astro:config:setup hook has been invoked for every integration in the project.

After the Astro config has finally got its act together and all the other integrations have done their thing, we can retrieve the final config and use it in our integration.

Once we fire astro:config:done, there are two branches to consider: development and production mode.

In development mode

Take a look at the flowchart above again. When developing your apps locally — without initiating a production build typically via npm run build or astro build — the order of hooks execution in our app will follow the left side of the flowchart. That flow triggers the following hooks:

  • astro:server:setup
  • astro:server:start
  • astro:server:done

These hooks are executed when building your app for local development.

Here’s a breakdown of when these are executed and how we could leverage these in our custom integrations:

Hook Executed when … Why use this?
`astro:server:setup` We create the Vite server in development mode, before the `listen()` server event is fired, i.e., before starting the server We can update the Vite server options and middleware. The Vite dev server object is passed as an argument to our hook.
`astro:server:start` We fire the Vite `listen()` method, i.e., the server is running We can jump in here to intercept network requests at the specified dev server address (passed as an argument to our hook)
`astro:server:done` We closed the dev server This is where we run cleanups. If you wish to clean up any side effects triggered during `astro:server:setup` or `astro:server:start`, now is a good time!

In production

When we run a production build, two hooks will always be triggered. These are:

  • astro:build:start
  • astro:build:setup

Here’s a breakdown of when these execute and how we can leverage them in our custom integrations:

Hook Executed when … Why use this?
`astro:build:start` After the Astro config is completely resolved, but before the production build begins The production build is about to start, but you can still set up some global objects or clients needed during the build
`astro:build:setup` The build is just about to start and the build config is fully constructed To steal the perfect phrase from the official Astro documentation: this is our final chance to modify the build. We can overwrite some defaults and make sure everything is looking top-notch.

I must mention that if you're not sure whether to use astro:build:setup or astro:build:start, go for astro:build:start instead.

Now, depending on how the page is being built (statically generated or server-side-rendered), either astro:build:generated or astro:build:ssr will be invoked, respectively. Finally, we invoke astro:build:done.

Here’s the final breakdown of when these are executed and how we can use them in our custom integrations:

Hook Executed when … Why use this?
`astro:build:generated` The static production build has completely generated routes and assets. We can access generated routes and assets before we clean up build artifacts. Per the official docs, this is uncommon, and we might be better off using `astro:build:done` in many cases, except when we really need to access the generated files before cleanup.
`astro:build:ssr` A production SSR build completes We get access to the SSR manifest, which is helpful when creating custom SSR builds.
`astro:build:done` The production build completes! This is where we may access the generated routes and assets, so we can copy them or transform them. To transform generated assets, consider using a Vite plugin and configuring `astro:config:setup`

Examining the hooks evaluation order

Let’s try some practice examples. Go ahead and write out a simple integration that, when invoked, spits out a log to the server console. Then, you can tinker with building several pages for production and inspect the logs.

Our eventual goal is to have a custom integration that looks something like this:

{
  name: "some-identifier",
  hooks: {
    "hook-name": () => {
    // log hook name so we know it's been invoked
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Let’s go ahead and build this out. Create a new Astro application with the following custom integration:

// 📂 src/integrations/lifecycle-logs.ts

import kleur from "kleur";
import type { AstroIntegration } from "astro";

// Create a new dateTimeFormat object
const dateTimeFormat = new Intl.DateTimeFormat([], {
  hour: "2-digit",
  minute: "2-digit",
  second: "2-digit",
});

export const lifecycleLogs = () => {
  const hooks = [
    `astro:config:setup`,
    `astro:config:done`,
    `astro:server:setup`,
    `astro:server:start`,
    `astro:server:done`,
    `astro:build:start`,
    `astro:build:setup`,
    `astro:build:generated`,
    `astro:build:ssr`,
    `astro:build:done`,
  ] as const;

  // base integration structure. "hooks" will be updated
  let integration: AstroIntegration = {
    name: "astro-lifecycle-logs",
    hooks: {},
  };

  // loop over the hooks list and add the name and log
  for (const hook of hooks) {
    integration.hooks[hook] = () => {
      // 👀 Get a new date string
      const date = dateTimeFormat.format(new Date());

      // log with kleur colours and formatting
      console.log(`${kleur.gray(date)} ${kleur
        .bold()
        .yellow("[lifecycle-log]")} ${kleur.green(hook)}
        `);
    };
  }

  return integration;
};

export default lifecycleLogs;
Enter fullscreen mode Exit fullscreen mode

Import lifecycleLogs and add it to your project’s integration list:

// astro.config.mjs 
import { defineConfig } from "astro/config";
import { lifecycleLogs } from 'src/integrations/lifecycle-logs'

export default defineConfig({
  // 👀 invoke the imported lifecycleLogs function in the list
  integrations: [lifecycleLogs()],
});
Enter fullscreen mode Exit fullscreen mode

Finally, (re)start your application to see the logs in the console as shown below: The dev lifecycle hooks As an exercise, I suggest you add a new SSR page and run a production build to see the order of hooks execution logged. Below is an example with two pages:

  • A static index.astro page
  • A server-side-rendered ssr.astro page

Now, observe the logs in the image below, and notice the [lifecycle-log] messages from astro:config:setup to astro:build:done. This represents the full lifecycle of the build process i.e., captured and logged. The entire hook lifecycle, logged

Conclusion

Understanding Astro integrations and the hooks lifecycle is essential to building custom integrations. With your new knowledge of Astro’s ten hooks and their timings, you can go ahead and build more robust custom integrations.

If you want to keep in touch with me, you can find all my contact information on my personal site or LinkedIn. Cheers!


Get set up with LogRocket's modern error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID.
  2. Install LogRocket via NPM or script tag. LogRocket.init() must be called client-side, not server-side.

NPM:

$ npm i --save logrocket 

// Code:

import LogRocket from 'logrocket'; 
LogRocket.init('app/id');
Enter fullscreen mode Exit fullscreen mode

Script Tag:

Add to your HTML:

<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
Enter fullscreen mode Exit fullscreen mode

3.(Optional) Install plugins for deeper integrations with your stack:

  • Redux middleware
  • ngrx middleware
  • Vuex plugin

Get started now

Top comments (1)

Collapse
 
kirilldedeshin profile image
Kirill Dedeshin

Thanks for your job