Skip to content

Conversation

@vsh1h
Copy link
Contributor

@vsh1h vsh1h commented Nov 19, 2025

This PR prevents a brief flicker that occurred when navigation happened during render, causing nested to temporarily render null. Navigation is now triggered before paint (React) or on mount (Solid), so the UI transitions smoothly without flashing.

Bug Fix

  • Eliminates the temporary blank/empty state shown when navigating during render.
  • Ensures nested content renders immediately with no flicker.

Summary by CodeRabbit

  • Refactor
    • Optimized navigation timing to ensure proper execution phase synchronization across React and Solid framework examples.
    • Updated navigation effect handling to use layout-phase execution for improved consistency.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 19, 2025

Walkthrough

Navigation calls in React and Solid examples and the React Router core were changed to run inside lifecycle hooks (React: layout effect; Solid: onMount) instead of invoking navigation synchronously during render.

Changes

Cohort / File(s) Change Summary
React example
examples/react/start-large/src/typePrimitives.tsx
Added import * as React from 'react' and changed useCustomNavigate to call navigate(options) inside a layout-effect-style hook (deferred to lifecycle).
Solid example
examples/solid/start-large/src/typePrimitives.tsx
Added onMount import from solid-js and changed useCustomNavigate to defer navigate(options) inside onMount.
React Router core
packages/react-router/src/useNavigate.tsx
Replaced direct useEffect usage with an isomorphic layout-effect selection (uses useLayoutEffect on client) so navigation side-effects run in a layout-effect phase.

Sequence Diagram

sequenceDiagram
    participant Component
    participant LifecycleHook as Lifecycle Hook (useLayoutEffect/onMount)
    participant Navigator as navigate(options)
    participant RouterState as Router / Browser

    Note over Component,RouterState: Deferred navigation via lifecycle hooks

    Component->>LifecycleHook: Register effect (deps: navigate, options)
    alt On layout/mount
        LifecycleHook->>Navigator: invoke navigate(options)
        Navigator->>RouterState: update route / URL
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Inspect packages/react-router/src/useNavigate.tsx for correct isomorphic effect selection and dependency arrays.
  • Verify examples' changes (React/Solid) preserve intended semantics and don't introduce mount-time races.
  • Confirm no public API/signature changes occurred and tests (if any) reflect lifecycle timing.

Suggested reviewers

  • brenelz

Poem

🐰
I hopped through render, then waited my cue,
Laid my path in a hook where the layout is true.
React and Solid, we now take our time,
Gentle jumps in sequence, tidy and prime. ✨

Pre-merge checks and finishing touches

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ 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 accurately summarizes the main change: preventing flicker when navigation happens during render by deferring navigation to layout effects, which runs before paint.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

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 (2)
packages/react-router/src/useNavigate.tsx (1)

70-71: Consider extracting to module level.

The useIsomorphicLayoutEffect hook is redefined on every render. For better performance, consider extracting it to the module level:

+const useIsomorphicLayoutEffect =
+  typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect
+
 export function Navigate<
   TRouter extends AnyRouter = RegisteredRouter,
   const TFrom extends string = string,
   const TTo extends string | undefined = undefined,
   const TMaskFrom extends string = TFrom,
   const TMaskTo extends string = '',
 >(props: NavigateOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>): null {
   const router = useRouter()
   const navigate = useNavigate()

   const previousPropsRef = React.useRef<NavigateOptions<
     TRouter,
     TFrom,
     TTo,
     TMaskFrom,
     TMaskTo
   > | null>(null)
-  const useIsomorphicLayoutEffect =
-    typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect
examples/react/start-large/src/typePrimitives.tsx (1)

25-26: Consider extracting isomorphic layout effect to a shared utility.

This pattern is duplicated in packages/react-router/src/useNavigate.tsx (lines 70-71). Consider creating a shared utility function or importing from the router package to avoid duplication:

// In a shared utilities file or exported from @tanstack/react-router
export const useIsomorphicLayoutEffect =
  typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect

Then import and use it:

 import * as React from 'react'
-import { Link, redirect, useNavigate } from '@tanstack/react-router'
+import { Link, redirect, useNavigate, useIsomorphicLayoutEffect } from '@tanstack/react-router'
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a165006 and 5a00207.

📒 Files selected for processing (3)
  • examples/react/start-large/src/typePrimitives.tsx (2 hunks)
  • examples/solid/start-large/src/typePrimitives.tsx (2 hunks)
  • packages/react-router/src/useNavigate.tsx (1 hunks)
🔇 Additional comments (2)
examples/react/start-large/src/typePrimitives.tsx (1)

28-30: LGTM!

The isomorphic layout effect correctly ensures navigation timing across client and server environments, and the dependencies ensure navigation updates when options change.

packages/react-router/src/useNavigate.tsx (1)

73-78: Code is correct; Navigate is intentionally client-only.

The isomorphic layout effect pattern is properly implemented. The Navigate component is designed for client-side rendering only—its JSDoc explicitly states "Navigation executes in an effect after mount/update." For server-side navigation during SSR, use the redirect() function from route loaders or beforeLoad hooks, which is the framework's server-side counterpart.

Comment on lines +26 to +28
onMount(() => {
navigate(options)
})
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

Verify intended navigation behavior difference.

The Solid implementation navigates once on mount via onMount, while the React implementation (in examples/react/start-large/src/typePrimitives.tsx lines 28-30) re-navigates whenever options changes using useIsomorphicLayoutEffect with [navigate, options] dependencies.

This means:

  • Solid: Navigation occurs once after mount, regardless of subsequent options changes
  • React: Navigation re-executes whenever options reference changes

Is this behavioral difference intentional, or should the Solid version also react to options changes using createEffect?


🏁 Script executed:

#!/bin/bash
# Search for usage patterns of useCustomNavigate to understand expected behavior

# Find all usages in Solid example
rg -n "useCustomNavigate" examples/solid/

# Find all usages in React example  
rg -n "useCustomNavigate" examples/react/

# Check if there are any tests that document the expected behavior
fd -e test -e spec | xargs rg -l "useCustomNavigate|CustomNavigate" | head -10

Length of output: 1029


🏁 Script executed:

#!/bin/bash
# Examine the actual implementations in both React and Solid

echo "=== Solid typePrimitives.tsx (lines 20-30) ==="
sed -n '20,30p' examples/solid/start-large/src/typePrimitives.tsx

echo -e "\n=== React typePrimitives.tsx (lines 19-30) ==="
sed -n '19,30p' examples/react/start-large/src/typePrimitives.tsx

echo -e "\n=== Solid linkProps.tsx (lines 20-35) ==="
sed -n '20,35p' examples/solid/start-large/src/routes/linkProps.tsx

echo -e "\n=== React linkProps.tsx (lines 20-35) ==="
sed -n '20,35p' examples/react/start-large/src/routes/linkProps.tsx

Length of output: 1458



🏁 Script executed:

#!/bin/bash
# Check for any comments or documentation about this difference
# and look at the full context around these functions

echo "=== Full Solid typePrimitives.tsx function context (lines 15-35) ==="
sed -n '15,35p' examples/solid/start-large/src/typePrimitives.tsx

echo -e "\n=== Full React typePrimitives.tsx function context (lines 14-35) ==="
sed -n '14,35p' examples/react/start-large/src/typePrimitives.tsx

# Check if there's a comment/discussion in git history or PR
echo -e "\n=== Check for Solid createEffect imports ==="
grep -n "createEffect\|onMount" examples/solid/start-large/src/typePrimitives.tsx

Length of output: 1572


🏁 Script executed:

#!/bin/bash
# Check for any comments or documentation about this difference
# and look at the full context around these functions

echo "=== Full Solid typePrimitives.tsx context with imports (lines 1-40) ==="
sed -n '1,40p' examples/solid/start-large/src/typePrimitives.tsx

echo -e "\n=== Full React typePrimitives.tsx context with imports (lines 1-40) ==="
sed -n '1,40p' examples/react/start-large/src/typePrimitives.tsx

# Check if createEffect is used elsewhere in Solid examples
echo -e "\n=== Check for createEffect usage in Solid examples ==="
rg -n "createEffect" examples/solid/ | head -20

Length of output: 4531


Use createEffect with dependency tracking instead of onMount for consistency with React behavior.

The review comment accurately identifies a behavioral difference: Solid uses onMount (runs once) while React uses useIsomorphicLayoutEffect with [navigate, options] dependencies (runs when dependencies change).

In Solid, when options changes, navigation will not re-execute because onMount only runs on component initialization. This differs from React, which will re-navigate if the options reference changes. For parity with the React implementation and to follow Solid reactive patterns, use createEffect with proper dependency tracking:

const navigate = useNavigate()
createEffect(() => {
  navigate(options)
})

This ensures both frameworks have consistent behavior when options change.

🤖 Prompt for AI Agents
In examples/solid/start-large/src/typePrimitives.tsx around lines 26 to 28, the
component uses onMount to call navigate(options) which only runs once; change
this to use Solid's reactive createEffect that depends on the navigate/options
so navigation re-runs when options change. Replace the onMount call with a
createEffect that calls navigate(options) inside it (ensuring navigate and
options are captured) so behavior matches React's dependency-driven
re-navigation.

@vsh1h vsh1h changed the title fixed Navigate tag issue Fix: Prevent Flicker When Navigation Happens During Render Nov 19, 2025
@vsh1h
Copy link
Contributor Author

vsh1h commented Nov 19, 2025

Hi @birkskyum!
I’ve worked on this and opened a PR with a fix for the flicker caused by navigation during render. When you have a moment, I’d appreciate your review. Please let me know if there’s anything you’d like adjusted or improved — happy to iterate.
Thanks!

@birkskyum
Copy link
Member

@vsh1h , for solid it seems like the only change is in an example? so this pr will effectively only impact the react-router. Is that because the solid-router already behaves as expected, if used correctly?

@vsh1h
Copy link
Contributor Author

vsh1h commented Nov 19, 2025

@birkskyum ,
Hi! Yes, the Solid side required only a small update in the example because Solid Router already handles this correctly when used in the intended way.
Solid’s navigate() runs before paint due to its synchronous update model, so it doesn’t hit the same flicker issue that React does. The only adjustment I made was to align the example with the recommended pattern (calling navigate() inside onMount / isomorphic layout effect), ensuring consistency across both implementations.
So the functional impact of this PR is mainly on the React router, while the Solid example update simply reflects the correct usage and avoids confusion for users.
Please let me know if there’s anything you’d like adjusted or improved — happy to update.
Thanks!

@nx-cloud
Copy link

nx-cloud bot commented Nov 19, 2025

View your CI Pipeline Execution ↗ for commit ac105f2

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

☁️ Nx Cloud last updated this comment at 2025-11-19 22:12:48 UTC

@pkg-pr-new
Copy link

pkg-pr-new bot commented Nov 19, 2025

More templates

@tanstack/arktype-adapter

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

@tanstack/directive-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/directive-functions-plugin@5905

@tanstack/eslint-plugin-router

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

@tanstack/history

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

@tanstack/nitro-v2-vite-plugin

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

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

@tanstack/react-start-client

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

@tanstack/react-start-server

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

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/server-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/server-functions-plugin@5905

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-router-ssr-query

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

@tanstack/solid-start

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

@tanstack/solid-start-client

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

@tanstack/solid-start-server

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

@tanstack/start-client-core

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

@tanstack/start-plugin-core

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

@tanstack/start-server-core

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

@tanstack/start-static-server-functions

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

@tanstack/start-storage-context

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

@tanstack/valibot-adapter

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

@tanstack/virtual-file-routes

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

@tanstack/zod-adapter

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

commit: ac105f2

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: 0

🧹 Nitpick comments (1)
packages/react-router/src/useNavigate.tsx (1)

45-47: Consider clarifying the timing in documentation.

The JSDoc comment states "Navigation executes in an effect after mount/update." While accurate, it could be more precise to mention that navigation executes in a layout effect, which runs synchronously before paint. This helps developers understand the timing guarantees.

Apply this diff to enhance clarity:

 /**
  * Component that triggers a navigation when rendered. Navigation executes
- * in an effect after mount/update.
+ * in a layout effect after mount/update, before the browser paints.
  *
  * Props are the same as `NavigateOptions` used by `navigate()`.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5a00207 and ac105f2.

📒 Files selected for processing (1)
  • packages/react-router/src/useNavigate.tsx (2 hunks)
🔇 Additional comments (2)
packages/react-router/src/useNavigate.tsx (2)

2-2: LGTM: Using the utility version as requested.

The import now uses useLayoutEffect from the local utils module, which addresses the previous review feedback. This utility likely provides SSR-safe behavior by falling back to useEffect on the server while using useLayoutEffect on the client.


71-76: LGTM: Layout effect prevents flicker as intended.

Switching to useLayoutEffect ensures navigation executes synchronously after DOM mutations but before the browser paints. This eliminates the brief flicker where nested <Outlet /> components would render null during navigation, directly addressing the PR objective.

The effect logic is correct:

  • Reference equality check on props triggers navigation when the props object identity changes
  • previousPropsRef tracks whether navigation has already occurred for current props
  • Dependencies are complete and appropriate

@SeanCassiere SeanCassiere merged commit 93c4d6b into TanStack:main Nov 19, 2025
6 checks passed
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.

3 participants