Custom client directives with Astro

nguyen

Alex Nguyen

Posted on December 6, 2023

Custom client directives with Astro

Custom client directives are useful when you want more control over when framework components or islands hydrate in an Astro site. A direct benefit is potentially improved page performance by only loading scripts when they're actually needed.

Use case

It's common to have two navigation menus - one for mobile and one for desktop. If the mobile navigation is toggleable, client:visible can be used, so it hydrates at a low priority.

However, if the desktop navigation has dropdown menus that don't need to hydrate at all even when visible, then that component could use hover or mouseover based client directive instead, so it only hydrates based on a user action.

For example, I used this at vsctrust.org.nz. Using client:visible in a local production build:

client:visible critical requests chain

Using client:mouseover:

client:mouseover critical requests chain

Did it improve the performance to a level where I could perceive it? No, but it was a fun exercise, and maybe doing something like this would make a real difference on a heavier page, especially on older devices.

Creating a custom client directive

We'll look at creating a custom hover or mouseover client directive.

Somewhere in your Astro project, (like a lib folder or similar), create a folder called client-directives. In this folder, create a mouseover.js (or mouseover.ts if you prefer TypeScript) file and add:

/**
 * @type {import('astro').ClientDirective}
 */
export default (load, opts, element) => {
  element.addEventListener(
    "mouseover",
    async () => {
      const hydrate = await load();
      await hydrate();
    },
    { once: true }, // remove the event listener 
  );
};
Enter fullscreen mode Exit fullscreen mode
import type { ClientDirective } from "astro";

export default (function (load, _opts, element) {
  element.addEventListener(
    "mouseover",
    async () => {
      const hydrate = await load();
      await hydrate();
    },
  { once: true },
);
} satisfies ClientDirective);
Enter fullscreen mode Exit fullscreen mode

This will hydrate the component once the mouseover event is triggered on the element.

Then add this directive as an Astro integration. Create a register.js or register.ts file and add:

/**
 * @type {() => import('astro').AstroIntegration}
 */
export default () => ({
  name: "client:mouseover",
  hooks: {
    "astro:config:setup": ({ addClientDirective }) => {
      addClientDirective({
        name: "mouseover",
        entrypoint: "./src/lib/client-directives/mouseover.js",
      });
    },
  },
});
Enter fullscreen mode Exit fullscreen mode
import type { AstroIntegration } from "astro";

export default (): AstroIntegration => ({
  name: "client:mouseover",
  hooks: {
    "astro:config:setup": ({ addClientDirective }) => {
      addClientDirective({
        name: "mouseover",
        entrypoint: "./src/lib/client-directives/mouseover.ts",
      });
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

We'll also add types for the directive. Create an index.d.ts file and add:

import "astro";
declare module "astro" {
  interface AstroClientDirectives {
    "client:mouseover"?: boolean;
  }
}
Enter fullscreen mode Exit fullscreen mode

For WebStorm at least, this doesn't seem to affect any of the types or warnings that are picked up once you use a custom client directive, but it doesn't hurt to add this anyway.

Bringing it all together

Finally, we'll add the client directive to our Astro config:

import mouseoverDirective from "/src/lib/client-directives/register";

export default defineConfig({
  integrations: [
    mouseoverDirective(),
    // ...
  ],
});
Enter fullscreen mode Exit fullscreen mode

We can add it to any component like so:

<ReactComponent client:mouseover />
{/* You may get warnings about using it in your IDE (for example, that it requires a value in WebStorm), you could explicitly pass it a value of `true` (which doesn't affect anything but removes the warning) or just leave it. */}
Enter fullscreen mode Exit fullscreen mode
import { useEffect } from "react";

export function ReactComponent() {
  useEffect(() => {
    const content = document.querySelector("#content");
    if (content) content.textContent = "Hydrated";
  }, []);

  return <div id="content">Hover over me</div>;
}
Enter fullscreen mode Exit fullscreen mode

Then restart your development server for the client directive to be loaded.

To see if it's working, open up your browser's network tab, and check if this component's scripts are fetched once you hover over it.

Browser network tab showing component requests

You may not ever need to use a custom client directive, but it's a pretty cool feature that highlights Astro's flexibility.

Feel free to follow me or check out my blog.

💖 💪 🙅 🚩
nguyen
Alex Nguyen

Posted on December 6, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Custom client directives with Astro