Articles
React Router Interview Questions: Nested Routes, Loaders, and Navigation Races
Prepare for React Router interviews with nested route examples, loader and action boundaries, navigation races, pending UI, and focused test cases.
An interviewer shows a dashboard whose detail pane changes when the URL changes. You can make it render. The harder question is whether the right layout stays mounted, which route loads each piece of data, and what users see when they click from ticket A to ticket B before A's slow request finishes. That is the difference between a component-level React answer and a React Router Data Mode answer. This guide uses one original ticket-workspace exercise to make the route tree, loader ownership, and navigation timeline explicit.
Evidence boundary: current React Router documentation supports the API behavior cited below. The ticket app, interview prompts, and assessment rubric are original editorial examples; this article does not report a company-specific process.
The interview exercise: keep the shell, replace the detail
Assume a support workspace has /tickets, /tickets/:ticketId, and /tickets/:ticketId/activity. The left rail contains search and a ticket list. The right pane shows a selected ticket, and its activity view adds an event feed. Authentication and a top navigation bar surround all three. The interviewer asks you to sketch a route configuration, name each data owner, and explain what happens when a user rapidly selects two tickets. You may use pseudocode; the quality of the answer is in the boundaries, not in memorizing every import.
First establish the route hierarchy. A root layout owns the global bar and an <Outlet /> for descendants. A tickets layout owns the rail and another <Outlet /> for the detail area. An index child can show “Select a ticket.” A :ticketId child owns ticket-specific data and renders an outlet if the activity view is nested below it. React Router's route-object documentation describes child routes and loaders. The route tree is a UI ownership map: parent layouts can remain visible while the matching child changes.
| URL match | Layout that remains visible | Data owner | Detail region |
|---|---|---|---|
/tickets | Root and tickets shell | Tickets list loader | Index prompt |
/tickets/42 | Root and tickets shell | Ticket 42 loader, plus parent data as needed | Ticket summary |
/tickets/42/activity | Root, tickets shell, ticket summary layout | Activity child loader | Event feed |
This does not mean every parent loader always reruns on every click. Loader calls and revalidation depend on navigation and data-router rules, with shouldRevalidate available for deliberate control. The interview answer should identify ownership first, then discuss optimization after measuring or specifying the behavior. If you put every fetch in the root, changing one ticket can make unrelated data harder to cache, test, or recover from errors. If you fetch the same ticket in three siblings, you may create redundant work and inconsistent snapshots. Explain where each record is required, and where the layout can safely render while a child is pending.
What should a loader do, and what should a component do?
A Data Mode loader receives route context such as params and a request, obtains data before the destination route renders, and can surface an error or redirect through the router. The Data Loading guide illustrates loaders and useLoaderData. In our exercise, the ticket detail loader validates ticketId, calls an authorized API endpoint, and returns ticket data. A component consumes that result and renders it; it should not silently launch a second authoritative fetch merely because it mounted.
One reasonable sketch is:
const router = createBrowserRouter([
{
path: "/",
Component: AppLayout,
children: [{
path: "tickets",
Component: TicketsLayout,
loader: loadTicketList,
children: [
{ index: true, Component: PickTicket },
{
path: ":ticketId",
Component: TicketLayout,
loader: loadTicket,
ErrorBoundary: TicketError,
children: [
{ index: true, Component: TicketSummary },
{ path: "activity", Component: Activity, loader: loadActivity },
],
},
],
}],
},
]);
The exact code can change with React Router mode and release. Say “I am answering for Data Mode” before using route objects, loaders, and actions; a React-only router or a Next.js App Router app has different contracts. The example's two nested layouts must actually render <Outlet />; otherwise matching child routes have nowhere to appear. The error boundary belongs near the ticket-dependent pane so a missing ticket need not replace the entire app shell. Authentication decisions still require a real server-side authorization check; a client route guard cannot protect the API.
Watch for a subtle ownership trap: a parent ticket layout and an index ticket summary may need the same ticket. Let the child read parent loader data through the router's data API or structure the route so one owner provides it. Do not have each component call /tickets/42 independently without a reason. Conversely, an activity feed may have different refresh and pagination needs; a child loader can express that separate concern. This is the design-level answer an interviewer is looking for when they ask why a nested route matters beyond pretty URLs.
Navigation race: which response is allowed to win?
Suppose the user clicks ticket 42 at t0. Its loader is slow. At t1 they click ticket 99, whose loader is fast. A naive component-level fetch could commit the 99 result and then let 42's late response overwrite the visible detail. React Router documents race-condition handling for navigations and revalidations: interrupted client requests are canceled and stale results are not committed as the current navigation. In the answer, distinguish which data the router displays from whether the server still performed work. Aborting a browser request does not prove that a mutation on the server was rolled back.
Walk the interviewer through the timeline. The first click starts loading 42 and should expose a pending state without erasing all useful context. The second click supersedes it and starts loading 99. If 99 completes, the URL and displayed ticket should agree. If 42 finishes later, its result must not make the pane show 42 under /tickets/99. If your application also performs custom fetches outside the router, those requests need their own cancellation or request-identity protection. Do not claim the router automatically controls every arbitrary useEffect fetch you add.
There is a product decision hiding in pending UI: should the old detail remain visible while the new ticket loads, or should the pane show a skeleton? Either can be defensible if it is clearly marked and never misrepresented as the newly selected ticket. A stale-looking card under a new URL can be worse than a brief loading state. Discuss keyboard focus and a screen-reader status message as well as spinner placement. If the rail remains usable, an error in one ticket should not make the user lose search context.
Actions and revalidation are a different race
The interviewer may add “Now let the agent change a ticket's status.” In Data Mode, an action handles a route mutation and the router revalidates loader data afterward. Explain what the action returns or redirects to, what validation errors the form displays, and what current ticket data should refresh. Do not treat a mutation as a loader or assume a local optimistic label is the source of truth. If the operation can be submitted twice, server-side idempotency or concurrency rules matter independently of client navigation.
Imagine the user submits “Close ticket 42,” then immediately selects ticket 99. The client may leave the 42 page, but the close request might still reach the server. The later navigation should not show stale 42 data on the 99 page. Yet the status change may still be committed. A strong answer says, “I will verify the mutation result and server behavior separately from which navigation wins in the browser.” This prevents the common false assurance that cancellation equals rollback.
Revalidation can create its own visible inconsistency when a slowly refreshed list conflicts with a newer list. React Router's concurrency behavior helps discard stale responses, but your API must still define stable sort, authorization, and record version semantics. For a status change, a list-count badge and detail status should agree after the accepted action. A route design can make the necessary data owners clear; it cannot manufacture transactional consistency in a backend that does not provide it.
Tests that prove the design, not just the happy path
The most valuable tests mirror the route timeline. Render the router with a controlled loader, navigate to ticket 42, hold its response, navigate to 99, release 99, then release 42. Assert the URL, heading, detail ID, and any pending label. Check that the 42 result never replaces 99. A second test begins at /tickets/42/activity and verifies that both the shell and activity region render on a direct load. A third returns a not-found response from the ticket loader and checks that the ticket-level error surface appears while the global shell remains navigable.
For a mutation, test validation and revalidation as separate observations. Submit a malformed status and assert that no success state is shown. Submit a valid change and assert that the route data reflects the authoritative returned status after revalidation. If your test double always resolves instantly, it cannot expose a race. Controlled deferred promises or a request-interception tool let you intentionally reverse completion order. Test the behavior at the router boundary rather than inspecting an internal isLoading variable that might change during a refactor.
| Test condition | Observation that matters | A false-positive test |
|---|---|---|
| Deep link to activity | Parent layout, selected ticket, and feed all appear | Only asserting the URL string |
| 42 slow, 99 fast | 99 remains current after 42 resolves | Awaiting both in original order |
| Ticket 42 missing | Local error with usable navigation | Checking only that an error was logged |
| Status action succeeds | Detail and list settle on server state | Asserting only an optimistic button label |
| Status action fails | Error is visible and old state is not claimed as saved | Mocking a success response for every request |
One nuance: test a real user-visible claim. “Request was aborted” is an implementation detail unless the application promises it; “ticket 99 is still displayed under its URL” is an invariant. The latter is robust even if the router's internal scheduling changes. For accessibility, keyboard navigation to a ticket should preserve a sensible focus target, and pending text should not announce an old ticket as though it were the selected one. These checks make the exercise about a product, not just API trivia.
Common interview answers to refine
“Nested routes share components” is true but incomplete. Name the persistent layout, where <Outlet /> is rendered, which route owns each loader, and how errors are scoped. “A loader is just useEffect” also misses the router's pre-render data lifecycle, navigation state, and revalidation contract. “Abort means the write cannot happen” is unsafe: distinguish client response commitment from server effects. “I will show a spinner” needs a location and an accessibility plan. A global spinner that hides a usable ticket rail sacrifices context for no inherent correctness gain.
If asked when not to use a child route, explain that local UI state such as opening a tooltip may not deserve a URL. A state worth bookmarking, deep linking, or loading independently often does. The route tree should reflect user navigation and data boundaries, not merely mirror the component folder structure. Similarly, shouldRevalidate is not a blanket performance switch; suppressing necessary refresh after an action can leave the list and detail contradictory. Start with correct behavior, then optimize with an explicit data dependency.
Sources and Further Reading
- React Router: Route Object — child routes, loaders, and route-level behavior.
- React Router: Data Loading — loading route data and consuming it.
- React Router: Actions — submissions and revalidation.
- React Router: Race Conditions — interrupted navigations and stale responses.