Skip to content

Comments

fix: delay focus intent preload and clear on blur across adapters#6744

Merged
Sheraff merged 9 commits intomainfrom
codex/preload-focus-delay
Feb 23, 2026
Merged

fix: delay focus intent preload and clear on blur across adapters#6744
Sheraff merged 9 commits intomainfrom
codex/preload-focus-delay

Conversation

@Sheraff
Copy link
Contributor

@Sheraff Sheraff commented Feb 23, 2026

Summary

  • update Link preload behavior in React/Solid/Vue adapters so focus uses the same delayed intent-preload handler as hover/enter
  • add blur handling to clear pending intent-preload timeouts (same cleanup path as mouse leave)
  • keep touchstart as immediate preload (no delay)
  • remove the extra handleFocus alias and wire onFocus directly to the shared enter handler
  • adjust one existing React perf assertion in store-updates-during-navigation to the new baseline after delayed-focus behavior (16 -> 15) and avoid the exact-delay race (50ms -> 100ms wait)

Why

Keyboard navigation across many Link elements with preload="intent" was triggering immediate preloads on focus, which can cause dropped frames/lag. Focus now follows the same intent delay semantics as hover, and blur now cancels pending intent preloads.

Testing

  • CI=1 NX_DAEMON=false pnpm nx run-many --target=test:unit --projects=@tanstack/react-router,@tanstack/solid-router,@tanstack/vue-router --outputStyle=stream --skipRemoteCache
  • CI=1 NX_DAEMON=false pnpm nx run-many --target=test:types --projects=@tanstack/react-router,@tanstack/solid-router,@tanstack/vue-router --outputStyle=stream --skipRemoteCache
  • CI=1 NX_DAEMON=false pnpm nx run-many --target=test:eslint --projects=@tanstack/react-router,@tanstack/solid-router,@tanstack/vue-router --outputStyle=stream --skipRemoteCache

Summary by CodeRabbit

  • New Features

    • Link components now accept an onBlur handler across router implementations.
    • Preload behavior refined for focus/hover interactions with intent-based timeout scheduling.
  • Tests

    • Adjusted timing and expectations in a navigation preload test to account for the new preload timing.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 23, 2026

📝 Walkthrough

Walkthrough

This PR adds onBlur prop support to link components and refactors preload handling across React, Solid, and Vue router packages. A new enqueueIntentPreload mechanism replaces previous focus/enter logic with centralized timeout-based preload scheduling, using currentTarget for event tracking via WeakMap, and onBlur now clears pending preload timeouts.

Changes

Cohort / File(s) Summary
Link Preload & Event Handling (React, Solid, Vue)
packages/react-router/src/link.tsx, packages/solid-router/src/link.tsx, packages/vue-router/src/link.tsx
Added onBlur to public prop surface and prop spreads. Introduced enqueueIntentPreload to schedule preload via timeout (respecting preload and disabled). Replaced previous handleFocus/handleEnter with centralized timeout logic using currentTarget and a WeakMap for per-target timeouts. handleLeave/onBlur now clear pending timeouts; touchstart triggers preload directly when appropriate.
Tests
packages/react-router/tests/store-updates-during-navigation.test.tsx
Adjusted timing and expectations: increased wait after focus from 50ms→100ms and reduced expected update count from 16→15 in a hover-preload navigation test.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • schiller-manuel
  • birkskyum
  • brenelz

Poem

🐰 I nibble code at break of dawn,
I queue the preload, then I'm gone,
Focus hops, timeouts sway,
Blur clears trails along the way,
Links gleam bright — a rabbit's fond yawn.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: delay focus intent preload and clear on blur across adapters' accurately and specifically describes the main change: implementing delayed preload on focus with blur cleanup across multiple router adapters.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/preload-focus-delay

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

@nx-cloud
Copy link

nx-cloud bot commented Feb 23, 2026

View your CI Pipeline Execution ↗ for commit 89db2d8

Command Status Duration Result
nx run tanstack-router-e2e-bundle-size:build --... ✅ Succeeded 1m 24s View ↗

☁️ Nx Cloud last updated this comment at 2026-02-23 21:04:27 UTC

@github-actions
Copy link

github-actions bot commented Feb 23, 2026

Bundle Size Benchmarks

  • Commit: fb051122b0d3
  • Measured at: 2026-02-23T20:50:05.860Z
  • Baseline source: history:851967c8b0bc
  • Dashboard: bundle-size history
Scenario Current (gzip) Delta vs baseline Raw Brotli Trend
react-router.minimal 86.53 KiB +5 B (+0.01%) 272.15 KiB 75.17 KiB ▇▇▇▇██▆▆▆▁▁▁
react-router.full 89.55 KiB -6 B (-0.01%) 282.49 KiB 77.88 KiB ██████▇▆▆▁▁▁
solid-router.minimal 35.80 KiB +8 B (+0.02%) 107.27 KiB 32.12 KiB ██████▆▆▆▁▁▁
solid-router.full 40.15 KiB +23 B (+0.06%) 120.32 KiB 36.10 KiB ██████▇▆▆▁▁▂
vue-router.minimal 51.66 KiB +18 B (+0.03%) 147.25 KiB 46.49 KiB ████▇▇▆▆▆▁▁▁
vue-router.full 56.49 KiB +12 B (+0.02%) 162.83 KiB 50.70 KiB ████▇▇▆▆▆▁▁▁
react-start.minimal 99.08 KiB +5 B (+0.00%) 311.29 KiB 85.71 KiB ▇▇▇▇▇▇▆▆█▁▁▁
react-start.full 102.42 KiB +7 B (+0.01%) 321.06 KiB 88.47 KiB ██████▆▅▆▁▁▁
solid-start.minimal 48.12 KiB +20 B (+0.04%) 144.84 KiB 42.55 KiB ████▇▇▆▅█▁▁▂
solid-start.full 53.58 KiB +11 B (+0.02%) 160.76 KiB 47.34 KiB ██████▆▆█▁▁▁

Trend sparkline is historical gzip bytes ending with this PR measurement; lower is better.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 23, 2026

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/arktype-adapter@6744

@tanstack/eslint-plugin-router

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

@tanstack/history

npm i https://pkg.pr.new/TanStack/router/@tanstack/history@6744

@tanstack/nitro-v2-vite-plugin

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start@6744

@tanstack/react-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-client@6744

@tanstack/react-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-server@6744

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-router-ssr-query

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

@tanstack/solid-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start@6744

@tanstack/solid-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-client@6744

@tanstack/solid-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-server@6744

@tanstack/start-client-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-client-core@6744

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-fn-stubs@6744

@tanstack/start-plugin-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-plugin-core@6744

@tanstack/start-server-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-core@6744

@tanstack/start-static-server-functions

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

@tanstack/start-storage-context

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-storage-context@6744

@tanstack/valibot-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/valibot-adapter@6744

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/TanStack/router/@tanstack/virtual-file-routes@6744

@tanstack/vue-router

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

@tanstack/vue-router-devtools

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

@tanstack/vue-router-ssr-query

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

@tanstack/vue-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-start@6744

@tanstack/vue-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-start-client@6744

@tanstack/vue-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-start-server@6744

@tanstack/zod-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/zod-adapter@6744

commit: 89db2d8

@Sheraff Sheraff changed the title fix: apply preloadDelay to link focus preloading across adapters fix: delay focus intent preload and clear on blur across adapters Feb 23, 2026
@Sheraff Sheraff marked this pull request as ready for review February 23, 2026 20:21
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
packages/react-router/src/link.tsx (2)

727-727: Module-level timeoutMap — verify cleanup on unmount.

Since timeoutMap is module-scoped, a pending timeout will survive component unmount and fire doPreload from a stale closure. This is likely benign (just a wasted network request), but if strict cleanup is desired, consider clearing the element's entry in a useEffect cleanup.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-router/src/link.tsx` at line 727, Module-level timeoutMap can
leave timers firing after a Link unmount and calling doPreload from a stale
closure; modify the Link-related component (where timeoutMap and doPreload are
used) to clear and delete the element's timeout in a useEffect cleanup: when you
set a timeout for an EventTarget put the timer id into timeoutMap, and in the
component's cleanup clearTimeout(timeoutId) and timeoutMap.delete(element) (also
ensure any event handlers that call doPreload check element presence or
cancelation state) so pending timers are properly cleaned up on unmount.

663-697: enqueueIntentPreload + handleLeave look correct and well-structured.

The WeakMap approach avoids polluting DOM elements and naturally GCs when elements are removed. The early-return at line 666 for zero delay is a nice optimization over the Solid/Vue implementations.

One very minor note: if preloadDelay is non-zero when enqueueIntentPreload fires but becomes zero by the time handleLeave runs (e.g. router config changes dynamically), the guard at line 690 (!preloadDelay) would skip cleanup, leaving a stale timeout in the map. This is extremely unlikely in practice, but for robustness you could drop the !preloadDelay guard from handleLeave:

Suggested tightening of handleLeave guard
  const handleLeave = (e: React.MouseEvent | React.FocusEvent) => {
-   if (disabled || !preload || !preloadDelay) return
+   if (disabled || !preload) return
    const eventTarget = e.currentTarget
    const id = timeoutMap.get(eventTarget)
    if (id) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-router/src/link.tsx` around lines 663 - 697, The cleanup in
handleLeave can miss clearing timeouts if preloadDelay changes to zero between
enqueueIntentPreload and handleLeave; update handleLeave to always attempt
cleanup regardless of preloadDelay by removing the "!preloadDelay" guard so it
checks only disabled and preload and then looks up the timer in timeoutMap (use
the existing timeoutMap.get/clearTimeout/timeoutMap.delete logic), referencing
the existing enqueueIntentPreload, handleLeave, timeoutMap, preloadDelay, and
doPreload symbols.
packages/solid-router/src/link.tsx (1)

351-363: Solid's enqueueIntentPreload always uses setTimeout, unlike React's zero-delay fast path.

In React, when preloadDelay is 0, doPreload() fires synchronously and no timeout is set. Here in Solid, setTimeout(() => …, 0) always creates a macrotask, meaning a synchronous blur after focus can cancel a zero-delay preload. This is arguably better for the PR's goal (avoiding unnecessary preloads during fast keyboard traversal), but it's a subtle cross-adapter inconsistency.

Not necessarily something to change — just flagging for awareness.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/solid-router/src/link.tsx` around lines 351 - 363,
enqueueIntentPreload currently always uses setTimeout even when preloadDelay()
=== 0, causing zero-delay preloads to run async; update enqueueIntentPreload so
that when preloadDelay() === 0 it calls doPreload() synchronously (without
creating/preparing eventTarget.preloadTimeout), otherwise keep the existing
setTimeout behavior using eventTarget.preloadTimeout and clearing it to null
after firing; reference enqueueIntentPreload, eventTarget.preloadTimeout,
preloadDelay(), and doPreload() when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/react-router/src/link.tsx`:
- Line 727: Module-level timeoutMap can leave timers firing after a Link unmount
and calling doPreload from a stale closure; modify the Link-related component
(where timeoutMap and doPreload are used) to clear and delete the element's
timeout in a useEffect cleanup: when you set a timeout for an EventTarget put
the timer id into timeoutMap, and in the component's cleanup
clearTimeout(timeoutId) and timeoutMap.delete(element) (also ensure any event
handlers that call doPreload check element presence or cancelation state) so
pending timers are properly cleaned up on unmount.
- Around line 663-697: The cleanup in handleLeave can miss clearing timeouts if
preloadDelay changes to zero between enqueueIntentPreload and handleLeave;
update handleLeave to always attempt cleanup regardless of preloadDelay by
removing the "!preloadDelay" guard so it checks only disabled and preload and
then looks up the timer in timeoutMap (use the existing
timeoutMap.get/clearTimeout/timeoutMap.delete logic), referencing the existing
enqueueIntentPreload, handleLeave, timeoutMap, preloadDelay, and doPreload
symbols.

In `@packages/solid-router/src/link.tsx`:
- Around line 351-363: enqueueIntentPreload currently always uses setTimeout
even when preloadDelay() === 0, causing zero-delay preloads to run async; update
enqueueIntentPreload so that when preloadDelay() === 0 it calls doPreload()
synchronously (without creating/preparing eventTarget.preloadTimeout), otherwise
keep the existing setTimeout behavior using eventTarget.preloadTimeout and
clearing it to null after firing; reference enqueueIntentPreload,
eventTarget.preloadTimeout, preloadDelay(), and doPreload() when making the
change.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c137bbc and d277c0b.

📒 Files selected for processing (3)
  • packages/react-router/src/link.tsx
  • packages/solid-router/src/link.tsx
  • packages/vue-router/src/link.tsx

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/react-router/tests/store-updates-during-navigation.test.tsx (1)

215-216: Document the implicit dependency on the default preloadDelay.

The 100 is meaningful: it must be greater than the router's default preloadDelay (50 ms) to avoid re-introducing the exact-delay race this change is fixing. Without a comment, future readers won't know why this value was chosen or that the assertion on line 228 will silently break if the default delay changes.

✏️ Suggested annotation
     fireEvent.focus(link)
-    await new Promise((resolve) => setTimeout(resolve, 100))
+    // Wait longer than defaultPreloadDelay (50 ms) so the intent-preload
+    // timer fires before the click; previously 50 ms caused an exact-delay race.
+    await new Promise((resolve) => setTimeout(resolve, 100))
     fireEvent.click(link)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-router/tests/store-updates-during-navigation.test.tsx` around
lines 215 - 216, Add a short comment immediately above the await new
Promise(...) after fireEvent.focus(link) that explains the magic 100ms value: it
must be greater than the router's default preloadDelay (50ms) to avoid
re-introducing the race this test guards against, and the later expect(...) in
this test relies on that; note that if preloadDelay changes the test should be
updated (or the test should instead reference a shared preloadDelay constant or
mock the delay). This makes the dependency explicit and prevents silent breaks
of the assertion that follows.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/solid-router/src/link.tsx`:
- Around line 351-365: The enqueueIntentPreload handler currently runs for any
truthy preload() value, causing conflicts with other preload strategies; update
the guard at the start of enqueueIntentPreload to only allow intent-only modes
by checking that local.disabled is false and preload() is exactly true or the
string 'intent' (i.e., replace the truthy preload() check with a strict check
like preload() === true || preload() === 'intent'); locate this in the
enqueueIntentPreload function (references: enqueueIntentPreload, doPreload,
preloadDelay, LinkCurrentTargetElement) and return early for all other modes.

---

Nitpick comments:
In `@packages/react-router/tests/store-updates-during-navigation.test.tsx`:
- Around line 215-216: Add a short comment immediately above the await new
Promise(...) after fireEvent.focus(link) that explains the magic 100ms value: it
must be greater than the router's default preloadDelay (50ms) to avoid
re-introducing the race this test guards against, and the later expect(...) in
this test relies on that; note that if preloadDelay changes the test should be
updated (or the test should instead reference a shared preloadDelay constant or
mock the delay). This makes the dependency explicit and prevents silent breaks
of the assertion that follows.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d277c0b and 89db2d8.

📒 Files selected for processing (2)
  • packages/react-router/tests/store-updates-during-navigation.test.tsx
  • packages/solid-router/src/link.tsx

Comment on lines +351 to +365
const enqueueIntentPreload = (e: MouseEvent | FocusEvent) => {
if (local.disabled || !preload()) return
const eventTarget = (e.currentTarget ||
e.target ||
{}) as LinkCurrentTargetElement

if (eventTarget.preloadTimeout) {
return
}
}

const handleTouchStart = handleFocus
eventTarget.preloadTimeout = setTimeout(() => {
eventTarget.preloadTimeout = null
doPreload()
}, preloadDelay())
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for preload definition and its possible values
rg -n "preload\s*[:=]|preload\s*\(\)" packages/solid-router/src -g '*.ts' -g '*.tsx' -A 2 -B 2

Repository: TanStack/router

Length of output: 2368


🏁 Script executed:

#!/bin/bash
# Look for preload type definition and enum/union values
rg -n "type.*preload|enum.*preload|'intent'|'render'|'viewport'" packages/solid-router/src -g '*.ts' -g '*.tsx' -B 2 -A 2

Repository: TanStack/router

Length of output: 703


🏁 Script executed:

#!/bin/bash
# Find how enqueueIntentPreload is called and what context it's in
rg -n "enqueueIntentPreload|handleMouseEnter|handleFocus" packages/solid-router/src/link.tsx -B 3 -A 3

Repository: TanStack/router

Length of output: 980


🏁 Script executed:

#!/bin/bash
# Check the full link.tsx file to understand preload flow and other related functions
wc -l packages/solid-router/src/link.tsx

Repository: TanStack/router

Length of output: 97


🏁 Script executed:

#!/bin/bash
# Get the full preload() definition
sed -n '199,220p' packages/solid-router/src/link.tsx

Repository: TanStack/router

Length of output: 710


🏁 Script executed:

#!/bin/bash
# Search for preload type or interface definition
rg -n "type.*Preload|interface.*Preload|export.*preload" packages/solid-router -g '*.ts' -g '*.tsx' -B 2 -A 4

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

#!/bin/bash
# Search for LinkProps or link options type definition to see preload type
rg -n "preload\s*\?\s*:" packages/solid-router/src -g '*.ts' -g '*.tsx' -B 1 -A 1

Repository: TanStack/router

Length of output: 290


🏁 Script executed:

#!/bin/bash
# Search for type definitions in shared router packages
rg -n "type.*Preload|'viewport'|'render'|'intent'" packages -g '*.ts' -g '*.tsx' | head -30

Repository: TanStack/router

Length of output: 2874


🏁 Script executed:

#!/bin/bash
# Look at the LinkProps interface/type definition in link.tsx
rg -n "interface.*Link|type.*Link" packages/solid-router/src/link.tsx -A 15 | head -50

Repository: TanStack/router

Length of output: 2249


🏁 Script executed:

#!/bin/bash
# Search for LinkOptions type to see preload property definition
rg -n "interface LinkOptions|type LinkOptions" packages/solid-router -g '*.ts' -g '*.tsx' -A 20 | head -60

Repository: TanStack/router

Length of output: 1338


🏁 Script executed:

#!/bin/bash
# Search for defaultPreload type/definition in router options
rg -n "defaultPreload|options\s*:" packages/core/src -g '*.ts' -A 5 | grep -A 5 "preload"

Repository: TanStack/router

Length of output: 116


🏁 Script executed:

#!/bin/bash
# Find the core router source
find packages -name "*.ts" -type f | grep -E "(core|router)" | head -20

Repository: TanStack/router

Length of output: 1416


🏁 Script executed:

#!/bin/bash
# Search in the entire packages directory for defaultPreload and preload type
rg -n "defaultPreload\s*[:\?]|preload\s*[:\?].*'viewport'|'intent'|'render'" packages -g '*.ts' -B 2 -A 2 | head -80

Repository: TanStack/router

Length of output: 1778


🏁 Script executed:

#!/bin/bash
# Look at the test cases for preload behavior to understand intent-specific behavior
rg -n "enqueueIntentPreload|intent.*preload|preload.*intent" packages/solid-router/tests/link.test.tsx -B 5 -A 5 | head -100

Repository: TanStack/router

Length of output: 709


🏁 Script executed:

#!/bin/bash
# Check how handleTouchStart and enqueueIntentPreload differ in intent handling
sed -n '351,375p' packages/solid-router/src/link.tsx

Repository: TanStack/router

Length of output: 681


Gate enqueueIntentPreload explicitly to intent-only modes.

The current truthy check !preload() allows this function to execute for any preload mode including 'render' and 'viewport', which conflicts with their own preload strategies. enqueueIntentPreload should only queue delayed preloads for 'intent' mode (or true, if applicable), not for modes handled elsewhere (viewport via intersection observer at line 275, render via explicit check at line 282).

Update the guard to:

if (local.disabled || (preload() !== true && preload() !== 'intent')) return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/solid-router/src/link.tsx` around lines 351 - 365, The
enqueueIntentPreload handler currently runs for any truthy preload() value,
causing conflicts with other preload strategies; update the guard at the start
of enqueueIntentPreload to only allow intent-only modes by checking that
local.disabled is false and preload() is exactly true or the string 'intent'
(i.e., replace the truthy preload() check with a strict check like preload() ===
true || preload() === 'intent'); locate this in the enqueueIntentPreload
function (references: enqueueIntentPreload, doPreload, preloadDelay,
LinkCurrentTargetElement) and return early for all other modes.

@Sheraff Sheraff merged commit 09405b2 into main Feb 23, 2026
8 checks passed
@Sheraff Sheraff deleted the codex/preload-focus-delay branch February 23, 2026 22:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants