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?
answer
- two arguments in, teardown out
- roots may be a fragment
- compose the built-in factories
- hydrate exactly once
basics
~10 sA 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 sA `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 linesimport {
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
Recall that a custom strategy is a function that calls hydrate when it decides to, and may return a teardown.
Explain both arguments, why roots can be several elements, and when the teardown runs.
Compose built-ins safely: a hydrate-once guard, teardown on every path, and care with triggers that fire synchronously.
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.