astro-hidratacion-directivas-client

Hydration in Astro

  • 6 min

In the previous article, we saw interactivity islands, but we stopped halfway through the best part (a little bit of a cliffhanger, you know… 😉).

We rendered a React component (a counter), it looked perfect, but when we clicked it did absolutely nothing.

The reason is that Astro, by default, sends clean, bare HTML. That is, it renders the component on the server, takes a “snapshot” of the initial state, and that’s what it sends to the browser. It removes all JavaScript to save weight.

For the component to “come back to life” and respond to our interactions, we need to Hydrate it 💧💧.

What is hydration?

In the context of web development, Hydration is the process by which the browser’s JavaScript code “hooks onto” the static HTML that came from the server to add interactivity (event listeners, state management, etc.).

Most frameworks (Next.js, Nuxt) hydrate the entire page as soon as it loads. Astro is different. Astro allows Partial Hydration.

We can tell it:

Astro, I want this page to be 90% static HTML, but I want this specific component to load its JavaScript and be interactive

To do this, we use the client: directives.

The hydration directives

These directives are special attributes that are added to the component when we use it inside an .astro file.

DirectiveJS LoadsPriorityUse Case
(none)NeverN/AStatic content, text, images.
client:loadImmediatelyHighMenus, critical elements.
client:idleWhen idleMediumWidgets, secondary interactivity.
client:visibleOn scrollLowGalleries, Footer, heavy elements.
client:mediaBased on screenVariableMobile menus, Sidebars.
client:onlyImmediatelyHighAccess to window, localStorage.

The client:* directives are not placed inside the component’s code (e.g., inside the .jsx), but at the moment of using it in the .astro template.

Let’s review them one by one, ordered by load priority.

client:load (high priority)

This is the most aggressive directive. It tells Astro: “Load and hydrate this component’s JavaScript immediately, as soon as the page loads”.

<Contador client:load />
Copied!
  • For elements the user needs to use immediately upon opening the website.
  • Examples: The navigation bar (if it has a mobile dropdown menu), a login banner, or the main element of an SPA.

Cost: Impacts the initial TTI (Time to Interactive), as the browser must download and execute the JS before finishing processing.

client:idle (medium priority)

This is where Astro starts to shine. This directive tells the browser: “Wait until you finish loading everything important, and when you have a free moment (are idle), hydrate this component”.

Technically, it uses requestIdleCallback to take advantage of the main thread’s idle time.

<Contador client:idle />
Copied!
  • For interactive elements that are not critical for the first impression.
  • Examples: A “Like” button, a support chat widget, or a search bar in the footer.

client:visible (on-demand priority)

This is my favorite and the one that saves the most resources. The component does not load its JavaScript until the user scrolls and the component enters the viewport.

Technically, it uses an internal IntersectionObserver. If the component is in the footer and the user never scrolls down, that JavaScript is never downloaded.

<Contador client:visible />
Copied!
  • For heavy components that are “below the fold” (outside the first screen).
  • Examples: Image carousels, interactive galleries, contact forms at the end of a landing page.

You can combine it with a safety margin: client:visible={{ rootMargin: "200px" }} to start loading a bit before it appears on screen, making the transition invisible to the user.

client:media (CSS conditional)

This directive hydrates the component only if a CSS Media Query is met. It is extremely useful for responsive design patterns.

<Sidebar client:media="(min-width: 768px)" />

<MenuMovil client:media="(max-width: 767px)" />
Copied!

If I am on a mobile device, the Sidebar component will be rendered as static HTML (it will be visible), but its JS logic (if it’s heavy) will not be downloaded.

client:only (no SSR)

This is the exception to the rule. All the previous ones render HTML on the server (SSR) and then hydrate. client:only skips server-side rendering.

The component does not exist in the initial HTML. The browser downloads the JS and renders the component from scratch on the client.

<Contador client:only="react" />
Copied!

When to use it?

  • When the component uses APIs that only exist in the browser (window, localStorage, document) and would fail if you tried to execute it in Node.js during the build.
  • For components that depend entirely on the user’s private data (like a logged-in dashboard) and you don’t want to render anything generic.

It is mandatory to specify the framework (e.g., client:only="react" or ="vue") because Astro doesn’t know which adapter to use during the server compilation.

The hydration process

Now that we know this, let’s go back to our example. If we change the code from the previous lesson:

---
import Contador from '../components/Contador.tsx';
---

<Contador />

<Contador client:load />
Copied!

By doing this, Astro will download a small JS file containing only the React logic needed for that counter. The rest of the page remains pure HTML.

This granular architecture is what allows Astro to get 100/100 Lighthouse scores even when using heavy frameworks.

Now we have interactive islands. But a new problem arises. If I have a Counter in React in the header and a Viewer in Svelte in the footer… How do I make them share data? If I press the button in React, how does Svelte find out?

In traditional SPAs we would use Redux or Context. In Astro, since the islands live separately, we need another strategy. In the next article, we will talk about Nano Stores and shared state.