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 87bc2f3

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded 8m 51s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 2m 19s View ↗

☁️ Nx Cloud last updated this comment at 2026-08-28 22:34:00 UTC

@brenelz

brenelz commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

@birkskyum thoughts?

@pkg-pr-new

pkg-pr-new Bot commented Aug 28, 2026

Copy link
Copy Markdown
More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@8179

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@8179

@tanstack/eslint-plugin-start

npm i https://pkg.pr.new/@tanstack/eslint-plugin-start@8179

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@8179

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@8179

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@8179

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@8179

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@8179

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@8179

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@8179

@tanstack/react-start-rsc

npm i https://pkg.pr.new/@tanstack/react-start-rsc@8179

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@8179

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@8179

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@8179

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@8179

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@8179

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@8179

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@8179

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@8179

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@8179

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@8179

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@8179

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@8179

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@8179

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@8179

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@8179

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@8179

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@8179

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@8179

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@8179

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@8179

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@8179

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@8179

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@8179

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@8179

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@8179

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@8179

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@8179

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@8179

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@8179

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@8179

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@8179

commit: 87bc2f3

@codspeed-hq

codspeed-hq Bot commented Aug 28, 2026

Copy link
Copy Markdown

Hooray! CodSpeed harness just leveled up!

The base and head of this comparison were measured with different runner settings, so their benchmark values are not directly comparable.

What changed between base and head:

Re-run the base with the same settings to get a valid performance comparison.


Comparing russelgal:solid-link-idle-preload-listeners (87bc2f3) with solid-router-v2-pre (67a9040)1

Open in CodSpeed

Footnotes

  1. No successful run was found on solid-router-v2-pre (5f65783) during the generation of this report, so 67a9040 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

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.

2 participants