Skip to content

Event replay: early click leaves stale __jsaction cache, making prerendered target="_blank" links permanently unclickable #71545

Open

Description

@ParthRorix

Which @angular/* package(s) are the source of the bug?

Don't known / other

Is this a regression?

Yes

Description

Angular issue: an early click makes a prerendered target="_blank" link permanently unclickable (stale __jsaction cache)

Package: @angular/core (event replay / incremental hydration)
Version: 22.2.3 (reproduced on a fresh ng new --ssr project; first seen after upgrading an app from 19 to 22)
Browser: Chrome (stable)

Description

With the v22 default provideClientHydration() (incremental hydration, which brings event replay), a prerendered
<a routerLink="..." target="_blank"> that is clicked while event replay is still active becomes permanently
unclickable
. Every later click is preventDefault()ed by the event contract, even after Angular has removed the
jsaction attribute. No new tab opens until the page is reloaded.

Clicking only after the app is stable works. Using withNoIncrementalHydration(), withEventReplay() also works.

Minimal reproduction

ng new repro --ssr --routing --defaults (Angular 22.2.3), then replace three files:

src/app/app.ts

import { Component, PendingTasks, inject } from '@angular/core';
import { RouterLink, RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  imports: [RouterLink, RouterOutlet],
  template: `
    <a id="new-tab" routerLink="/other" target="_blank">Open /other in a new tab</a>
    <router-outlet />
  `,
})
export class App {
  constructor() {
    // Stand-in for an HTTP request in flight after load: keeps the app unstable for 3 s.
    inject(PendingTasks).run(() => new Promise<void>((resolve) => setTimeout(resolve, 3000)));
  }
}

src/app/other.ts

import { Component } from '@angular/core';

@Component({ selector: 'app-other', template: `<p>Other page</p>` })
export class Other {}

src/app/app.routes.ts

import { Component } from '@angular/core';
import { Routes } from '@angular/router';
import { Other } from './other';

@Component({ selector: 'app-home', template: `<p>Home page</p>` })
class Home {}

export const routes: Routes = [
  { path: '', component: Home },
  { path: 'other', component: Other },
];

app.config.ts stays as generated: provideClientHydration().

Build (ng build, prerender), serve dist/repro/browser statically, open /.

Steps

  1. Within the first ~3 s (app bootstrapped, not yet stable), click "Open /other in a new tab".
  2. Wait until the app is stable; the jsaction attribute disappears from the link.
  3. Click the link again.

Expected: step 3 opens /other in a new tab.
Actual: nothing happens. The click is preventDefault()ed by the event contract; this repeats on every click until reload.

Control cases (same app, real trusted clicks):

Configuration Early click, then click at 6 s Clicks only at 6 s and 8 s
provideClientHydration() (v22 default) 6 s click: no tab (defaultPrevented=true) both open a tab
provideClientHydration(withNoIncrementalHydration(), withEventReplay()) both open a tab both open a tab

Stack of the preventDefault() on the 6 s click (dev build): Dispatcher.dispatch → EventDispatcher.dispatch →
_EventContract.dispatcher → _EventContract.handleEventInfo → _EventContract.handleEvent → listener on <body>.

Root cause (from the 22.2.3 sources)

  1. On the early click, ActionResolver.parseActions(element) parses jsaction="click:;" and caches the parsed map
    on the element as the property element.__jsaction (set(actionElement, actionMap)).
  2. When the block/app is cleaned up, removeListeners(el) does
    el.removeAttribute('jsaction'); el.removeAttribute('ngb'); el.__jsaction_fns = undefined;
    but it does not clear el.__jsaction.
  3. With incremental hydration enabled, the EventContract is not cleaned up after whenStable(). withEventReplay's
    bootstrap listener only calls eventContract.cleanUp() on app destroy when isIncrementalHydrationEnabled, so the
    <body> listener stays active.
  4. On every later click, parseActions returns the stale cached map, so an action is found. Because the element is an
    <a> and the event is a click, shouldPreventDefaultBeforeDispatching returns true and the default (opening the new
    tab) is cancelled. RouterLink then leaves target="_blank" to the browser, so nothing navigates.

Without the early click, __jsaction was never cached; after the attribute is removed parseActions sees no attribute,
and the click is not prevented. With incremental hydration off, the contract is cleaned up at stable, so the stale cache
is never consulted.

Same-tab routerLinks mask the problem (RouterLink navigates itself after the default is prevented); any anchor that
relies on the browser default (target="_blank", plain href with a (click) listener, download links) is affected.

Workaround we use

An inline script at the end of index.html removes jsaction from a[target="_blank"] before any click, so these
links are never intercepted or cached:

<script>
  document.querySelectorAll('a[target="_blank"][jsaction]').forEach(function (a) { a.removeAttribute('jsaction'); });
</script>

Please provide a link to a minimal reproduction of the bug

No response

Please provide the exception or error you saw


Please provide the environment you discovered this bug in (run ng version)

Angular CLI       : 22.2.3
Angular           : 22.2.3
Node.js           : 24.21.0
Package Manager   : npm 11.19.0
Operating System  : darwin arm64

Anything else?

No response

Activity

  1. added this to the needsTriage milestone on Oct 22, 2026
  2. added this to the needsTriage milestone on Oct 22, 2026
  3. added a commit that references this issue on Nov 7, 2026
    93b0344
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: coreIssues related to the framework runtime

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      Sponsor
      SponsoredKunjungi sekarang
      Promo