skip to content

How do you write a custom Vue 3.5 HydrationStrategy that hydrates when the browser is idle or on the first click, whichever comes first?

level: seniorimportance: nice to knowfreq 15%

answer

  1. two arguments in, teardown out
  2. roots may be a fragment
  3. compose the built-in factories
  4. hydrate exactly once

basics

~10 s

A HydrationStrategy receives hydrate and forEachElement and may return a teardown. Compose hydrateOnIdle() and hydrateOnInteraction('click') by calling each with a guarded hydrate-once callback, and return a teardown that stops both.

solid answer

~40 s

A `HydrationStrategy` is a function `(hydrate, forEachElement) => teardown | void`. `hydrate` performs the component's hydration; `forEachElement` iterates its server-rendered root elements, which may be several because the root can be a fragment; the returned teardown runs if the component unmounts before hydrating. Because the built-ins are factories returning strategies, I compose them: call `hydrateOnIdle()(hydrateOnce, forEachElement)` and `hydrateOnInteraction('click')(hydrateOnce, forEachElement)`, where `hydrateOnce` sets a flag, stops the other trigger and calls `hydrate`. The guard matters: Vue's hydrate callback does not ignore repeat calls, so the strategy must fire it once. Clicks get the interaction strategy's replay for free, and I return a teardown that stops both.

code

ts · 36 lines
ts
import {
  defineAsyncComponent,
  hydrateOnIdle,
  hydrateOnInteraction,
  type HydrationStrategy,
} from 'vue'

// Hydrate when the browser is idle, or earlier on the first click.
const idleOrClick: HydrationStrategy = (hydrate, forEachElement) => {
  let done = false
  let stopIdle: (() => void) | void
  let stopClick: (() => void) | void

  const hydrateOnce = () => {
    if (done) return
    done = true
    stopIdle?.()
    stopClick?.()
    hydrate()
  }

  // Neither trigger calls back synchronously, so both can be wired up front.
  stopIdle = hydrateOnIdle(8000)(hydrateOnce, forEachElement)
  stopClick = hydrateOnInteraction('click')(hydrateOnce, forEachElement)

  // Runs if the component unmounts before it hydrates.
  return () => {
    stopIdle?.()
    stopClick?.()
  }
}

export const CommentsWidget = defineAsyncComponent({
  loader: () => import('./CommentsWidget.vue'),
  hydrate: idleOrClick,
})

go deeper

for a junior

Recall that a custom strategy is a function that calls hydrate when it decides to, and may return a teardown.

for a middle

Explain both arguments, why roots can be several elements, and when the teardown runs.

for a senior

Compose built-ins safely: a hydrate-once guard, teardown on every path, and care with triggers that fire synchronously.

for a principal

Decide whether a custom strategy is worth owning, or whether the built-ins plus a clear page policy already cover the need.

## The contract In Vue 3.5 a lazy hydration strategy is a plain function with the exported type `HydrationStrategy`: - **`hydrate`**: call it to hydrate the component now. - **`forEachElement(cb)`**: iterates the component's server-rendered **root elements**. A component's root can be a fragment of several elements (marked in the HTML by comment anchors), so the helper walks all of them; returning `false` from `cb` stops the walk. - **Return value**: optionally a **teardown** function. Vue registers it to run before the component unmounts, which covers the case of a component that is removed before it ever hydrates - the place to remove listeners, observers and timers. The built-ins are **factories**: `hydrateOnIdle(timeout)` returns a `HydrationStrategy`. That makes them composable: call the factory, then call the returned strategy with your own `hydrate` callback. ## Building 'idle or first click' 1. **Guard the callback.** Write `hydrateOnce`, which returns if it already ran, sets a flag, tears down both triggers and calls `hydrate`. 2. **Start the idle trigger**: `hydrateOnIdle()(hydrateOnce, forEachElement)`. It schedules an idle callback and returns a teardown that cancels it. 3. **Start the click trigger**: `hydrateOnInteraction('click')(hydrateOnce, forEachElement)`. It adds click listeners to the root elements, and on the first click calls `hydrateOnce` and then replays the click. 4. **Return a teardown** that stops both, for the unmount-before-hydration case. ## Why the guard is not optional | Without the guard | Consequence | |---|---| | idle fires, then the user clicks | the interaction strategy calls `hydrate` a second time | | the click fires, then the idle callback runs | the idle strategy calls `hydrate` a second time | Vue's internal wrapper around `hydrate` skips hydration only in specific cases - the component was already updated, or its DOM was detached - and does **not** ignore a repeated call. Tearing down the other trigger inside `hydrateOnce` is the tidy way to keep it to one. ## Why compose rather than hand-roll - The interaction factory already **replays** the triggering event after hydration, so the first click is not lost. - It already iterates **fragment roots** through `forEachElement`. - The idle factory already has the **timeout and the fallback** for browsers without `requestIdleCallback`. ## Testing a custom strategy A strategy is a plain function, so it can be tested without rendering a page: 1. Create a DOM element to stand in for the component's root and a `forEachElement` that calls back with it. 2. Pass a spy as `hydrate`. 3. Dispatch a click on the element and assert the spy ran **once**, then let the idle timer run and assert it is **still** once. 4. Repeat in the other order: idle first, then a click. 5. Call the returned teardown before either trigger and assert that neither a click nor the idle timer calls the spy afterwards. Those five checks cover the guard, both trigger orders and the unmount path - the three places composed strategies usually break. ## Traps when composing - **Synchronous triggers.** `hydrateOnVisible` and `hydrateOnMediaQuery` can call `hydrate` synchronously while being set up, if the root is already visible or the query already matches. If such a trigger is set up first, check the flag before wiring the next one; otherwise the second one attaches listeners to an already hydrated component, and an interaction strategy would replay a duplicate event. - **Teardown on both paths.** The function you return must stop everything you started, whether or not hydration happened. - **Events that do not bubble** will not reach root listeners from inside the component; prefer `click` or `focusin` over `focus`. The docs call this API intentionally low-level: a custom strategy is ordinary DOM code around one callback, and its correctness is yours to test.

  • Why does forEachElement exist instead of the strategy receiving one root element?
    A Vue component can render a fragment, so its server HTML may have several root elements between comment anchors. `forEachElement` walks all of them, which lets a strategy observe or listen on every root; returning `false` from the callback stops the walk early.
  • What changes if you swap hydrateOnIdle for hydrateOnVisible in this composition?
    `hydrateOnVisible` may call back synchronously when a root is already on screen, before the click trigger exists. Check the flag before wiring the interaction strategy; otherwise it attaches click listeners to a hydrated component, and a later click would be replayed as a duplicate.

saying these in an interview costs you the question

  • Vue ignores repeated hydrate calls, so combined triggers need no guard.
  • A strategy receives the component instance and its props.
  • The teardown is optional because Vue removes a strategy's listeners itself.
  • A strategy must call hydrate synchronously before returning.
  • The built-in strategies cannot be combined with custom code.