# How React Router works under the hood

# Introduction

This post dives into how **React Router as a framework** works under the hood. Remix recently merged with React Router.

React Router is now not only a routing library but also a full-stack web framework.

# Preface

It’s not your typical post. I more or less took notes after going through the codebase and asking a lot of questions.

I hope the way I explain things isn’t too confusing. And to be clear, this is not a post for beginners. I do recommend you playing around with React Router as a framework, then coming back and reading this after a while.

# React Router as a Framework: Deep Dive

## Initial Request Types

When a request hits the server, React Router handles different types:

```javascript
// Main request types
if (url.pathname === manifestUrl) {
  return handleManifestRequest(); // /__manifest for fog-of-war
} else if (url.pathname.endsWith(".data")) {
  return handleDataRequest(); // /route.data for client navigations
} else {
  return handleDocumentRequest(); // Full page load
}
```

`/__manifest` requests happen when client needs new route info. `.data` requests are for client-side navigation data loading (after initial page load). Document requests are full page loads (initial page load).

Initial page load happens on the first request. When you refresh the page, it's a full page load! After initial SSR, React Router is client-side only (unless you use \`reloadDocument).

`__manifest` is quite cool. If you explore the network tab, you'll see `__manifest` requests. It returns something like this (taken from my [multiplayer naruto game](https://github.com/tigerabrodi/narumory)):

<details>
  <summary>🍿 See JSON</summary>
<hr />
<pre><code class="language-json">{
  "routes/views/auth/root": {
    "id": "routes/views/auth/root",
    "parentId": "root",
    "path": "/",
    "hasAction": false,
    "hasLoader": true,
    "hasClientAction": false,
    "hasClientLoader": false,
    "hasErrorBoundary": false,
    "module": "/assets/root-D2vetjBp.js",
    "imports": [
      "/assets/utils-zTSokisb.js",
      "/assets/index-BEb7FAyx.js",
      "/assets/card-C3OqSkw-.js",
      "/assets/constants-Be6MM4va.js",
      "/assets/constants-pAWeGExn.js",
      "/assets/index-DNU-sf_S.js",
      "/assets/index-3TgtNGEO.js",
      "/assets/index-q15GdxUK.js",
      "/assets/index-DXbShEkv.js"
    ],
    "css": []
  },
  "routes/views/auth/login": {
    "id": "routes/views/auth/login",
    "parentId": "routes/views/auth/root",
    "path": "login",
    "hasAction": true,
    "hasLoader": true,
    "hasClientAction": false,
    "hasClientLoader": false,
    "hasErrorBoundary": false,
    "module": "/assets/login-DZz-useP.js",
    "imports": [
      "/assets/utils-zTSokisb.js",
      "/assets/index-BEb7FAyx.js",
      "/assets/label-P4RMUw0u.js",
      "/assets/button-B1mfwelL.js",
      "/assets/constants-Be6MM4va.js",
      "/assets/constants-pAWeGExn.js",
      "/assets/input-DqgOOe5U.js",
      "/assets/createLucideIcon-DWA2PLD3.js",
      "/assets/index-DNU-sf_S.js",
      "/assets/index-3TgtNGEO.js",
      "/assets/index-q15GdxUK.js",
      "/assets/index-DXbShEkv.js"
    ],
    "css": []
  }
}</code></pre>
</details>

Fog of War in React Router is a smart way to improve performance. Instead of sending all the route information to the client at once, it only sends the routes needed for the current page. As users move around, it finds and loads new routes when necessary.

The hook being used: [useFogOFWarDiscovery](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/ssr/fog-of-war.ts#L95). It also respects the user's preferences via `navigator.connection.saveData`. FYI - that's not [available in all browsers](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/saveData).

---

## Initial Page Load

Server starts by running loaders and preparing state:

```typescript
// Server-side
let context = await staticHandler.query(request, {
  requestContext: loadContext,
});

// Prepare state for client hydration

// Server UI state to send to the client.
// - When single fetch is enabled, this is streamed down via `serverHandoffStream`
// - Otherwise it's stringified into `serverHandoffString`
let state = {
  loaderData: context.loaderData,
  actionData: context.actionData,
  errors: serializeErrors(context.errors, serverMode),
};
let entryContext: EntryContext = {
  manifest: build.assets,
  routeModules: createEntryRouteModules(build.routes),
  staticHandlerContext: context,
  criticalCss,
  serverHandoffString: createServerHandoffString({
    basename: build.basename,
    criticalCss,
    future: build.future,
    isSpaMode: build.isSpaMode,
  }),
  serverHandoffStream: encodeViaTurboStream(
    state,
    request.signal,
    build.entry.module.streamTimeout,
    serverMode
  ),
  renderMeta: {},
  future: build.future,
  isSpaMode: build.isSpaMode,
  serializeError: (err) => serializeError(err, serverMode),
};
```

This code can be found inside [handleDocumentRequest](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/server-runtime/server.ts#L325).

This state gets sent to the client for hydration. React Router does SSR first. Some benefits:

1. Faster initial page load (no client-server roundtrip)
    
2. SEO (content available in initial HTML)
    
3. Progressive enhancement (works without JS)
    

---

## Hydration Process

There are two ways to approach hydration. One where you set `clientLoader.hydrate = true` and one where you don't.

Let's look at both.

### Without `clientLoader.hydrate = true`

```typescript
export async function loader() {
  /*
    Do some server stuff
  */
}

export async function clientLoader() {
  await doSomeClientStuff();
  return data;
}
```

In this case, `clientLoader` doesn't participate in the hydration process. It won't run on the initial page load.

"Hydration" is a term often thrown around. If you want to learn this in-depth (you can just read the first few sections), read: [Why are React Server Components actually beneficial? (full history)](https://tigerabrodi.blog/why-is-react-server-components-actually-beneficial-full-history)

Let's understand what's happening here:

1. Server renders components to HTML
    
2. Client receives HTML
    
3. Client downloads React + your components (javascript bundles)
    
4. Client runs the components. This builds the virtual dom.
    
5. React walks the virtual DOM tree and the real DOM tree in parallel. Matches them up node by node. Attaches event handlers to the real DOM nodes. Sets up state management and makes everything interactive.
    

### With `clientLoader.hydrate = true`

```typescript
export async function loader() {
  /*
    Do some server stuff
  */
}

export function HydrateFallback() {
  return <Loading />;
}

export async function clientLoader() {
  await doSomeClientStuff();
  return data;
}

clientLoader.hydrate = true;
```

If `clientLoader.hydrate = true`, RR shows `HydrateFallback` during hydration. This is important when client initialization is needed before the UI is useful. Without `HydrateFallback`, users see a flash of server content before client data loads. If this isn't an issue for you, you likely don't need `hydrate = true`!

---

## Client-Side Navigation

When navigating, React Router uses a single-fetch strategy:

```javascript
// Instead of separate fetches per loader:
// /posts.data
// /posts/$id.data
// /posts/$id/comments.data

// Single fetch with all loader data:
fetch("/posts/$id/comments.data?_routes=posts,post,comments");
```

This is more efficient because:

1. Single network request
    
2. Server can optimize data loading
    
3. Reduced waterfall effect
    

---

## Actions and Revalidation

When an action completes, all current route loaders revalidate:

```typescript
// Nested routes
/app
  /dashboard
    /profile

// After action on /dashboard
// All these revalidate:
- app loader
- dashboard loader
- profile loader
```

RR revalidates all loaders because:

1. Data might be stale after mutation
    
2. Parent/child routes often share data dependencies
    
3. Ensures UI consistency
    

You can opt out for a specific route with [shouldRevalidate](https://reactrouter.com/start/framework/route-module#shouldrevalidate) if needed. It’s worth mentioning that it’s super easy to shoot yourself in the foot here. If you do opt out, it’s like breaking hooks rule in React. **It’s very easy for your data to get out of sync if you don’t know what you’re doing.** If I use it, I always add a clear comment explaining the *why*.

---

## Race Conditions

RR automatically handles navigation race conditions:

```typescript
// If user clicks quickly:
navigate("/a");
navigate("/b"); // Cancels /a navigation
navigate("/c"); // Cancels /b navigation
```

This works because:

1. Latest navigation wins
    
2. Previous requests are aborted
    
3. Matches browser behavior
    

The code for this happens in the [startNavigation](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/router.ts#L1490) function. React Router uses AbortController to cancel previous requests. My friend Artem recently wrote an amazing post about it: [Don't Sleep on AbortController](https://kettanaito.com/blog/dont-sleep-on-abort-controller).

Fun fact: Link ([source](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx#L558)) components use `navigate` internally. The hook they use can be found in the [lib.tsx#L1278](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx#L1278).

---

Same applies to form submissions, but fetchers work differently:

1. Each fetcher is independent
    
2. Can only interrupt itself
    

If you need multiple concurrent submissions, read: [How to handle multiple concurrent submissions in Remix](https://tigerabrodi.blog/how-to-handle-multiple-concurrent-submissions-in-remix).

---

## Router State and UI Updates

React Router maintains state through context:

```typescript
<DataRouterContext.Provider value={router}>
  <DataRouterStateContext.Provider value={state}>
    {children}
  </DataRouterStateContext.Provider>
</DataRouterContext.Provider>
```

This can be found in [RouterProvider](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/components.tsx#L197).

When state changes (navigation/action), UI updates because:

1. Router state changes in context.
    
2. Components that consume the context re-render. (this is how React itself works!)
    

This is why hooks like `useLoaderData()` work automatically. They're subscribed to router state changes.

A quick look into `useLoaderData`:

```typescript
export function useLoaderData<T = any>(): SerializeFrom<T> {
  let state = useDataRouterState(DataRouterStateHook.UseLoaderData);
  let routeId = useCurrentRouteId(DataRouterStateHook.UseLoaderData);
  return state.loaderData[routeId] as SerializeFrom<T>;
}
```

`DataRouterStateHook` is just an enum:

```typescript
enum DataRouterStateHook {
  UseBlocker = "useBlocker",
  UseLoaderData = "useLoaderData",
  UseActionData = "useActionData",
  UseRouteError = "useRouteError",
  UseNavigation = "useNavigation",
  UseRouteLoaderData = "useRouteLoaderData",
  UseMatches = "useMatches",
  UseRevalidator = "useRevalidator",
  UseNavigateStable = "useNavigate",
  UseRouteId = "useRouteId",
}
```

However, more recently, you get loader and action data as props:

```typescript
export default function Component({ loaderData }: Route.ComponentProps) {
  return <h1>{loaderData.name}</h1>;
}
```

How does this work?

First and foremost, where they do their "on the fly magic" type generation: [generate.ts](https://github.com/remix-run/react-router/blob/main/packages/react-router-dev/typegen/generate.ts).

Now, where exactly they "inject" the props, that's a [vite plugin](https://github.com/remix-run/react-router/blob/7a270cbc97bf3c9d73448401eef1d99699bf8bb6/packages/react-router-dev/vite/with-props.ts#L18).

# Recap

I spent 8 hours digging into this. It sounds like a lot, but it's just a day. It's not like I spent weeks or months on this.

The point?

Give yourself some time, and you can actually understand how things work.

It's not magic like [Ginger Bill](https://www.youtube.com/@GingerGames) says :3
