You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Resource hasValue() is false for a moment when using TransferState cached data, causing hydration to fail #62897
Which @angular/* package(s) are the source of the bug?
core
Is this a regression?
No
Description
A minimal reproduction demonstrating hydration issues when using @else and @empty blocks with the control flow syntax. The app fetches Pokemon data (httpClient + rxResource + transfer state enabled) and displays three scenarios:
Works: Uses separate @if blocks - hydration works correctly
So preliminarily, what I see is that the Resource is what's causing the problem. The hasValue check is returning false first, which in the case of no @else, the data was serialized so it can safely hydrate what's there. With the @else and the @empty, what's happening is Angular first renders the @else case and that gets hydrated, and then the hasValue() returns true, which means the @if gets rendered fresh. The same thing is happening with the @empty. So I think this is timing with the rxResource. The data does get properly serialized in the transfer state, but for some reason the Resource says false for data. So I think this is actually a resource bug, and not a hydration bug.
changed the title [-]Hydration Fails with @else and @empty Control Flow Blocks[/-][+]Resource `hasValue()` is false for a moment when using TransferState cached data, causing hydration to fail[/+]on Aug 4, 2025
We encountered the same bug, tried to reproduce it with the whole team until we opened Github and found the same issue. Are there any chances that this will be fixed before the 20.2 release? Thank you!
Hey team Angular. I understand your hard work and release schedule, but i’ll leave a comment again to make sure this issue is not lost in the list.
Thanks for your work!
Hey.
I've updated the repro with new case: https://github.com/zip-fa/ng20-new-hydration-issue
While other cases were resolved by your commit, Angular (22.0.0-next.8) still struggles to hydrate with one particular case: nested router-outlet.
^ Confirming your fix works for single router-outlet
Reproduction: navigate to /broken-wrapper. The route tree is a parent component: BrokenWrapper (which renders its own ) with a child route that renders BrokenWrapperHome — a component using rxResource + @if (hasValue()) { ... } @else { ... }. Hydration breaks (DOM is destroyed and re-created, event replay is lost).
Inlining the same child component directly into the wrapper's template (no nested ) hydrates correctly.
Switching the child route from loadComponent to eager component: does not fix it, so this isn't about the lazy-import boundary - it reproduces even when the child class is available synchronously.
Removing only the @else branch makes hydration work, matching the pattern from the original issue.
My suspicion is that the sync TransferState path relies on the resource being constructed inside the initial hydration window. Because instantiates its target imperatively through the router pipeline (which settles across at least one microtask even for eager routes), the child component's rxResource field initializer runs after that window has closed. hasValue() is therefore false on the child's first CD, Angular picks the @else branch, and that diverges from the server-rendered @if branch → hydration mismatch.
Happy to adjust the repro or test a candidate fix if helpful.
Hey. I've updated the repro with new case: https://github.com/zip-fa/ng20-new-hydration-issue While other cases were resolved by your commit, Angular (22.0.0-next.8) still struggles to hydrate with one particular case: nested router-outlet.
^ Confirming your fix works for single router-outlet
Reproduction: navigate to /broken-wrapper. The route tree is a parent component: BrokenWrapper (which renders its own ) with a child route that renders BrokenWrapperHome — a component using rxResource + @if (hasValue()) { ... } @else { ... }. Hydration breaks (DOM is destroyed and re-created, event replay is lost).
Inlining the same child component directly into the wrapper's template (no nested ) hydrates correctly.
Switching the child route from loadComponent to eager component: does not fix it, so this isn't about the lazy-import boundary - it reproduces even when the child class is available synchronously.
Removing only the @else branch makes hydration work, matching the pattern from the original issue.
My suspicion is that the sync TransferState path relies on the resource being constructed inside the initial hydration window. Because instantiates its target imperatively through the router pipeline (which settles across at least one microtask even for eager routes), the child component's rxResource field initializer runs after that window has closed. hasValue() is therefore false on the child's first CD, Angular picks the @else branch, and that diverges from the server-rendered @if branch → hydration mismatch.
Happy to adjust the repro or test a candidate fix if helpful.
Isn’t this a concern of separate issue? @JeanMeche
@alxhub hi. You closed the issue, however it still reproduces with the case stated above. Should i create a separate issue for nested router-outlet hydration bug?
What do you mean hydration id for transer state caching? Repro uses transfer state + rxjs http client which does that automatically by default.
I also stated previously that sync cached value fix helped only partially - it fixes single router-outlet case, but not the nested one, which breaks as it was in initial message of this issue
Activity
thePunderWoman commentedon Jul 30, 2025
So preliminarily, what I see is that the Resource is what's causing the problem. The
hasValuecheck is returning false first, which in the case of no@else, the data was serialized so it can safely hydrate what's there. With the@elseand the@empty, what's happening is Angular first renders the@elsecase and that gets hydrated, and then thehasValue()returns true, which means the@ifgets rendered fresh. The same thing is happening with the@empty. So I think this is timing with the rxResource. The data does get properly serialized in the transfer state, but for some reason the Resource says false for data. So I think this is actually a resource bug, and not a hydration bug.thePunderWoman commentedon Jul 30, 2025
It could also be Zoneless related with change detection timing and microtasks. I'll look into this more. @atscott may have thoughts.
[-]Hydration Fails with @else and @empty Control Flow Blocks[/-][+]Resource `hasValue()` is false for a moment when using TransferState cached data, causing hydration to fail[/+]StriplThund commentedon Aug 14, 2025
We encountered the same bug, tried to reproduce it with the whole team until we opened Github and found the same issue. Are there any chances that this will be fixed before the 20.2 release? Thank you!
popeye88 commentedon Aug 22, 2025
same here, could it be fixed please? it makes hydration with rxResource completely useless
Raume commentedon Aug 22, 2025
just encountered the same bug
it is very annoying that team ignores bug which breaks one the biggest SSR features
zip-fa commentedon Aug 26, 2025
Hey team Angular. I understand your hard work and release schedule, but i’ll leave a comment again to make sure this issue is not lost in the list.
Thanks for your work!
19 remaining items
feat(core): add ability to cache resources for SSR
feat(core): add ability to cache resources for SSR
feat(core): add ability to cache resources for SSR
Raume commentedon Mar 31, 2026
Any chances fix will land in v22?
feat(core): add ability to cache resources for SSR
zip-fa commentedon Apr 17, 2026
Hey.
I've updated the repro with new case: https://github.com/zip-fa/ng20-new-hydration-issue
While other cases were resolved by your commit, Angular (22.0.0-next.8) still struggles to hydrate with one particular case: nested router-outlet.
^ Confirming your fix works for single router-outlet
Reproduction: navigate to /broken-wrapper. The route tree is a parent component: BrokenWrapper (which renders its own ) with a child route that renders BrokenWrapperHome — a component using rxResource + @if (hasValue()) { ... } @else { ... }. Hydration breaks (DOM is destroyed and re-created, event replay is lost).
What exactly breaks:
Notable observations:
My suspicion is that the sync TransferState path relies on the resource being constructed inside the initial hydration window. Because instantiates its target imperatively through the router pipeline (which settles across at least one microtask even for eager routes), the child component's rxResource field initializer runs after that window has closed. hasValue() is therefore false on the child's first CD, Angular picks the @else branch, and that diverges from the server-rendered @if branch → hydration mismatch.
Happy to adjust the repro or test a candidate fix if helpful.
Raume commentedon Apr 21, 2026
Isn’t this a concern of separate issue? @JeanMeche
feat(core): add ability to cache resources for SSR
zip-fa commentedon May 7, 2026
@alxhub hi. You closed the issue, however it still reproduces with the case stated above. Should i create a separate issue for nested router-outlet hydration bug?
JeanMeche commentedon May 7, 2026
Which version did you tried this on ? Did you enable the hydration id, for transfer state caching ?
zip-fa commentedon May 8, 2026
https://github.com/zip-fa/ng20-new-hydration-issue
Repro is on next.11
What do you mean hydration id for transer state caching? Repro uses transfer state + rxjs http client which does that automatically by default.
I also stated previously that sync cached value fix helped only partially - it fixes single router-outlet case, but not the nested one, which breaks as it was in initial message of this issue
zip-fa commentedon May 11, 2026
Do you need any other context?
angular-automatic-lock-bot commentedon Jun 11, 2026
This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.
Read more about our automatic conversation locking policy.
This action has been performed automatically by a bot.