In Angular incremental hydration, why does a nested @defer block's hydrate when condition never fire while its parent block is dehydrated?
answer
- parents before children
- the condition lives in the parent
- top-most dehydrated block only
- never freezes the subtree
basics
~20 sHydration runs from the top-most dehydrated @defer block down, and a hydrate when condition belongs to the parent's template, which is not live while dehydrated. So hydrate when works only on the top-most dehydrated block.
solid answer
~40 sBecause a component cannot be active before its ancestors, Angular hydrates nested `@defer` blocks from the top-most dehydrated block down to the one whose trigger fired. A `hydrate when` expression is evaluated in the parent component's template context; if that context sits inside a dehydrated block, its bindings are not live and Angular cannot evaluate the condition. The guide therefore limits `hydrate when` to the top-most dehydrated block. Fixes are to move the condition to the outer block, to use an event-based trigger such as `hydrate on interaction` on the inner block, or to accept that it hydrates after its parent. A related rule: `hydrate never` freezes its entire subtree, so no nested hydrate trigger fires, and it cannot be combined with other hydrate triggers.
code
html · 11 lines<!-- Condition moved to the outer, top-most block -->
@defer (hydrate on viewport; hydrate when reviewsExpanded()) {
<app-reviews />
@defer (hydrate on interaction) {
<app-review-filters />
} @placeholder {
<span>Filters</span>
}
} @placeholder {
<p>Reviews</p>
}go deeper
Recall that parent blocks hydrate before their nested blocks and that hydrate never keeps a whole subtree static on the first load.
Explain why hydrate when needs a live parent context and so only works on the top-most dehydrated block.
Diagnose triggers that never fire by mapping block nesting, and restructure conditions or triggers so each fires where it can be evaluated.
Keep hydration boundary nesting shallow and documented so teams can predict what a trigger will load and when.
## The symptom A product page has nested incremental hydration boundaries: ```html @defer (hydrate on viewport) { <app-reviews /> @defer (hydrate when reviewsExpanded()) { <app-review-filters /> } @placeholder { <span>Filters</span> } } @placeholder { <p>Reviews</p> } ``` `reviewsExpanded()` becomes `true` while the outer block is still dehydrated, yet the filters never hydrate at that moment. In another template, a widget inside a `hydrate never` block ignores its own `hydrate on interaction`. ## Rule 1: hydration runs from the top-most dehydrated block down Angular's component tree and injector hierarchy mean a child cannot be active while its parent is not. When any trigger fires for a nested block, Angular hydrates starting from the **top-most dehydrated `@defer` block** and works down to the one that was triggered, in that order. Hovering an inner `hydrate on hover` block inside a dehydrated `hydrate on interaction` block hydrates the outer block first, then the inner one. Consequences: - An inner block cannot be cheaper than its dehydrated parents; triggering it pays for them too. - Keep expensive components in outer blocks only if they are needed whenever inner blocks are. ## Rule 2: hydrate when works only on the top-most dehydrated block `hydrate when` evaluates an expression that belongs to the **parent component's template context**. If that parent is itself inside a dehydrated block, its bindings are not live, so Angular cannot evaluate the condition. The guide states the rule directly: `hydrate when` conditions only trigger when their block is the top-most dehydrated `@defer` block. In the example, `hydrate when reviewsExpanded()` cannot fire while the outer reviews block is dehydrated. Options: 1. Move the condition to the outer block, for example `@defer (hydrate on viewport; hydrate when reviewsExpanded())`. 2. Use an event-based trigger on the inner block (`hydrate on interaction`) that does not need the parent's state. 3. Accept that the inner block hydrates only after the outer one has. ## Rule 3: hydrate never freezes the whole subtree `hydrate never` keeps its block dehydrated for the initial load, turning it into static content, and it **prevents hydration of the entire nested subtree**. No `hydrate` trigger underneath it fires. Two compile-time and design consequences: - `hydrate never` cannot be combined with other hydrate triggers on the same block; the compiler rejects it. - Interactive widgets must not live inside a `hydrate never` block if they need to work on the first load. On a later client-side render, the block's regular triggers apply as usual, so the subtree does load then. ## A diagnosis routine 1. Draw the nesting of `@defer` blocks with their hydrate triggers. 2. For each inner `hydrate when`, check whether any ancestor block is still dehydrated when the condition turns true. 3. For each `hydrate never`, list what lives inside it and remove anything interactive. 4. Watch the network panel: an inner trigger that fetches several chunks is hydrating its ancestors first. ## Why the rules exist All three rules follow from one fact: a dehydrated block is **server HTML that Angular has not attached to its component tree yet**. Nothing inside it has live bindings, injectors or listeners. So a child cannot hydrate before its parent (it would have no parent injector), a condition written in a dehydrated template cannot be evaluated (it has no live context), and a block marked `hydrate never` has no way to activate anything beneath it. Seeing the rules this way makes new cases predictable instead of memorised. ## Summary table | Construct | Rule | Typical bug | | --- | --- | --- | | Nested dehydrated blocks | Hydrate outer to inner | Inner trigger pulls large parent code | | `hydrate when` | Only on the top-most dehydrated block | Condition becomes true but nothing happens | | `hydrate never` | Freezes the whole subtree on first load | Nested widget never becomes interactive |
- What happens when a user hovers an inner hydrate on hover block whose parent is still dehydrated?Angular hydrates the outer block first and then the inner one, in that order. The inner trigger therefore also pays for fetching and hydrating the parent block's code.
- Can a block declare hydrate never together with hydrate on viewport?No. The compiler rejects additional `hydrate` triggers when `hydrate never` is present. Regular triggers such as `on viewport` are still allowed, because they govern later client-side renders.
saying these in an interview costs you the question
- Inner @defer blocks can hydrate before their dehydrated parents
- hydrate when is evaluated inside the dehydrated child block itself
- A nested hydrate trigger overrides a parent's hydrate never
- hydrate never can be combined with other hydrate triggers
- Nested blocks hydrate in parallel in no particular order