Used

How toLeverage React Router's Built-in Data Deduplication

Modern web applications often need to fetch multiple pieces of data independently while also combining them for different parts of the UI. A common pattern is to have individual promises for specific data and then merge them with Promise.all() for aggregate views. The problem? Most frameworks would send duplicate data when these combined promises resolve.

React Router has a hidden optimization that changes this completely. When you combine promises using Promise.all() or similar methods, the framework automatically detects shared data and transmits it using references instead of duplicating the actual values.

Let's explore this with a practical example. We'll create a loader that returns two independent promises and a third promise that combines them:

export function loader() {
  let p1 = delay(1500).then(() => crypto.randomUUID());
  let p2 = delay(1000).then(() => crypto.randomUUID());
  let p3 = Promise.all([p1, p2]);
  return { p1, p2, p3 };
}

Discovering the Deduplication in Action

When we inspect the HTML response, we can observe React Router's deduplication system at work. While the data flows to the client as each promise resolves, the real magic happens when combined promises complete:

<script>
  window.__reactRouterContext.streamController.enqueue(
    '[{"_1":2,"_11":-5,"_12":-5},"loaderData",{"_3":4},"routes/home",{"_5":6,"_7":8,"_9":10},"p1",["P",6],"p2",["P",8],"p3",["P",10],"actionData","errors"]\n',
  );
</script>

<script>
  window.__reactRouterContext.streamController.enqueue(
    'P8:["0b9db045-a02b-4e2a-94ae-15f76c0aceb7"]\n',
  );
</script>

<script>
  window.__reactRouterContext.streamController.enqueue(
    'P6:["e3381505-7354-401d-ae68-ffa50398ad4c"]\n',
  );
</script>

<script>
  window.__reactRouterContext.streamController.enqueue("P10:[[14,13]]\n");
</script>

The initial script tag establishes the streaming infrastructure. Notice how React Router sets up references for p1, p2, and p3 using identifiers like ["P",6], ["P",8], and ["P",10]. These aren't arbitrary—they're pointers to the actual data that will arrive in subsequent chunks.

Since p2 resolves first (after 1000ms), its data streams to the client immediately:

<script>
  window.__reactRouterContext.streamController.enqueue(
    'P8:["0b9db045-a02b-4e2a-94ae-15f76c0aceb7"]\n',
  );
</script>

The P8 reference connects this resolved UUID to the promise structure established earlier. React Router knows exactly where this data belongs in the overall data graph.

Next, when p1 completes (after 1500ms), its data follows:

<script>
  window.__reactRouterContext.streamController.enqueue(
    'P6:["e3381505-7354-401d-ae68-ffa50398ad4c"]\n',
  );
</script>

Again, the P6 reference ensures this data lands in the correct position.

Here's where React Router's intelligence really shines. With both individual promises resolved, p3 can now complete:

<script>
  window.__reactRouterContext.streamController.enqueue("P10:[[14,13]]\n");
</script>

Notice something remarkable: instead of sending the actual UUID values again, React Router sends [[14,13]]—references to the previously transmitted data. This demonstrates the framework's sophisticated understanding of data relationships. Since p3 is just Promise.all([p1, p2]), there's no need to duplicate the UUIDs. The framework creates a pointer-based structure that eliminates redundancy.

Building the UI with Streaming Data

Now let's see how to consume this streaming data in our React component:

export default function Home({ loaderData }: Route.ComponentProps) {
  return (
    <Suspense fallback={<Fallback label="Global" />}>
      <Suspense fallback={<Fallback label="Fallback P1" />}>
        <Await resolve={loaderData.p1}>{(d1) => <p>{d1}</p>}</Await>
      </Suspense>

      <Await resolve={loaderData.p2}>{(d2) => <p>{d2}</p>}</Await>

      <Suspense fallback={<Fallback label="Fallback P3" />}>
        <Await resolve={loaderData.p3}>{(d3) => <p>{d3.join(" & ")}</p>}</Await>
      </Suspense>
    </Suspense>
  );
}

This setup creates a cascading loading experience: the global Suspense boundary handles the overall loading state, while individual boundaries for p1 and p3 provide granular feedback. As each promise resolves, the corresponding UI section renders immediately.

Deduplication with Complex Data Structures

The reference-based deduplication becomes even more valuable with complex data. Let's replace our simple delay-based loader with real API calls:

interface Todo {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
}

async function fetchTodo(id: number) {
  let response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${id}`,
  );
  let data = await response.json();
  return data as Todo;
}

export function loader() {
  let p1 = fetchTodo(1);
  let p2 = fetchTodo(2);
  let p3 = Promise.all([p1, p2]);
  return { p1, p2, p3 };
}

Now we're fetching actual todo items from the JSONPlaceholder API. The deduplication behavior remains consistent, but with more complex objects, the bandwidth savings become more significant:

<script>
  window.__reactRouterContext.streamController.enqueue(
    '[{"_1":2,"_11":-5,"_12":-5},"loaderData",{"_3":4},"routes/home",{"_5":6,"_7":8,"_9":10},"p1",["P",6],"p2",["P",8],"p3",["P",10],"actionData","errors"]\n',
  );
</script>

<script>
  window.__reactRouterContext.streamController.enqueue(
    'P6:[{"_14":15,"_16":15,"_17":18,"_19":20},"userId",1,"id","title","delectus aut autem","completed",false]\n',
  );
</script>

<script>
  window.__reactRouterContext.streamController.enqueue(
    'P8:[{"_14":15,"_16":22,"_17":23,"_19":20},2,"quis ut nam facilis et officia qui"]\n',
  );
</script>

<script>
  window.__reactRouterContext.streamController.enqueue("P10:[[13,21]]\n");
</script>

Even with complex objects containing multiple properties, React Router maintains its reference-based optimization. The P10 chunk still uses references ([[13,21]]) instead of duplicating the entire todo objects.

The component code adapts naturally to the richer data:

export default function Home({ loaderData }: Route.ComponentProps) {
  return (
    <Suspense fallback={<Fallback label="Global" />}>
      <Suspense fallback={<Fallback label="Fallback P1" />}>
        <Await resolve={loaderData.p1}>{(todo) => <p>{todo.title}</p>}</Await>
      </Suspense>

      <Await resolve={loaderData.p2}>{(todo) => <p>{todo.title}</p>}</Await>

      <Suspense fallback={<Fallback label="Fallback P3" />}>
        <Await resolve={loaderData.p3}>
          {(todos) => <p>{todos.map((todo) => todo.title).join(" & ")}</p>}
        </Await>
      </Suspense>
    </Suspense>
  );
}

Why This Optimization Matters

What we've uncovered here goes beyond typical streaming implementations. Most frameworks that support streaming would naively send the resolved data for p3 as a complete array containing both UUIDs or todo objects. React Router's reference-based approach is fundamentally different—and much smarter.

When you think about it, this deduplication becomes incredibly valuable in real applications. Consider a typical e-commerce product page that loads product details, user reviews, and related products simultaneously. Each review contains user information, related products share category data, and the main product itself appears in the "related products" list. Without smart deduplication, you'd transmit the same user objects dozens of times (once per review), duplicate category information across related products, and send the main product data twice. React Router's reference system means each unique piece of data gets transmitted exactly once, regardless of how many times it appears in your promise structure.

The elegance is that you don't have to think about any of this. You write natural promise-based code using Promise.all(), Promise.allSettled(), or any combination logic, and React Router automatically detects the relationships and optimizes the data transfer. It's like having a smart compiler for your data loading patterns.