Skip to content

fix(solid-router): don't install intent-preload listeners when preloading is off - #8179

Open
russelgal wants to merge 2 commits into
TanStack:solid-router-v2-prefrom
russelgal:solid-link-idle-preload-listeners
Open

fix(solid-router): don't install intent-preload listeners when preloading is off#8179
russelgal wants to merge 2 commits into
TanStack:solid-router-v2-prefrom
russelgal:solid-link-idle-preload-listeners

Conversation

@russelgal

Copy link
Copy Markdown

Problem

useLinkProps assigns the preload handlers unconditionally (link.tsx):

linkProps.onClick = onClick
linkProps.onBlur = onBlur
linkProps.onFocus = onFocus
linkProps.onMouseEnter = onMouseEnter
linkProps.onMouseOver = onMouseOver
linkProps.onMouseLeave = onMouseLeave
linkProps.onMouseOut = onMouseOut
linkProps.onTouchStart = onTouchStart

The preload check lives inside the handlers (if (preload() !== 'intent') return), so with preloading off they are still attached and still fire — they just return.

That is not free. Solid's DelegatedEvents set does not include mouseenter, mouseleave, focus or blur (they don't bubble), so assignProp installs a real addEventListener per anchor for each of them. click, mouseover, mouseout and touchstart are delegated and cost nothing.

Net effect: four listeners per <Link> that do nothing at all whenever preload is false, 'viewport' or 'render'.

I hit this on a booking board where every row is a link. Measured in Chrome on a real page, counting addEventListener calls for one client-side navigation onto the screen:

listeners
165 rows rendered with <Link preload={false}> 660 (blur/focus/mouseenter/mouseleave × 165)
same rows, handlers not attached 0

Fix

Resolve those props through getters (the file already relies on this: "values that no longer apply resolve to undefined, which spread()/assign() treats as attribute removal"). With intent preloading off, the property yields whatever the consumer passed — or undefined, and nothing gets attached:

const onIntent =
  (composed: (event: any) => void, user: () => unknown) => () =>
    preload() === 'intent' ? composed : user()

Behaviour is unchanged:

  • preload: 'intent' — identical to before.
  • 'viewport' — preloading runs through the IntersectionObserver; the events were already no-ops there.
  • 'render' — preloading runs in an effect; same.
  • A consumer's own onMouseEnter/onFocus/… still fires, because the getter falls back to it.
  • Still reactive: flipping preload back to 'intent' re-runs the consuming spread(), which attaches the composed handler then (and assignProp removes a stale non-delegated listener before adding).

Tests

Added to tests/link.test.tsx, alongside the existing preload/IntersectionObserver ones:

  • Router.preload="false" | "viewport" | "render" — no mouseenter/mouseleave/focus/blur listeners on the anchor (spying on HTMLAnchorElement.prototype.addEventListener).
  • Router.preload="intent" — the listeners are installed, as before.
  • Link.preload={false} with the consumer's own onMouseEnter/onFocus — both still fire.

Verified the new tests fail without the fix (3 of them) and pass with it.

Ran locally on solid-router-v2-pre:

  • eslint src/link.tsx tests/link.test.tsx — clean
  • tsc -p tsconfig.legacy.json — clean
  • vitest run — 59 files, 870 passed / 2 skipped
  • vitest run --mode server — 3 files, 4 passed

Changeset included (patch).

…ding is off

useLinkProps handed out onFocus/onBlur/onMouseEnter/onMouseLeave (plus the
mouseover/mouseout/touchstart trio) unconditionally, with the
`preload() !== 'intent'` check inside each handler. Solid does not delegate
mouseenter, mouseleave, focus or blur, so every anchor installed four real
listeners whose only job was to bail out — four per row on list views.

Resolve those props through getters instead: with intent preloading off the
property yields the consumer's own handler (or undefined, which
spread()/assign() treats as removal), so nothing is attached. The getters stay
reactive, so switching preload back to 'intent' re-runs the consuming spread
and attaches the composed handler.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5a8bd926-8d19-4fde-a031-48dd44336a97

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nx-cloud

nx-cloud Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit a5eda23


☁️ Nx Cloud last updated this comment at 2026-08-27 17:10:32 UTC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant