How to Fix Remix Dynamic Route Precedence: The Underscore Solution
Solve Remix routing conflicts between index and dynamic child routes using pathless layout routes

I was building a reviews management system when I hit one of the most frustrating Remix routing bugs I've encountered. Both /admin/reviews/ and /admin/reviews/4e316b4e-4511-4418-a110-e15e31bfeae6 were rendering the exact same component - the general reviews list instead of individual review details. After hours of debugging route precedence, file naming conventions, and TypeScript compilation errors, I discovered the culprit: a critical but poorly documented Remix file-based routing behavior that breaks dynamic nested routes.
If you're struggling with Remix dynamic routes not matching correctly, parent routes overriding child routes, or file-based routing conflicts between index and dynamic segments, this guide reveals the exact solution that finally made my nested routes work properly.
The Problem: Remix Route Precedence Chaos
Here's what should work in Remix but doesn't:
app/routes/ ├── admin.reviews.tsx // Should handle /admin/reviews └── admin.reviews.$id.tsx // Should handle /admin/reviews/{id}
With this structure, visiting /admin/reviews/some-uuid should load the individual review component from admin.reviews.$id.tsx. Instead, Remix incorrectly routes both URLs to admin.reviews.tsx, showing the reviews list for both the index route and dynamic route.
This creates a maddening debugging experience where your dynamic route loader never executes, your component never renders, and there's no error message explaining why. The route matching algorithm treats both paths as if they should resolve to the parent route, completely ignoring the dynamic segment.
The Root Cause: Ambiguous Route Matching
The issue stems from how Remix interprets file-based routing hierarchies. When you have admin.reviews.tsx and admin.reviews.$id.tsx in the same directory, Remix's route matcher cannot definitively distinguish between:
- A request for the index route (/admin/reviews/)
- A request for a dynamic child route (/admin/reviews/{id})
This ambiguity causes the route matching algorithm to default to the parent route in both cases, effectively making your dynamic route unreachable. The problem is particularly insidious because both files exist, both compile successfully, and there are no runtime errors - the wrong component just silently renders.
The Solution: Underscore Pathless Layout Routes
The fix requires using Remix's underscore notation to create a "pathless layout route" that properly separates the dynamic child from its parent:
app/routes/ ├── admin.reviews.tsx // Handles /admin/reviews └── admin.reviews_.$id.tsx // Handles /admin/reviews/{id}
The critical difference is the underscore before the dollar sign: admin.reviews_.$id.tsx. This tells Remix to create a child route that inherits the parent path (/admin/reviews/) but doesn't add an additional path segment - it treats the $id parameter as a direct child of the parent route.
Here's the working implementation:
// File: app/routes/admin.reviews_.$id.tsx import type { LoaderFunctionArgs } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react";
export async function loader({ request, params }: LoaderFunctionArgs) { const { id } = params; // This now properly captures the dynamic segment
// Your individual review loading logic
const review = await getReview({ id });
return { review };
}
export default function AdminReviewDetail() { const { review } = useLoaderData();
return (
<div>
<h1>Review Details</h1>
<p>Review ID: {review.id}</p>
{/* Individual review component */}
</div>
);
}
With this structure, Remix now correctly routes:
- /admin/reviews/ → admin.reviews.tsx (index route)
- /admin/reviews/4e316b4e-4511-4418-a110-e15e31bfeae6 → admin.reviews_.$id.tsx (dynamic child route)
Why the Underscore Works: Pathless Layout Routes Explained
The underscore in Remix routing creates what's called a "pathless layout route." This is a route that participates in the route hierarchy for component nesting and data loading but doesn't contribute its own path segment to the URL structure.
Without the underscore, admin.reviews.$id.tsx attempts to create a route at the same hierarchical level as admin.reviews.tsx, causing the conflict. With the underscore, admin.reviews_.$id.tsx becomes a child route that inherits the parent's path (/admin/reviews/) and appends the dynamic segment directly.
This distinction is crucial for complex nested routing scenarios where you need both index routes and dynamic child routes under the same path prefix. The underscore pattern ensures clean URL structures while maintaining proper component hierarchy and data loading behavior.
Updating Your Route Links
Don't forget to update any navigation links to use the correct URL structure:
// File: app/routes/admin.reviews.tsx (general reviews list)
<Button url={/admin/reviews/${review.id}
}>
View Details
// File: app/routes/admin.stores.$id.tsx (store details page)
<Button url={/admin/reviews/${review.id}
}>
View Review
The URL structure remains clean and intuitive - only the file naming convention changes to resolve the routing conflict.
The Broader Remix Routing Lesson
This bug highlights a critical gap in Remix's file-based routing documentation. The framework's routing behavior isn't always intuitive, especially when dealing with nested dynamic routes, and the error messages don't help identify when routes are conflicting.
Key takeaways for Remix routing:
- Use underscores for pathless layout routes when you need dynamic children of index routes
- File naming directly impacts route matching precedence
- Silent routing conflicts can occur without error messages
- Always test both parent and child route access during development
The underscore pattern is essential knowledge for any Remix application with complex routing hierarchies. Understanding when and how to use pathless layout routes prevents hours of debugging mysterious routing behavior.
Conclusion
The Remix dynamic route precedence bug between admin.reviews.tsx and admin.reviews.$id.tsx is a perfect example of how file-based routing can create unexpected conflicts. The underscore notation in admin.reviews_.$id.tsx creates the pathless layout route structure needed to properly separate index routes from their dynamic children.
You now know how to structure nested dynamic routes in Remix without losing your mind to silent routing conflicts. This pattern works for any scenario where you need both an index route and dynamic child routes under the same path prefix - user profiles, product pages, admin panels, or any hierarchical data structure.
Let me know in the comments if you've encountered other Remix routing edge cases, and subscribe for more practical development guides that solve real debugging challenges.
Thanks, Matija