Repository: TanStack/router
Pull request #1669 (2024-05-25 16:33:07)

fix(react-router): only instantiate the confirmation promise when required in the `useBlocker` hook
Open this PR as @schiller-manuel encouraged based on my bug comment in  https://github.com/TanStack/router/pull/1649#discussion_r1614737330.

Since this is my first contribution to this repo, I only strive to make this dead simple.

I manually tested this code in my own project to see it is now working as expected. I checked the PR #1649 to see if I can amend a test suite. But I found it is tricky to add one because the promise is now only a transient instance. 

c.c. @Balastrong 


vercel[bot]: [vc]: #cdvEnwn5obH1YGuhLprj+xs7gaFwOCUOZulGP+Q25gY=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJzdGFydC1iYXNpYyIsInJvb3REaXJlY3RvcnkiOiJleGFtcGxlcy9yZWFjdC9zdGFydC1iYXNpYyIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS90YW5zdGFjay9zdGFydC1iYXNpYy8zckg3YlVLQWpLTnNOVEQzRHZhblphVXV3YkpTIiwicHJldmlld1VybCI6InN0YXJ0LWJhc2ljLWdpdC1mb3JrLWxlb3lsaS1maXgtdXNlYmxvY2tlci10YW5zdGFjay52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IlBFTkRJTkciLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoic3RhcnQtYmFzaWMtZ2l0LWZvcmstbGVveWxpLWZpeC11c2VibG9ja2VyLXRhbnN0YWNrLnZlcmNlbC5hcHAifX1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **start-basic** | 🔄 Building ([Inspect](https://vercel.com/tanstack/start-basic/3rH7bUKAjKNsNTD3DvanZaUuwbJS)) | [Visit Preview](https://vercel.live/open-feedback/start-basic-git-fork-leoyli-fix-useblocker-tanstack.vercel.app?via=pr-comment-visit-preview-link&passThrough=1) | 💬 [**Add feedback**](https://vercel.live/open-feedback/start-basic-git-fork-leoyli-fix-useblocker-tanstack.vercel.app?via=pr-comment-feedback-link) | May 25, 2024 4:33pm |


schiller-manuel: can the examples be enhanced to show the original problem?
and then I can test your fix with the example
nx-cloud[bot]: # ☁️ Nx Cloud Report

CI is running for commit 35c091c979aa62499630101df56ab5fde56711a4.

[📂 Click to track the progress, see the status, the terminal output, and the build insights.](https://cloud.nx.app/cipes/6652137f253a9d21e656ddb9?utm_source=pull-request&utm_medium=comment)


---

Sent with 💌 from [NxCloud.](https://nx.app?utm_source=pull-request&utm_medium=comment)

Patch:
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index 097fe6159..3d81369b0 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -81,3 +81,42 @@ jobs:
         run: pnpm add -Dw typescript@${{ matrix.typescript-version }}
       - name: Start Type Test
         run: TS_VERSION=${{ matrix.typescript-version }} pnpm run test:types
+  wait-for-vercel:
+    name: Wait for vercel
+    runs-on: ubuntu-latest
+    steps:
+      - name: Wait for Vercel preview deployment to be ready
+        uses: patrickedqvist/wait-for-vercel-preview@v1.3.1
+        id: wait
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+          max_timeout: 120
+    outputs:
+      preview_url: ${{ steps.wait.outputs.url }}
+  e2e-start-basic-test:
+    name: Playwright e2e tests
+    needs: [wait-for-vercel]
+    timeout-minutes: 60
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+      - name: Setup pnpm
+        uses: pnpm/action-setup@v2
+        with:
+          version: 8
+      - name: Setup Node
+        uses: actions/setup-node@v4
+        with:
+          node-version-file: .nvmrc
+          cache: pnpm
+      - name: Install dependencies
+        run: pnpm install --frozen-lockfile --prefer-offline
+      - name: Get Replay Chromium
+        run: npx replayio install
+      - name: Run Playwright tests
+        run: pnpm test:e2e
+        env:
+          APP_BASE_URL: ${{ needs.wait-for-vercel.outputs.preview_url }}
+          REPLAY_API_KEY: ${{ secrets.REPLAY_API_KEY }}
diff --git a/.gitignore b/.gitignore
index 8a6277732..18da8dd5f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,6 +18,7 @@ artifacts
 .rpt2_cache
 coverage
 *.tgz
+**/*app.config.*
 
 # tests
 packages/router-generator/tests/**/*.gen.ts
@@ -61,4 +62,4 @@ gpt/db.json
 vite.config.js.timestamp-*
 vite.config.ts.timestamp-*
 .idea
-*.vitest-temp.json
\ No newline at end of file
+*.vitest-temp.json
diff --git a/docs/framework/react/api/router/RouterType.md b/docs/framework/react/api/router/RouterType.md
index 1b2b9b83e..6cea3e509 100644
--- a/docs/framework/react/api/router/RouterType.md
+++ b/docs/framework/react/api/router/RouterType.md
@@ -171,48 +171,6 @@ Matches a pathname and search params against the router's route tree and returns
   - A route match object if a match was found.
   - `false` if no match was found.
 
-### `.injectHtml` method
-
-Injects the HTML for the router into the DOM.
-
-- Type: `(html: string | (() => Promise<string> | string)) => void`
-- Properties
-  - `html`
-    - Type: `string | (() => Promise<string> | string)`
-    - Required
-    - The HTML to inject into the DOM.
-    - If a function is provided, it will be called and the resolved value will be injected into the DOM.
-
-### `.dehydrateData` method
-
-Dehydrates data into the router on the server that can be streamed to the client and rehydrated later.
-
-- Type: `<T>(key: any, getData: T | (() => Promise<T> | T)) => () => void`
-- Properties
-  - `key`
-    - Type: `any`
-    - Required
-    - The key that will be used to identify the dehydrated data.
-  - `getData`
-    - Type: `T | (() => Promise<T> | T)`
-    - Required
-    - The data that will be dehydrated.
-    - If a function is provided, it will be called and the resolved value will be dehydrated.
-
-### `.hydrateData` method
-
-Hydrates data into the router on the client that was dehydrated on the server.
-
-- Type: `<T>(key: any) => T | undefined`
-- Properties
-  - `key`
-    - Type: `any`
-    - Required
-    - The key that was used to identify the dehydrated data.
-- Returns
-  - The dehydrated data if it exists.
-  - `undefined` if the dehydrated data does not exist.
-
 ### `.dehydrate` method
 
 Dehydrates the router's critical state into a serializable object that can be sent to the client in an initial request.
diff --git a/docs/framework/react/guide/deferred-data-loading.md b/docs/framework/react/guide/deferred-data-loading.md
index 052cc0d9b..3022991b9 100644
--- a/docs/framework/react/guide/deferred-data-loading.md
+++ b/docs/framework/react/guide/deferred-data-loading.md
@@ -93,7 +93,7 @@ The following is a high-level overview of how deferred data streaming works with
   - The client receives the initial html from the server
   - `<Await>` components suspend with placeholder promises while they wait for their data to resolve on the server
 - Server
-  - As deferred promises resolve, their results (or errors) are serialized and streamed to the client (via Router's `router.dehydrateData()` and `router.hydrateData()` methods)
+  - As deferred promises resolve, their results (or errors) are serialized and streamed to the client via an inline script tag
   - The resolved `<Await>` components and their suspense boundaries are resolved and their resulting HTML is streamed to the client along with their dehydrated data
 - Client
   - The suspended placeholder promises within `<Await>` are resolved with the streamed data/error responses and either render the result or throw the error to the nearest error boundary
diff --git a/docs/framework/react/guide/external-data-loading.md b/docs/framework/react/guide/external-data-loading.md
index 3e079c7c3..bb61eb459 100644
--- a/docs/framework/react/guide/external-data-loading.md
+++ b/docs/framework/react/guide/external-data-loading.md
@@ -189,121 +189,3 @@ export function createRouter() {
   })
 }
 ```
-
-### 3rd Party Streaming with `transformStreamWithRouter`
-
-- The `router.injectHtml` function
-  - e.g. `router.injectHtml(() => '<script>console.log("Hello World!")</script>')`
-  - This function can be called multiple times during rendering to inject arbitrary HTML markup into the stream.
-  - Use it to inject things like `<script>` tags, `<style>` tags, or any other arbitrary HTML markup.
-  - 🧠 Make sure you inject your HTML right after a successfully rendered Suspense boundary to ensure that the HTML is injected at the right time.
-- The `router.dehydrateData` function
-  - e.g. `router.dehydrateData('foo', () => ({ bar: 'baz' }))`
-  - This function is a higher-level abstraction around `router.injectHtml`, designed for injecting JSON under a specific key. It can be called multiple times during rendering to inject arbitrary JSON data into the stream under a specific key which can be retrieved later on the client using the `router.hydrateData` function.
-  - Use it to inject things like dehydrated data for your application.
-  - 🧠 Make sure you inject your data right after a successfully rendered Suspense boundary to ensure that the data is injected in unison with the corresponding markup that requires it in the stream.
-- The `router.hydrateData` function
-  - e.g. `router.hydrateData('foo')`
-  - This function is a companion to `router.dehydrateData`, designed for retrieving JSON data that was injected into the stream using `router.dehydrateData`.
-  - Use it to retrieve things like dehydrated data for your application.
-  - 🧠 Make sure you retrieve your data as early as possible on the client to ensure that it is available for hydration when your application renders.
-
-Let's take a look at an example of how to use these utilities to stream a TanStack Router application.
-
-### Injecting HTML
-
-Now that we have a stream that is being transformed by our router, we can inject arbitrary HTML markup into the stream using the `router.injectHtml` function.
-
-```tsx
-function Test() {
-  const router = useRouter()
-  router.injectHtml(() => '<script>console.log("Hello World!")</script>')
-  return null
-}
-```
-
-### Dehydrating and Hydrating Deferred Data
-
-Injecting HTML is pretty low-level, so let's use the `router.dehydrateData` and `router.hydrateData` functions to inject and retrieve some JSON data instead.
-
-```tsx
-function Custom() {
-  return (
-    <Suspense fallback={<div>Loading...</div>}>
-      <Inner />
-    </Suspense>
-  )
-}
-
-let testCache: string
-
-function Test() {
-  const router = useRouter()
-
-  // Attempt to rehydrate our data on the client
-  // On the server, this is a no=op and
-  // will return undefined
-  const data = router.hydrateData('testCache')
-
-  // Suspend and load our fake data
-  if (!testCache) {
-    throw new Promise((resolve) => {
-      setTimeout(() => {
-        testCache = Date.now()
-        resolve()
-      }, 1000)
-    })
-  }
-
-  // Dehydrate our data on the server so it can
-  // be rehydrated on the client
-  router.dehydrateData('testCache', testCache)
-
-  return data
-}
-```
-
-### Providing Dehydration/Hydration utilities to external tools
-
-The `router.dehydrateData` and `router.hydrateData` functions are designed to be used by external tools to dehydrate and hydrate data. For example, imagine an external data fetching library called `cool-cache` (not real) that allows for custom hydration and dehydration functions to be provided. We could use the `router.dehydrateData` and `router.hydrateData` functions to provide hydration and dehydration capabilities to `cool-cache`:
-
-```tsx
-// src/router.tsx
-
-export function createRouter() {
-  const coolCache = createCoolCache()
-
-  const router = createRouter({
-    ...
-  })
-
-  coolCache.setHydrationFn((coolCacheEntry) =>
-      router.hydrateData(coolCacheEntry.hashedKey))
-
-  coolCache.setDehydrationFn((coolCacheEntry) =>
-      router.dehydrateData(coolCacheEntry.hashedKey, () => coolCacheEntry))
-
-  return router
-}
-```
-
-This would allow `cool-cache` to be used with TanStack Router and have its data automatically hydrated and dehydrated with no extra work.
-
-```tsx
-// src/components/MyComponent.tsx
-
-import * as React from 'react'
-
-const coolCacheFetcher = createCoolCacheFetcher({
-  key: 'todos',
-  fetchFn: () => fetchTodos(),
-})
-
-export function Test() {
-  // On the client, hydration would happen before `useCoolCacheFetcher` returns the data
-  const data = useCoolCacheFetcher(coolCacheFetcher)
-  // On the server, dehydration would happen after `useCoolCacheFetcher` is able to resolve the data
-
-  return <>...</>
-}
-```
diff --git a/docs/framework/react/guide/ssr.md b/docs/framework/react/guide/ssr.md
index 6714758c3..d7bd35fc1 100644
--- a/docs/framework/react/guide/ssr.md
+++ b/docs/framework/react/guide/ssr.md
@@ -221,56 +221,11 @@ Streaming SSR is the most modern flavor of SSR and is the process of continuousl
 
 This pattern can be useful for pages that have slow or high-latency data fetching requirements. For example, if you have a page that needs to fetch data from a third-party API, you can stream the critical initial markup and data to the client and then stream the less-critical third-party data to the client as it is resolved.
 
-To enable this streaming pattern with TanStack Router, you will need to use React's `renderToPipeableStream` function to render your application to a readable stream. This function returns a stream that can be piped to the response. Here's the utility information:
+**This streaming pattern is all automatic as long as you are using `renderToPipeableStream`**.
 
-- `transformStreamWithRouter` from `@tanstack/start`
-  - e.g. `transformStreamWithRouter(router)`
-  - This function returns a stream Transform instance that can be used to transform a stream of HTML markup from React DOM's `renderToPipeableStream` function as it is piped to the response.
-  - This transform automatically and incrementally embeds fine-grained HTML injections and dehydrated data chunks into the stream.
+## Streaming Dehydration/Hydration
 
-### Transforming the Stream
-
-Let's implement the `transformStreamWithRouter` function from `@tanstack/start` to transform the stream of HTML markup from React DOM's `renderToPipeableStream` function as it is piped to the response.
-
-```tsx
-// Render the app to a readable stream
-let stream!: PipeableStream
-
-await new Promise<void>((resolve) => {
-  stream = ReactDOMServer.renderToPipeableStream(
-    <StartServer router={router} />,
-    {
-      [callbackName]: () => {
-        res.statusCode = didError ? 500 : 200
-        res.setHeader('Content-Type', 'text/html')
-        resolve()
-      },
-      onError: (err) => {
-        didError = true
-        console.log(err)
-      },
-    },
-  )
-})
-
-if (router.hasNotFoundMatch() && res.statusCode !== 500) {
-  res.statusCode = 404
-}
-
-// Add our Router transform to the stream
-const transforms = [transformStreamWithRouter(router)]
-
-// Pipe the stream through our transforms
-const transformedStream = transforms.reduce(
-  (stream, transform) => stream.pipe(transform as any),
-  stream,
-)
-
-// Pipe the transformed stream to the response
-transformedStream.pipe(res)
-```
-
-With `renderToPipeableStream` and `transformStreamWithRouter`, TanStack Router is now configured to stream data to the client as it is rendered on the server!
+Streaming dehydration/hydration is an advanced pattern that goes beyond markup and allows you to dehydrate and stream any supporting data from the server to the client and rehydrate it on arrival. This is useful for applications that may need to further use/manage the underlying data that was used to render the initial markup on the server.
 
 ## Data Transformers
 
diff --git a/examples/react/authenticated-routes/package.json b/examples/react/authenticated-routes/package.json
index 54d6aa6bc..2d460afac 100644
--- a/examples/react/authenticated-routes/package.json
+++ b/examples/react/authenticated-routes/package.json
@@ -9,9 +9,9 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.34.0",
-    "@tanstack/router-devtools": "^1.34.0",
-    "@tanstack/router-vite-plugin": "^1.33.12",
+    "@tanstack/react-router": "^1.34.6",
+    "@tanstack/router-devtools": "^1.34.6",
+    "@tanstack/router-vite-plugin": "^1.34.1",
     "redaxios": "^0.5.0",
     "immer": "^10.0.3",
     "react": "^18.2.0",
diff --git a/examples/react/basic-default-search-params/package.json b/examples/react/basic-default-search-params/package.json
index fdec44e6c..58839d90f 100644
--- a/examples/react/basic-default-search-params/package.json
+++ b/examples/react/basic-default-search-params/package.json
@@ -10,8 +10,8 @@
   },
   "dependencies": {
     "@tanstack/react-query": "^5.17.8",
-    "@tanstack/react-router": "^1.34.0",
-    "@tanstack/router-devtools": "^1.34.0",
+    "@tanstack/react-router": "^1.34.6",
+    "@tanstack/router-devtools": "^1.34.6",
     "redaxios": "^0.5.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
diff --git a/examples/react/basic-file-based-codesplitting/package.json b/examples/react/basic-file-based-codesplitting/package.json
index cab09e905..ce1fe75eb 100644
--- a/examples/react/basic-file-based-codesplitting/package.json
+++ b/examples/react/basic-file-based-codesplitting/package.json
@@ -9,9 +9,9 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.34.0",
-    "@tanstack/router-devtools": "^1.34.0",
-    "@tanstack/router-vite-plugin": "^1.33.12",
+    "@tanstack/react-router": "^1.34.6",
+    "@tanstack/router-devtools": "^1.34.6",
+    "@tanstack/router-vite-plugin": "^1.34.1",
     "redaxios": "^0.5.0",
     "immer": "^10.0.3",
     "react": "^18.2.0",
diff --git a/examples/react/basic-file-based/package.json b/examples/react/basic-file-based/package.json
index 83548be17..e52856744 100644
--- a/examples/react/basic-file-based/package.json
+++ b/examples/react/basic-file-based/package.json
@@ -9,9 +9,9 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.34.0",
-    "@tanstack/router-devtools": "^1.34.0",
-    "@tanstack/router-vite-plugin": "^1.33.12",
+    "@tanstack/react-router": "^1.34.6",
+    "@tanstack/router-devtools": "^1.34.6",
+    "@tanstack/router-vite-plugin": "^1.34.1",
     "redaxios": "^0.5.0",
     "immer": "^10.0.3",
     "react": "^18.2.0",
diff --git a/examples/react/basic-react-query-file-based/package.json b/examples/react/basic-react-query-file-based/package.json
index 8a12b4b3f..446bef687 100644
--- a/examples/react/basic-react-query-file-based/package.json
+++ b/examples/react/basic-react-query-file-based/package.json
@@ -11,9 +11,9 @@
   "dependencies": {
     "@tanstack/react-query": "^5.17.8",
     "@tanstack/react-query-devtools": "^5.17.8",
-    "@tanstack/react-router": "^1.34.0",
-    "@tanstack/router-devtools": "^1.34.0",
-    "@tanstack/router-vite-plugin": "^1.33.12",
+    "@tanstack/react-router": "^1.34.6",
+    "@tanstack/router-devtools": "^1.34.6",
+    "@tanstack/router-vite-plugin": "^1.34.1",
     "redaxios": "^0.5.0",
     "immer": "^10.0.3",
     "react": "^18.2.0",
diff --git a/examples/react/basic-react-query/package.json b/examples/react/basic-react-query/package.json
index aa1dda492..3bb2650ba 100644
--- a/examples/react/basic-react-query/package.json
+++ b/examples/react/basic-react-query/package.json
@@ -11,8 +11,8 @@
   "dependencies": {
     "@tanstack/react-query": "^5.17.8",
     "@tanstack/react-query-devtools": "^5.17.8",
-    "@tanstack/react-router": "^1.34.0",
-    "@tanstack/router-devtools": "^1.34.0",
+    "@tanstack/react-router": "^1.34.6",
+    "@tanstack/router-devtools": "^1.34.6",
     "redaxios": "^0.5.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0"
diff --git a/examples/react/basic-ssr-file-based/package.json b/examples/react/basic-ssr-file-based/package.json
index 8ea60ebdf..d32bc0c27 100644
--- a/examples/react/basic-ssr-file-based/package.json
+++ b/examples/react/basic-ssr-file-based/package.json
@@ -12,10 +12,10 @@
     "debug": "node --inspect-brk server"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.34.0",
-    "@tanstack/start": "^1.34.0",
-    "@tanstack/router-devtools": "^1.34.0",
-    "@tanstack/router-vite-plugin": "^1.33.12",
+    "@tanstack/react-router": "^1.34.6",
+    "@tanstack/start": "^1.34.6",
+    "@tanstack/router-devtools": "^1.34.6",
+    "@tanstack/router-vite-plugin": "^1.34.1",
     "redaxios": "^0.5.0",
     "get-port": "^7.0.0",
     "react": "^18.2.0",
diff --git a/examples/react/basic-ssr-streaming-file-based/package.json b/examples/react/basic-ssr-streaming-file-based/package.json
index 282698a80..a59da22f1 100644
--- a/examples/react/basic-ssr-streaming-file-based/package.json
+++ b/examples/react/basic-ssr-streaming-file-based/package.json
@@ -12,10 +12,10 @@
     "debug": "node --inspect-brk server"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.34.0",
-    "@tanstack/start": "^1.34.0",
-    "@tanstack/router-devtools": "^1.34.0",
-    "@tanstack/router-vite-plugin": "^1.33.12",
+    "@tanstack/react-router": "^1.34.6",
+    "@tanstack/start": "^1.34.6",
+    "@tanstack/router-devtools": "^1.34.6",
+    "@tanstack/router-vite-plugin": "^1.34.1",
     "redaxios": "^0.5.0",
     "get-port": "^7.0.0",
     "react": "^18.2.0",
diff --git a/examples/react/basic-ssr-streaming-file-based/src/entry-server.tsx b/examples/react/basic-ssr-streaming-file-based/src/entry-server.tsx
index 6893cc75e..95c6d8577 100644
--- a/examples/react/basic-ssr-streaming-file-based/src/entry-server.tsx
+++ b/examples/react/basic-ssr-streaming-file-based/src/entry-server.tsx
@@ -1,14 +1,14 @@
-import * as React from 'react'
-import ReactDOMServer, { PipeableStream } from 'react-dom/server'
-import { createMemoryHistory } from '@tanstack/react-router'
-import { StartServer, transformStreamWithRouter } from '@tanstack/start/server'
+import { PassThrough } from 'stream'
+import { StartServer } from '@tanstack/start/server'
 import { isbot } from 'isbot'
-import { ServerResponse } from 'http'
-import express from 'express'
+import { renderToPipeableStream } from 'react-dom/server'
+import { createMemoryHistory } from '@tanstack/react-router'
+import { createRouter } from './router'
+import type express from 'express'
+import type { ServerResponse } from 'http'
 
 // index.js
 import './fetch-polyfill'
-import { createRouter } from './router'
 
 type ReactReadableStream = ReadableStream<Uint8Array> & {
   allReady?: Promise<void> | undefined
@@ -38,44 +38,30 @@ export async function render(opts: {
   // (streamed data will continue to load in the background)
   await router.load()
 
-  // Track errors
-  let statusCode = 200
-
-  // Clever way to get the right callback. Thanks Remix!
-  const callbackName = isbot(opts.req.headers['user-agent'])
-    ? 'onAllReady'
-    : 'onShellReady'
+  const passthrough = new PassThrough()
 
-  // Render the app to a readable stream
-  let stream!: PipeableStream
-
-  await new Promise<void>((resolve) => {
-    stream = ReactDOMServer.renderToPipeableStream(
-      <StartServer router={router} />,
-      {
-        [callbackName]: () => {
-          resolve()
-        },
-        onError: (err) => {
-          statusCode = 500
-          console.log(err)
-        },
-      },
-    )
+  const pipeable = renderToPipeableStream(<StartServer router={router} />, {
+    ...(isbot(opts.req.headers['user-agent'])
+      ? {
+          onAllReady() {
+            pipeable.pipe(passthrough)
+          },
+        }
+      : {
+          onShellReady() {
+            pipeable.pipe(passthrough)
+          },
+        }),
+    onShellError(err) {
+      throw err
+    },
   })
 
-  if (router.hasNotFoundMatch() && statusCode !== 500) statusCode = 404
-
-  opts.res.statusCode = statusCode
   opts.res.setHeader('Content-Type', 'text/html')
+  opts.res.statusCode = router.state.statusCode
+  opts.res.statusMessage = `${router.state.statusCode}`.startsWith('5')
+    ? 'Internal Server Error'
+    : 'OK'
 
-  // Add our Router transform to the stream
-  const transforms = [transformStreamWithRouter(router)]
-
-  const transformedStream = transforms.reduce(
-    (stream, transform) => stream.pipe(transform as any),
-    stream,
-  )
-
-  transformedStream.pipe(opts.res)
+  pipeable.pipe(opts.res)
 }
diff --git a/examples/react/basic/package.json b/examples/react/basic/package.json
index 98c075c19..de5d8481c 100644
--- a/examples/react/basic/package.json
+++ b/examples/react/basic/package.json
@@ -10,8 +10,8 @@
   },
   "dependencies": {
     "@tanstack/react-query": "^5.17.8",
-    "@tanstack/react-router": "^1.34.0",
-    "@tanstack/router-devtools": "^1.34.0",
+    "@tanstack/react-router": "^1.34.6",
+    "@tanstack/router-devtools": "^1.34.6",
     "redaxios": "^0.5.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0"
diff --git a/examples/react/deferred-data/package.json b/examples/react/deferred-data/package.json
index b83e60cac..d3bce8b2c 100644
--- a/examples/react/deferred-data/package.json
+++ b/examples/react/deferred-data/package.json
@@ -9,8 +9,8 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.34.0",
-    "@tanstack/router-devtools": "^1.34.0",
+    "@tanstack/react-router": "^1.34.6",
+    "@tanstack/router-devtools": "^1.34.6",
     "redaxios": "^0.5.0",
     "immer": "^10.0.3",
     "react": "^18.2.0",
diff --git a/examples/react/kitchen-sink-file-based/package.json b/examples/react/kitchen-sink-file-based/package.json
index 943fd0bec..d9713f3ab 100644
--- a/examples/react/kitchen-sink-file-based/package.json
+++ b/examples/react/kitchen-sink-file-based/package.json
@@ -9,9 +9,9 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.34.0",
-    "@tanstack/router-devtools": "^1.34.0",
-    "@tanstack/router-vite-plugin": "^1.33.12",
+    "@tanstack/react-router": "^1.34.6",
+    "@tanstack/router-devtools": "^1.34.6",
+    "@tanstack/router-vite-plugin": "^1.34.1",
     "redaxios": "^0.5.0",
     "immer": "^10.0.3",
     "react": "^18.2.0",
diff --git a/examples/react/kitchen-sink-react-query-file-based/package.json b/examples/react/kitchen-sink-react-query-file-based/package.json
index b8bb67d45..f8f2c8a0a 100644
--- a/examples/react/kitchen-sink-react-query-file-based/package.json
+++ b/examples/react/kitchen-sink-react-query-file-based/package.json
@@ -11,9 +11,9 @@
   "dependencies": {
     "@tanstack/react-query": "^5.17.8",
     "@tanstack/react-query-devtools": "^5.17.8",
-    "@tanstack/react-router": "^1.34.0",
-    "@tanstack/router-devtools": "^1.34.0",
-    "@tanstack/router-vite-plugin": "^1.33.12",
+    "@tanstack/react-router": "^1.34.6",
+    "@tanstack/router-devtools": "^1.34.6",
+    "@tanstack/router-vite-plugin": "^1.34.1",
     "redaxios": "^0.5.0",
     "immer": "^10.0.3",
     "react": "^18.2.0",
diff --git a/examples/react/kitchen-sink-react-query/package.json b/examples/react/kitchen-sink-react-query/package.json
index eeafced45..5a4ae18fa 100644
--- a/examples/react/kitchen-sink-react-query/package.json
+++ b/examples/react/kitchen-sink-react-query/package.json
@@ -11,8 +11,8 @@
   "dependencies": {
     "@tanstack/react-query": "^5.17.8",
     "@tanstack/react-query-devtools": "^5.17.8",
-    "@tanstack/react-router": "^1.34.0",
-    "@tanstack/router-devtools": "^1.34.0",
+    "@tanstack/react-router": "^1.34.6",
+    "@tanstack/router-devtools": "^1.34.6",
     "redaxios": "^0.5.0",
     "immer": "^10.0.3",
     "react": "^18.2.0",
diff --git a/examples/react/kitchen-sink/package.json b/examples/react/kitchen-sink/package.json
index f67c12a74..f738f33eb 100644
--- a/examples/react/kitchen-sink/package.json
+++ b/examples/react/kitchen-sink/package.json
@@ -9,8 +9,8 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.34.0",
-    "@tanstack/router-devtools": "^1.34.0",
+    "@tanstack/react-router": "^1.34.6",
+    "@tanstack/router-devtools": "^1.34.6",
     "redaxios": "^0.5.0",
     "immer": "^10.0.3",
     "react": "^18.2.0",
diff --git a/examples/react/large-file-based/package.json b/examples/react/large-file-based/package.json
index 8dc33a954..9c0e6c7a5 100644
--- a/examples/react/large-file-based/package.json
+++ b/examples/react/large-file-based/package.json
@@ -12,9 +12,9 @@
   },
   "dependencies": {
     "@tanstack/react-query": "^5.17.8",
-    "@tanstack/react-router": "^1.34.0",
-    "@tanstack/router-devtools": "^1.34.0",
-    "@tanstack/router-vite-plugin": "^1.33.12",
+    "@tanstack/react-router": "^1.34.6",
+    "@tanstack/router-devtools": "^1.34.6",
+    "@tanstack/router-vite-plugin": "^1.34.1",
     "redaxios": "^0.5.0",
     "immer": "^10.0.3",
     "react": "^18.2.0",
diff --git a/examples/react/location-masking/package.json b/examples/react/location-masking/package.json
index fbbbf297b..b29c8dad9 100644
--- a/examples/react/location-masking/package.json
+++ b/examples/react/location-masking/package.json
@@ -11,8 +11,8 @@
   "dependencies": {
     "@radix-ui/react-dialog": "^1.0.5",
     "@tanstack/react-query": "^5.17.8",
-    "@tanstack/react-router": "^1.34.0",
-    "@tanstack/router-devtools": "^1.34.0",
+    "@tanstack/react-router": "^1.34.6",
+    "@tanstack/router-devtools": "^1.34.6",
     "redaxios": "^0.5.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0"
diff --git a/examples/react/navigation-blocking/package.json b/examples/react/navigation-blocking/package.json
index 2ce159a7e..cc12e5b47 100644
--- a/examples/react/navigation-blocking/package.json
+++ b/examples/react/navigation-blocking/package.json
@@ -10,8 +10,8 @@
   },
   "dependencies": {
     "@tanstack/react-query": "^5.17.8",
-    "@tanstack/react-router": "^1.34.0",
-    "@tanstack/router-devtools": "^1.34.0",
+    "@tanstack/react-router": "^1.34.6",
+    "@tanstack/router-devtools": "^1.34.6",
     "redaxios": "^0.5.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0"
diff --git a/examples/react/quickstart-file-based/package.json b/examples/react/quickstart-file-based/package.json
index 0f0fef043..acf622405 100644
--- a/examples/react/quickstart-file-based/package.json
+++ b/examples/react/quickstart-file-based/package.json
@@ -9,9 +9,9 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.34.0",
-    "@tanstack/router-devtools": "^1.34.0",
-    "@tanstack/router-vite-plugin": "^1.33.12",
+    "@tanstack/react-router": "^1.34.6",
+    "@tanstack/router-devtools": "^1.34.6",
+    "@tanstack/router-vite-plugin": "^1.34.1",
     "redaxios": "^0.5.0",
     "immer": "^10.0.3",
     "react": "^18.2.0",
diff --git a/examples/react/quickstart/package.json b/examples/react/quickstart/package.json
index 64da407d7..1a4183255 100644
--- a/examples/react/quickstart/package.json
+++ b/examples/react/quickstart/package.json
@@ -9,8 +9,8 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.34.0",
-    "@tanstack/router-devtools": "^1.34.0",
+    "@tanstack/react-router": "^1.34.6",
+    "@tanstack/router-devtools": "^1.34.6",
     "react": "^18.2.0",
     "react-dom": "^18.2.0"
   },
diff --git a/examples/react/scroll-restoration/package.json b/examples/react/scroll-restoration/package.json
index 39623331b..031f92941 100644
--- a/examples/react/scroll-restoration/package.json
+++ b/examples/react/scroll-restoration/package.json
@@ -9,9 +9,9 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.34.0",
+    "@tanstack/react-router": "^1.34.6",
     "@tanstack/react-virtual": "^3.0.1",
-    "@tanstack/router-devtools": "^1.34.0",
+    "@tanstack/router-devtools": "^1.34.6",
     "react": "^18.2.0",
     "react-dom": "^18.2.0"
   },
diff --git a/examples/react/start-basic-rsc/app/client.tsx b/examples/react/start-basic-rsc/app/client.tsx
index 48a41989d..f16ba73f6 100644
--- a/examples/react/start-basic-rsc/app/client.tsx
+++ b/examples/react/start-basic-rsc/app/client.tsx
@@ -1,9 +1,7 @@
 import { hydrateRoot } from 'react-dom/client'
 import { StartClient } from '@tanstack/start'
 import { createRouter } from './router'
-import { Suspense } from 'react'
 
 const router = createRouter()
 
-// hydrateRoot(document, <StartClient router={router} />)
 hydrateRoot(document.getElementById('root')!, <StartClient router={router} />)
diff --git a/examples/react/start-basic-rsc/app/server.tsx b/examples/react/start-basic-rsc/app/server.tsx
deleted file mode 100644
index b780e4e6d..000000000
--- a/examples/react/start-basic-rsc/app/server.tsx
+++ /dev/null
@@ -1,169 +0,0 @@
-/// <reference types="vinxi/types/server" />
-import type { PipeableStream } from 'react-dom/server'
-import { renderToPipeableStream } from 'react-dom/server'
-import { eventHandler, getResponseHeaders, toWebRequest } from 'vinxi/server'
-import { getManifest } from 'vinxi/manifest'
-import { StartServer, transformStreamWithRouter } from '@tanstack/start/server'
-// import { Transform, PassThrough } from 'node:stream'
-
-import { createRouter } from './router'
-import { createMemoryHistory } from '@tanstack/react-router'
-import {
-  serverFnPayloadTypeHeader,
-  serverFnReturnTypeHeader,
-} from '@tanstack/start'
-import isbot from 'isbot'
-
-export default eventHandler(async (event) => {
-  const req = toWebRequest(event)
-  const url = new URL(req.url)
-  const href = url.href.replace(url.origin, '')
-
-  // Get assets for the server/client
-  const clientManifest = getManifest('client')
-  // let assets = (
-  //   await clientManifest.inputs[clientManifest.handler].assets()
-  // ).filter((d: any) => {
-  //   return !d.children?.includes('nuxt-devtools')
-  // }) as any
-
-  const assets = []
-
-  if (import.meta.env.DEV) {
-    assets.push({
-      tag: 'script',
-      children: `window.__vite_plugin_react_preamble_installed__ = true`,
-    })
-  }
-
-  assets.push({
-    tag: 'script',
-    attrs: {
-      src: clientManifest.inputs[clientManifest.handler].output.path,
-      type: 'module',
-      async: true,
-      suppressHydrationWarning: true,
-    },
-  })
-
-  // Create a router
-  const router = createRouter()
-
-  // Create a history for the router
-  const history = createMemoryHistory({
-    initialEntries: [href],
-  })
-
-  // Update the router with the history and context
-  router.update({
-    history,
-    context: {
-      assets,
-    },
-  })
-
-  await router.load()
-
-  // Handle Redirects
-  const { redirect } = router.state
-
-  if (redirect) {
-    console.info('Redirecting...', redirect.statusCode, redirect.href)
-    return new Response(null, {
-      status: redirect.statusCode,
-      headers: {
-        ...redirect.headers,
-        Location: redirect.href,
-      },
-    })
-  }
-
-  const isRobot = isbot(req.headers.get('User-Agent'))
-
-  const stream = await new Promise<PipeableStream>(async (resolve) => {
-    // Yes, I'm shadowing "stream" again, keep your shirt on!
-    const stream = renderToPipeableStream(<StartServer router={router} />, {
-      ...(isRobot
-        ? {
-            onAllReady() {
-              resolve(stream)
-            },
-          }
-        : {
-            onShellReady() {
-              resolve(stream)
-            },
-          }),
-    })
-  })
-
-  // Add our Router transform to the stream
-  const transforms = [
-    transformStreamWithRouter(router),
-    // // A transform that logs chunks
-    // new Transform({
-    //   transform(chunk, encoding, callback) {
-    //     const str = chunk.toString()
-    //     console.info('')
-    //     console.info('CHUNK')
-    //     console.info('')
-    //     console.info(str)
-    //     this.push(str)
-    //     return callback()
-    //   },
-    // }),
-  ]
-
-  // Pipe the stream through our transforms
-  const transformedStream = transforms.reduce(
-    (stream, transform) => stream.pipe(transform as any),
-    stream,
-  )
-
-  ;(event as any).__tsrHeadersSent = true
-
-  let headers = {
-    ...getResponseHeaders(event),
-    'Content-Type': 'text/html',
-    ...router.state.matches.reduce((acc, match) => {
-      if (match.headers) {
-        Object.assign(acc, match.headers)
-      }
-      return acc
-    }, {}),
-  }
-
-  // Remove server function headers
-  ;[serverFnReturnTypeHeader, serverFnPayloadTypeHeader].forEach((header) => {
-    delete headers[header]
-  })
-
-  // Dedupe headers
-  headers = dedupeHeaders(Object.entries(headers))
-
-  return new Response(transformedStream as any, {
-    status: router.state.statusCode,
-    statusText: `${router.state.statusCode}`.startsWith('5')
-      ? 'Internal Server Error'
-      : 'OK',
-    headers,
-  })
-})
-
-function dedupeHeaders(headerList: [string, string][]): [string, string][] {
-  // Object to store the deduplicated headers
-  const seen = new Set()
-
-  // Reverse, filter with seen, then reverse again
-  return [...headerList]
-    .reverse()
-    .filter(([name]) => {
-      const key = name.toLowerCase()
-      if (seen.has(key)) {
-        return false
-      }
-      seen.add(key)
-      return true
-    })
-    .reverse()
-}
diff --git a/examples/react/start-basic-rsc/app/ssr.tsx b/examples/react/start-basic-rsc/app/ssr.tsx
new file mode 100644
index 000000000..79ac23f57
--- /dev/null
+++ b/examples/react/start-basic-rsc/app/ssr.tsx
@@ -0,0 +1,12 @@
+import {
+  createRequestHandler,
+  defaultStreamHandler,
+} from '@tanstack/start/server'
+import { getRouterManifest } from '@tanstack/start/router-manifest'
+
+import { createRouter } from './router'
+
+export default createRequestHandler({
+  createRouter,
+  getRouterManifest,
+})(defaultStreamHandler)
diff --git a/examples/react/start-basic-rsc/package.json b/examples/react/start-basic-rsc/package.json
index 3872bc72a..0ba82cf70 100644
--- a/examples/react/start-basic-rsc/package.json
+++ b/examples/react/start-basic-rsc/package.json
@@ -12,10 +12,10 @@
     "format": "prettier --write '**/*' --ignore-unknown"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.34.0",
-    "@tanstack/router-devtools": "^1.34.0",
-    "@tanstack/router-vite-plugin": "^1.33.12",
-    "@tanstack/start": "^1.34.0",
+    "@tanstack/react-router": "^1.34.6",
+    "@tanstack/router-devtools": "^1.34.6",
+    "@tanstack/router-vite-plugin": "^1.34.1",
+    "@tanstack/start": "^1.34.6",
     "redaxios": "^0.5.0",
     "isbot": "^3.7.0",
     "react": "^18.3.1",
diff --git a/examples/react/start-basic/.gitignore b/examples/react/start-basic/.gitignore
index d3387e00c..be342025d 100644
--- a/examples/react/start-basic/.gitignore
+++ b/examples/react/start-basic/.gitignore
@@ -16,3 +16,7 @@ yarn.lock
 .vinxi
 # Sentry Config File
 .env.sentry-build-plugin
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/examples/react/start-basic/app.config.timestamp_1716499485731.js b/examples/react/start-basic/app.config.timestamp_1716499485731.js
deleted file mode 100644
index 7731c85d7..000000000
--- a/examples/react/start-basic/app.config.timestamp_1716499485731.js
+++ /dev/null
@@ -1,13 +0,0 @@
-// app.config.ts
-import { defineConfig } from '@tanstack/start/config'
-import tsConfigPaths from 'vite-tsconfig-paths'
-var app_config_default = defineConfig({
-  vite: {
-    plugins: () => [
-      tsConfigPaths({
-        projects: ['./tsconfig.json'],
-      }),
-    ],
-  },
-})
-export { app_config_default as default }
diff --git a/examples/react/start-basic/app/client.tsx b/examples/react/start-basic/app/client.tsx
index 48a41989d..f16ba73f6 100644
--- a/examples/react/start-basic/app/client.tsx
+++ b/examples/react/start-basic/app/client.tsx
@@ -1,9 +1,7 @@
 import { hydrateRoot } from 'react-dom/client'
 import { StartClient } from '@tanstack/start'
 import { createRouter } from './router'
-import { Suspense } from 'react'
 
 const router = createRouter()
 
-// hydrateRoot(document, <StartClient router={router} />)
 hydrateRoot(document.getElementById('root')!, <StartClient router={router} />)
diff --git a/examples/react/start-basic/app/routeTree.gen.ts b/examples/react/start-basic/app/routeTree.gen.ts
index 1ca7aa861..144eedd9b 100644
--- a/examples/react/start-basic/app/routeTree.gen.ts
+++ b/examples/react/start-basic/app/routeTree.gen.ts
@@ -11,7 +11,9 @@
 // Import Routes
 
 import { Route as rootRoute } from './routes/__root'
+import { Route as RedirectImport } from './routes/redirect'
 import { Route as PostsImport } from './routes/posts'
+import { Route as DeferredImport } from './routes/deferred'
 import { Route as LayoutImport } from './routes/_layout'
 import { Route as IndexImport } from './routes/index'
 import { Route as PostsIndexImport } from './routes/posts.index'
@@ -23,11 +25,21 @@ import { Route as LayoutLayout2LayoutAImport } from './routes/_layout/_layout-2/
 
 // Create/Update Routes
 
+const RedirectRoute = RedirectImport.update({
+  path: '/redirect',
+  getParentRoute: () => rootRoute,
+} as any)
+
 const PostsRoute = PostsImport.update({
   path: '/posts',
   getParentRoute: () => rootRoute,
 } as any)
 
+const DeferredRoute = DeferredImport.update({
+  path: '/deferred',
+  getParentRoute: () => rootRoute,
+} as any)
+
 const LayoutRoute = LayoutImport.update({
   id: '/_layout',
   getParentRoute: () => rootRoute,
@@ -86,6 +98,13 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof LayoutImport
       parentRoute: typeof rootRoute
     }
+    '/deferred': {
+      id: '/deferred'
+      path: '/deferred'
+      fullPath: '/deferred'
+      preLoaderRoute: typeof DeferredImport
+      parentRoute: typeof rootRoute
+    }
     '/posts': {
       id: '/posts'
       path: '/posts'
@@ -93,6 +112,13 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof PostsImport
       parentRoute: typeof rootRoute
     }
+    '/redirect': {
+      id: '/redirect'
+      path: '/redirect'
+      fullPath: '/redirect'
+      preLoaderRoute: typeof RedirectImport
+      parentRoute: typeof rootRoute
+    }
     '/_layout/_layout-2': {
       id: '/_layout/_layout-2'
       path: ''
@@ -148,7 +174,9 @@ export const routeTree = rootRoute.addChildren({
       LayoutLayout2LayoutBRoute,
     }),
   }),
+  DeferredRoute,
   PostsRoute: PostsRoute.addChildren({ PostsPostIdRoute, PostsIndexRoute }),
+  RedirectRoute,
   PostsPostIdDeepRoute,
 })
 
@@ -162,7 +190,9 @@ export const routeTree = rootRoute.addChildren({
       "children": [
         "/",
         "/_layout",
+        "/deferred",
         "/posts",
+        "/redirect",
         "/posts/$postId/deep"
       ]
     },
@@ -175,6 +205,9 @@ export const routeTree = rootRoute.addChildren({
         "/_layout/_layout-2"
       ]
     },
+    "/deferred": {
+      "filePath": "deferred.tsx"
+    },
     "/posts": {
       "filePath": "posts.tsx",
       "children": [
@@ -182,6 +215,9 @@ export const routeTree = rootRoute.addChildren({
         "/posts/"
       ]
     },
+    "/redirect": {
+      "filePath": "redirect.tsx"
+    },
     "/_layout/_layout-2": {
       "filePath": "_layout/_layout-2.tsx",
       "parent": "/_layout",
diff --git a/examples/react/start-basic/app/routes/__root.tsx b/examples/react/start-basic/app/routes/__root.tsx
index f4f6412bc..107b8b832 100644
--- a/examples/react/start-basic/app/routes/__root.tsx
+++ b/examples/react/start-basic/app/routes/__root.tsx
@@ -97,6 +97,14 @@ function RootDocument({ children }: { children: React.ReactNode }) {
           >
             Layout
           </Link>
+          <Link
+            to="/deferred"
+            activeProps={{
+              className: 'font-bold',
+            }}
+          >
+            Deferred
+          </Link>
           <Link
             // @ts-expect-error
             to="/this-route-does-not-exist"
@@ -108,7 +116,6 @@ function RootDocument({ children }: { children: React.ReactNode }) {
           </Link>
         </div>
         <hr />
-        {/* <React.Suspense fallback={null}>{children}</React.Suspense> */}
         {children}
         <ScrollRestoration />
         <TanStackRouterDevtools position="bottom-right" />
diff --git a/examples/react/start-basic/app/routes/deferred.tsx b/examples/react/start-basic/app/routes/deferred.tsx
new file mode 100644
index 000000000..dbc504293
--- /dev/null
+++ b/examples/react/start-basic/app/routes/deferred.tsx
@@ -0,0 +1,32 @@
+import { Await, createFileRoute, defer } from '@tanstack/react-router'
+import { Suspense, useState } from 'react'
+
+export const Route = createFileRoute('/deferred')({
+  loader: () => {
+    return {
+      deferredStuff: defer(
+        new Promise<string>((r) =>
+          setTimeout(() => r('Hello deferred!'), 5000),
+        ),
+      ),
+    }
+  },
+  component: Deferred,
+})
+
+function Deferred() {
+  const [count, setCount] = useState(0)
+  const { deferredStuff } = Route.useLoaderData()
+
+  return (
+    <div className="p-2">
+      <Suspense fallback="Loading...">
+        <Await promise={deferredStuff} children={(data) => <h3>{data}</h3>} />
+      </Suspense>
+      <div>Count: {count}</div>
+      <div>
+        <button onClick={() => setCount(count + 1)}>Increment</button>
+      </div>
+    </div>
+  )
+}
diff --git a/examples/react/start-basic/app/routes/redirect.tsx b/examples/react/start-basic/app/routes/redirect.tsx
new file mode 100644
index 000000000..c9286de13
--- /dev/null
+++ b/examples/react/start-basic/app/routes/redirect.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute, redirect } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/redirect')({
+  beforeLoad: async () => {
+    throw redirect({
+      to: '/posts',
+    })
+  },
+})
diff --git a/examples/react/start-basic/app/server.tsx b/examples/react/start-basic/app/server.tsx
deleted file mode 100644
index 99abb17ca..000000000
--- a/examples/react/start-basic/app/server.tsx
+++ /dev/null
@@ -1,184 +0,0 @@
-/// <reference types="vinxi/types/server" />
-import type { PipeableStream } from 'react-dom/server'
-import { renderToPipeableStream } from 'react-dom/server'
-import { eventHandler, getResponseHeaders, toWebRequest } from 'vinxi/server'
-import { getManifest } from 'vinxi/manifest'
-import { StartServer, transformStreamWithRouter } from '@tanstack/start/server'
-import { getRouterManifest } from '@tanstack/start/router-manifest'
-// import vinxi from 'vinxi'
-// import { Transform, PassThrough } from 'node:stream'
-
-import { createRouter } from './router'
-import { createMemoryHistory } from '@tanstack/react-router'
-import {
-  serverFnPayloadTypeHeader,
-  serverFnReturnTypeHeader,
-} from '@tanstack/start'
-import isbot from 'isbot'
-
-export default eventHandler(async (event) => {
-  const req = toWebRequest(event)
-  const url = new URL(req.url)
-  const href = url.href.replace(url.origin, '')
-
-  const routesManifest = getRouterManifest()
-  const rootRoute = (routesManifest.routes.__root__ =
-    routesManifest.routes.__root__ || {})
-
-  rootRoute.assets = rootRoute.assets || []
-
-  // Always fake that HMR is ready
-  if (import.meta.env.DEV) {
-    rootRoute.assets.push({
-      tag: 'script',
-      children: `window.__vite_plugin_react_preamble_installed__ = true`,
-    })
-  }
-
-  // Get the entry for the client from vinxi
-  const vinxiClientManifest = getManifest('client')
-
-  rootRoute.assets.push({
-    tag: 'script',
-    attrs: {
-      src: vinxiClientManifest.inputs[vinxiClientManifest.handler].output.path,
-      type: 'module',
-      async: true,
-      suppressHydrationWarning: true,
-    },
-  })
-
-  // Create a router
-  const router = createRouter()
-
-  // Create a history for the router
-  const history = createMemoryHistory({
-    initialEntries: [href],
-  })
-
-  // Update the router with the history and context
-  router.update({
-    history,
-  })
-
-  router.manifest = {
-    ...routesManifest,
-    routes: Object.fromEntries(
-      Object.entries(routesManifest.routes).map(([k, v]: any) => {
-        const { preloads, assets } = v
-        return [
-          k,
-          {
-            preloads,
-            assets,
-          },
-        ]
-      }),
-    ),
-  }
-
-  await router.load()
-
-  // Handle Redirects
-  const { redirect } = router.state
-
-  if (redirect) {
-    console.info('Redirecting...', redirect.statusCode, redirect.href)
-    return new Response(null, {
-      status: redirect.statusCode,
-      headers: {
-        ...redirect.headers,
-        Location: redirect.href,
-      },
-    })
-  }
-
-  const isRobot = isbot(req.headers.get('User-Agent'))
-
-  const stream = await new Promise<PipeableStream>(async (resolve) => {
-    // Yes, I'm shadowing "stream" again, keep your shirt on!
-    const stream = renderToPipeableStream(<StartServer router={router} />, {
-      ...(isRobot
-        ? {
-            onAllReady() {
-              resolve(stream)
-            },
-          }
-        : {
-            onShellReady() {
-              resolve(stream)
-            },
-          }),
-    })
-  })
-
-  // Add our Router transform to the stream
-  const transforms = [
-    transformStreamWithRouter(router),
-    // // A transform that logs chunks
-    // new Transform({
-    //   transform(chunk, encoding, callback) {
-    //     const str = chunk.toString()
-    //     console.info('')
-    //     console.info('CHUNK')
-    //     console.info('')
-    //     console.info(str)
-    //     this.push(str)
-    //     return callback()
-    //   },
-    // }),
-  ]
-
-  // Pipe the stream through our transforms
-  const transformedStream = transforms.reduce(
-    (stream, transform) => stream.pipe(transform as any),
-    stream,
-  )
-
-  ;(event as any).__tsrHeadersSent = true
-
-  let headers = {
-    ...getResponseHeaders(event),
-    'Content-Type': 'text/html',
-    ...router.state.matches.reduce((acc, match) => {
-      if (match.headers) {
-        Object.assign(acc, match.headers)
-      }
-      return acc
-    }, {}),
-  }
-
-  // Remove server function headers
-  ;[serverFnReturnTypeHeader, serverFnPayloadTypeHeader].forEach((header) => {
-    delete headers[header]
-  })
-
-  // Dedupe headers
-  headers = dedupeHeaders(Object.entries(headers))
-
-  return new Response(transformedStream as any, {
-    status: router.state.statusCode,
-    statusText: `${router.state.statusCode}`.startsWith('5')
-      ? 'Internal Server Error'
-      : 'OK',
-    headers,
-  })
-})
-
-function dedupeHeaders(headerList: [string, string][]): [string, string][] {
-  // Object to store the deduplicated headers
-  const seen = new Set()
-
-  // Reverse, filter with seen, then reverse again
-  return [...headerList]
-    .reverse()
-    .filter(([name]) => {
-      const key = name.toLowerCase()
-      if (seen.has(key)) {
-        return false
-      }
-      seen.add(key)
-      return true
-    })
-    .reverse()
-}
diff --git a/examples/react/start-basic/app/ssr.tsx b/examples/react/start-basic/app/ssr.tsx
new file mode 100644
index 000000000..79ac23f57
--- /dev/null
+++ b/examples/react/start-basic/app/ssr.tsx
@@ -0,0 +1,12 @@
+import {
+  createRequestHandler,
+  defaultStreamHandler,
+} from '@tanstack/start/server'
+import { getRouterManifest } from '@tanstack/start/router-manifest'
+
+import { createRouter } from './router'
+
+export default createRequestHandler({
+  createRouter,
+  getRouterManifest,
+})(defaultStreamHandler)
diff --git a/examples/react/start-basic/package.json b/examples/react/start-basic/package.json
index 9ca500e82..8c4a22b98 100644
--- a/examples/react/start-basic/package.json
+++ b/examples/react/start-basic/package.json
@@ -9,15 +9,18 @@
     "build": "vinxi build",
     "start": "vinxi start",
     "lint": "prettier --check '**/*' --ignore-unknown && eslint --ext .ts,.tsx ./app",
-    "format": "prettier --write '**/*' --ignore-unknown"
+    "format": "prettier --write '**/*' --ignore-unknown",
+    "test:playwright": "playwright test --project=chromium",
+    "test:playwright:replay": "playwright test --project=replay-chromium"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.34.0",
-    "@tanstack/router-devtools": "^1.34.0",
-    "@tanstack/router-vite-plugin": "^1.33.12",
-    "@tanstack/start": "^1.34.0",
+    "@tanstack/react-router": "^1.34.6",
+    "@tanstack/router-devtools": "^1.34.6",
+    "@tanstack/router-vite-plugin": "^1.34.1",
+    "@tanstack/start": "^1.34.6",
     "@typescript-eslint/parser": "^7.2.0",
     "@vitejs/plugin-react": "^4",
+    "dotenv": "^16.4.5",
     "isbot": "^3.7.0",
     "react": "^18.3.1",
     "react-dom": "^18.3.1",
@@ -26,6 +29,9 @@
     "vinxi": "^0.3.11"
   },
   "devDependencies": {
+    "@playwright/test": "^1.44.0",
+    "@replayio/playwright": "^3.0.1",
+    "@types/node": "^20.12.11",
     "@types/react": "^18.2.65",
     "@types/react-dom": "^18.2.21",
     "@vitejs/plugin-react": "^4",
diff --git a/examples/react/start-basic/playwright.config.ts b/examples/react/start-basic/playwright.config.ts
new file mode 100644
index 000000000..00e7ec4cd
--- /dev/null
+++ b/examples/react/start-basic/playwright.config.ts
@@ -0,0 +1,40 @@
+import { defineConfig, devices } from '@playwright/test'
+import {
+  createReplayReporterConfig as replayReporter,
+  devices as replayDevices,
+} from '@replayio/playwright'
+
+import dotenv from 'dotenv'
+
+dotenv.config()
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+  testDir: './tests',
+
+  reporter: [
+    replayReporter({
+      apiKey: process.env.REPLAY_API_KEY,
+      upload: true,
+    }),
+    ['line'],
+  ],
+
+  use: {
+    /* Base URL to use in actions like `await page.goto('/')`. */
+    baseURL: process.env.APP_BASE_URL ?? 'http://localhost:3000/',
+  },
+
+  projects: [
+    {
+      name: 'replay-chromium',
+      use: { ...replayDevices['Replay Chromium'] },
+    },
+    {
+      name: 'chromium',
+      use: { ...devices['Desktop Chrome'] },
+    },
+  ],
+})
diff --git a/examples/react/start-basic/tsr.config.json b/examples/react/start-basic/tsr.config.json
deleted file mode 100644
index bc7055a01..000000000
--- a/examples/react/start-basic/tsr.config.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
-  "rootDirectory": ".",
-  "routesDirectory": "./app/routes",
-  "generatedRouteTree": "./app/routeTree.gen.ts"
-}
diff --git a/examples/react/wip-trpc-react-query/package.json b/examples/react/wip-trpc-react-query/package.json
index 7e830bae5..d07edd1de 100644
--- a/examples/react/wip-trpc-react-query/package.json
+++ b/examples/react/wip-trpc-react-query/package.json
@@ -13,8 +13,8 @@
   "dependencies": {
     "@tanstack/react-query": "^4.36.1",
     "@tanstack/react-query-devtools": "^4.36.1",
-    "@tanstack/react-router": "^1.34.0",
-    "@tanstack/router-devtools": "^1.34.0",
+    "@tanstack/react-router": "^1.34.6",
+    "@tanstack/router-devtools": "^1.34.6",
     "@trpc/client": "^10.45.0",
     "@trpc/react-query": "^10.45.0",
     "@trpc/server": "^10.45.0",
diff --git a/examples/react/wip-waku-rsc/package.json b/examples/react/wip-waku-rsc/package.json
index 3512f69ed..f4e3bcdde 100644
--- a/examples/react/wip-waku-rsc/package.json
+++ b/examples/react/wip-waku-rsc/package.json
@@ -10,8 +10,8 @@
   },
   "dependencies": {
     "@hono/node-server": "^1.4.1",
-    "@tanstack/react-router": "^1.34.0",
-    "@tanstack/router-devtools": "^1.34.0",
+    "@tanstack/react-router": "^1.34.6",
+    "@tanstack/router-devtools": "^1.34.6",
     "hono": "^3.12.0",
     "react": "18.3.0-canary-0cdfef19b-20231211",
     "react-dom": "18.3.0-canary-0cdfef19b-20231211",
diff --git a/examples/react/wip-with-bling/package.json b/examples/react/wip-with-bling/package.json
index 6d79a373c..743bed9f6 100644
--- a/examples/react/wip-with-bling/package.json
+++ b/examples/react/wip-with-bling/package.json
@@ -13,10 +13,10 @@
   },
   "dependencies": {
     "@tanstack/bling": "^0.5.0",
-    "@tanstack/react-router": "^1.34.0",
-    "@tanstack/start": "^1.34.0",
+    "@tanstack/react-router": "^1.34.6",
+    "@tanstack/start": "^1.34.6",
     "@tanstack/react-store": "^0.2.1",
-    "@tanstack/router-devtools": "^1.34.0",
+    "@tanstack/router-devtools": "^1.34.6",
     "redaxios": "^0.5.0",
     "fastify": "^4.25.2",
     "get-port": "^7.0.0",
diff --git a/examples/react/wip-with-bling/src/entry-server.tsx b/examples/react/wip-with-bling/src/entry-server.tsx
index 323add50e..2e0f32b1e 100644
--- a/examples/react/wip-with-bling/src/entry-server.tsx
+++ b/examples/react/wip-with-bling/src/entry-server.tsx
@@ -1,14 +1,13 @@
+import { PassThrough } from 'stream'
 import * as React from 'react'
-import ReactDOMServer, { PipeableStream } from 'react-dom/server'
+import { renderToPipeableStream } from 'react-dom/server'
 import { createMemoryHistory } from '@tanstack/react-router'
-import { StartServer, transformStreamWithRouter } from '@tanstack/start/server'
+import { StartServer } from '@tanstack/start/server'
 import { isbot } from 'isbot'
-import { ServerResponse } from 'http'
-import express from 'express'
-
-// index.js
-import './fetch-polyfill'
 import { createRouter } from './router'
+import type { ServerResponse } from 'http'
+import type express from 'express'
+import './fetch-polyfill'
 
 type ReactReadableStream = ReadableStream<Uint8Array> & {
   allReady?: Promise<void> | undefined
@@ -38,41 +37,30 @@ export async function render(opts: {
   // (streamed data will continue to load in the background)
   await router.load()
 
-  // Track errors
-  let didError = false
-
-  // Clever way to get the right callback. Thanks Remix!
-  const callbackName = isbot(opts.req.headers['user-agent'])
-    ? 'onAllReady'
-    : 'onShellReady'
+  const passthrough = new PassThrough()
 
-  // Render the app to a readable stream
-  let stream!: PipeableStream
-
-  await new Promise<void>((resolve) => {
-    stream = ReactDOMServer.renderToPipeableStream(
-      <StartServer router={router} />,
-      {
-        [callbackName]: () => {
-          opts.res.statusCode = didError ? 500 : 200
-          opts.res.setHeader('Content-Type', 'text/html')
-          resolve()
-        },
-        onError: (err) => {
-          didError = true
-          console.log(err)
-        },
-      },
-    )
+  const pipeable = renderToPipeableStream(<StartServer router={router} />, {
+    ...(isbot(opts.req.headers['user-agent'])
+      ? {
+          onAllReady() {
+            pipeable.pipe(passthrough)
+          },
+        }
+      : {
+          onShellReady() {
+            pipeable.pipe(passthrough)
+          },
+        }),
+    onShellError(err) {
+      throw err
+    },
   })
 
-  // Add our Router transform to the stream
-  const transforms = [transformStreamWithRouter(router)]
-
-  const transformedStream = transforms.reduce(
-    (stream, transform) => stream.pipe(transform as any),
-    stream,
-  )
+  opts.res.setHeader('Content-Type', 'text/html')
+  opts.res.statusCode = router.state.statusCode
+  opts.res.statusMessage = `${router.state.statusCode}`.startsWith('5')
+    ? 'Internal Server Error'
+    : 'OK'
 
-  transformedStream.pipe(opts.res)
+  pipeable.pipe(opts.res)
 }
diff --git a/examples/react/wip-with-framer-motion/package.json b/examples/react/wip-with-framer-motion/package.json
index 7971e07ee..636c81406 100644
--- a/examples/react/wip-with-framer-motion/package.json
+++ b/examples/react/wip-with-framer-motion/package.json
@@ -9,8 +9,8 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.34.0",
-    "@tanstack/router-devtools": "^1.34.0",
+    "@tanstack/react-router": "^1.34.6",
+    "@tanstack/router-devtools": "^1.34.6",
     "redaxios": "^0.5.0",
     "framer-motion": "^10.17.9",
     "immer": "^10.0.3",
diff --git a/examples/react/with-trpc/package.json b/examples/react/with-trpc/package.json
index 5c8d608a5..4b42c9cdb 100644
--- a/examples/react/with-trpc/package.json
+++ b/examples/react/with-trpc/package.json
@@ -13,8 +13,8 @@
   "dependencies": {
     "@tanstack/react-query": "^4.36.1",
     "@tanstack/react-query-devtools": "^4.36.1",
-    "@tanstack/react-router": "^1.34.0",
-    "@tanstack/router-devtools": "^1.34.0",
+    "@tanstack/react-router": "^1.34.6",
+    "@tanstack/router-devtools": "^1.34.6",
     "@trpc/client": "^10.45.0",
     "@trpc/react-query": "^10.45.0",
     "@trpc/server": "^10.45.0",
diff --git a/package.json b/package.json
index 62d6d2657..ca98dd432 100644
--- a/package.json
+++ b/package.json
@@ -19,8 +19,10 @@
     "test:unit:dev": "pnpm run test:unit && nx watch --all -- pnpm run test:unit",
     "test:build": "nx affected --target=test:build --exclude=examples/**",
     "test:types": "nx affected --target=test:types --exclude=examples/**",
+    "test:e2e": "nx run tanstack-router-example-react-start-basic:test:playwright:replay",
     "build": "nx affected --target=build --exclude=examples/**",
     "build:all": "nx run-many --target=build --exclude=examples/**",
+    "build:examples": "nx run-many --target=build",
     "watch": "pnpm run build:all && nx watch --all -- pnpm run build:all",
     "dev": "pnpm run watch",
     "prettier": "prettier --ignore-unknown '**/*'",
diff --git a/packages/react-router/package.json b/packages/react-router/package.json
index 227ee7d33..001466889 100644
--- a/packages/react-router/package.json
+++ b/packages/react-router/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@tanstack/react-router",
-  "version": "1.34.0",
+  "version": "1.34.6",
   "description": "",
   "author": "Tanner Linsley",
   "license": "MIT",
diff --git a/packages/react-router/src/Matches.tsx b/packages/react-router/src/Matches.tsx
index 5c03f31dc..ea6ad575f 100644
--- a/packages/react-router/src/Matches.tsx
+++ b/packages/react-router/src/Matches.tsx
@@ -17,6 +17,8 @@ import {
 import { rootRouteId } from './root'
 import type { ResolveRelativePath, ToOptions } from './link'
 import type {
+  AllContext,
+  AllLoaderData,
   AllParams,
   FullSearchSchema,
   ParseRoute,
@@ -72,16 +74,20 @@ export interface RouteMatch<
 export type MakeRouteMatch<
   TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
   TRouteId = ParseRoute<TRouteTree>['id'],
-  TReturnIntersection extends boolean = false,
+  TStrict extends boolean = true,
   TTypes extends AnyRoute['types'] = RouteById<TRouteTree, TRouteId>['types'],
-  TAllParams = TReturnIntersection extends false
-    ? TTypes['allParams']
-    : Partial<AllParams<TRouteTree>>,
-  TFullSearchSchema = TReturnIntersection extends false
-    ? TTypes['fullSearchSchema']
-    : Partial<FullSearchSchema<TRouteTree>>,
-  TLoaderData = TTypes['loaderData'],
-  TAllContext = TTypes['allContext'],
+  TAllParams = TStrict extends false
+    ? AllParams<TRouteTree>
+    : TTypes['allParams'],
+  TFullSearchSchema = TStrict extends false
+    ? FullSearchSchema<TRouteTree>
+    : TTypes['fullSearchSchema'],
+  TLoaderData = TStrict extends false
+    ? AllLoaderData<TRouteTree>
+    : TTypes['loaderData'],
+  TAllContext = TStrict extends false
+    ? AllContext<TRouteTree>
+    : TTypes['allContext'],
   TRouteContext = TTypes['routeContext'],
   TLoaderDeps = TTypes['loaderDeps'],
 > = RouteMatch<
@@ -549,13 +555,9 @@ export function MatchRoute<
 export function useMatches<
   TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
   TRouteId extends RouteIds<TRouteTree> = ParseRoute<TRouteTree>['id'],
-  TReturnIntersection extends boolean = false,
-  TRouteMatch = MakeRouteMatch<TRouteTree, TRouteId, TReturnIntersection>,
+  TRouteMatch = MakeRouteMatch<TRouteTree, TRouteId>,
   T = Array<TRouteMatch>,
->(opts?: {
-  select?: (matches: Array<TRouteMatch>) => T
-  experimental_returnIntersection?: TReturnIntersection
-}): T {
+>(opts?: { select?: (matches: Array<TRouteMatch>) => T }): T {
   return useRouterState({
     select: (state) => {
       const matches = state.matches
@@ -569,13 +571,9 @@ export function useMatches<
 export function useParentMatches<
   TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
   TRouteId extends RouteIds<TRouteTree> = ParseRoute<TRouteTree>['id'],
-  TReturnIntersection extends boolean = false,
-  TRouteMatch = MakeRouteMatch<TRouteTree, TRouteId, TReturnIntersection>,
+  TRouteMatch = MakeRouteMatch<TRouteTree, TRouteId>,
   T = Array<TRouteMatch>,
->(opts?: {
-  select?: (matches: Array<TRouteMatch>) => T
-  experimental_returnIntersection?: TReturnIntersection
-}): T {
+>(opts?: { select?: (matches: Array<TRouteMatch>) => T }): T {
   const contextMatchId = React.useContext(matchContext)
 
   return useMatches({
@@ -594,13 +592,9 @@ export function useParentMatches<
 export function useChildMatches<
   TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
   TRouteId extends RouteIds<TRouteTree> = ParseRoute<TRouteTree>['id'],
-  TReturnIntersection extends boolean = false,
-  TRouteMatch = MakeRouteMatch<TRouteTree, TRouteId, TReturnIntersection>,
+  TRouteMatch = MakeRouteMatch<TRouteTree, TRouteId>,
   T = Array<TRouteMatch>,
->(opts?: {
-  select?: (matches: Array<TRouteMatch>) => T
-  experimental_returnIntersection?: TReturnIntersection
-}): T {
+>(opts?: { select?: (matches: Array<TRouteMatch>) => T }): T {
   const contextMatchId = React.useContext(matchContext)
 
   return useMatches({
diff --git a/packages/react-router/src/RouterProvider.tsx b/packages/react-router/src/RouterProvider.tsx
index 8c00123de..b5ce4c946 100644
--- a/packages/react-router/src/RouterProvider.tsx
+++ b/packages/react-router/src/RouterProvider.tsx
@@ -40,7 +40,9 @@ export type NavigateFn = <
   TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
   TMaskTo extends string = '',
 >(
-  opts: NavigateOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>,
+  opts: NavigateOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo> & {
+    __isRedirect?: boolean
+  },
 ) => Promise<void>
 
 export type BuildLocationFn<TRouteTree extends AnyRoute> = <
diff --git a/packages/react-router/src/Transitioner.tsx b/packages/react-router/src/Transitioner.tsx
index fa3a7fe3b..412d6eaf3 100644
--- a/packages/react-router/src/Transitioner.tsx
+++ b/packages/react-router/src/Transitioner.tsx
@@ -23,7 +23,9 @@ export function Transitioner() {
     routerState.isLoading || isTransitioning || hasPendingMatches
   const previousIsAnyPending = usePrevious(isAnyPending)
 
-  router.startReactTransition = startReactTransition_
+  if (!router.isServer) {
+    router.startReactTransition = startReactTransition_
+  }
 
   // Subscribe to location changes
   // and try to load the new location
@@ -77,16 +79,11 @@ export function Transitioner() {
       const pathChanged = fromLocation.href !== toLocation.href
 
       router.emit({
-        type: 'onLoad',
+        type: 'onLoad', // When the new URL has committed, when the new matches have been loaded into state.matches
         fromLocation,
         toLocation,
         pathChanged,
       })
-
-      // if (router.viewTransitionPromise) {
-      //   console.log('resolving view transition promise')
-      // }
-      // router.viewTransitionPromise?.resolve(true)
     }
   }, [previousIsLoading, router, routerState.isLoading])
 
diff --git a/packages/react-router/src/awaited.tsx b/packages/react-router/src/awaited.tsx
index f5c8208fd..de94c6fd1 100644
--- a/packages/react-router/src/awaited.tsx
+++ b/packages/react-router/src/awaited.tsx
@@ -10,7 +10,9 @@ export type AwaitOptions<T> = {
   promise: DeferredPromise<T>
 }
 
-export function useAwaited<T>({ promise }: AwaitOptions<T>): [T] {
+export function useAwaited<T>({
+  promise,
+}: AwaitOptions<T>): [T, DeferredPromise<T>] {
   const router = useRouter()
   // const rerender = React.useReducer((x) => x + 1, 0)[1]
 
@@ -56,20 +58,6 @@ export function useAwaited<T>({ promise }: AwaitOptions<T>): [T] {
     throw isDehydratedDeferred(promise) ? state.promise : promise
   }
 
-  // If we are the originator of the promise,
-  // inject the state into the HTML stream
-  if (!isDehydratedDeferred(promise)) {
-    router.injectHtml(
-      `<script class='tsr_deferred_data'>window.__TSR__DEFERRED__${state.uid} = ${JSON.stringify(router.options.transformer.stringify(state))}
-  if (window.__TSR__ROUTER__) {
-    let deferred = window.__TSR__ROUTER__.getDeferred('${state.uid}');
-    if (deferred) deferred.resolve(window.__TSR__DEFERRED__${state.uid});
-  }
-  document.querySelectorAll('.tsr_deferred_data').forEach((el) => el.parentElement.removeChild(el));
-</script>`,
-    )
-  }
-
   if (state.status === 'error') {
     if (typeof document !== 'undefined') {
       if (isServerSideError(state.error)) {
@@ -93,7 +81,7 @@ export function useAwaited<T>({ promise }: AwaitOptions<T>): [T] {
     }
   }
 
-  return [promise.__deferredState.data as any]
+  return [promise.__deferredState.data as any, promise]
 }
 
 export function Await<T>(
@@ -115,6 +103,42 @@ function AwaitInner<T>(
     children: (result: T) => React.ReactNode
   },
 ) {
-  const awaited = useAwaited(props)
-  return props.children(...awaited) as React.JSX.Element
+  const router = useRouter()
+  const [data, promise] = useAwaited(props)
+  const state = promise.__deferredState
+  // If we are the originator of the promise,
+  // inject the state into the HTML stream
+  return (
+    <>
+      {!isDehydratedDeferred(promise) ? (
+        <ScriptOnce
+          children={`window.__TSR__DEFERRED__${state.uid} = ${JSON.stringify(router.options.transformer.stringify(state))}
+  if (window.__TSR__ROUTER__) {
+    let deferred = window.__TSR__ROUTER__.getDeferred('${state.uid}');
+    if (deferred) deferred.resolve(window.__TSR__DEFERRED__${state.uid});
+  }`}
+        />
+      ) : null}
+      {props.children(data)}
+    </>
+  )
+}
+
+export function ScriptOnce({
+  className,
+  children,
+  ...rest
+}: { children: string } & React.HTMLProps<HTMLScriptElement>) {
+  return (
+    <script
+      {...rest}
+      className={`tsr-script-once ${className || ''}`}
+      dangerouslySetInnerHTML={{
+        __html: [
+          children,
+          `document.querySelectorAll('.tsr-script-once').forEach((el) => el.parentElement.removeChild(el));`,
+        ].join('\n'),
+      }}
+    />
+  )
 }
diff --git a/packages/react-router/src/fileRoute.ts b/packages/react-router/src/fileRoute.ts
index 2db7a76e7..7359458e6 100644
--- a/packages/react-router/src/fileRoute.ts
+++ b/packages/react-router/src/fileRoute.ts
@@ -94,7 +94,7 @@ export class FileRoute<
     TAllContext = ResolveAllContext<TParentRoute, TRouteContext>,
     TRouterContext = AnyContext,
     TLoaderDeps extends Record<string, any> = {},
-    TLoaderDataReturn = unknown,
+    TLoaderDataReturn = {},
     TLoaderData = ResolveLoaderData<TLoaderDataReturn>,
     TChildren = unknown,
   >(
diff --git a/packages/react-router/src/index.tsx b/packages/react-router/src/index.tsx
index 3a72831cc..6e5e5c24d 100644
--- a/packages/react-router/src/index.tsx
+++ b/packages/react-router/src/index.tsx
@@ -12,7 +12,7 @@ export {
 } from '@tanstack/history'
 export { default as invariant } from 'tiny-invariant'
 export { default as warning } from 'tiny-warning'
-export { useAwaited, Await, type AwaitOptions } from './awaited'
+export { useAwaited, Await, type AwaitOptions, ScriptOnce } from './awaited'
 export {
   defer,
   isDehydratedDeferred,
diff --git a/packages/react-router/src/route.ts b/packages/react-router/src/route.ts
index 4a500713b..8c918d09d 100644
--- a/packages/react-router/src/route.ts
+++ b/packages/react-router/src/route.ts
@@ -71,10 +71,8 @@ export type RouteOptions<
   TRouterContext = AnyContext,
   TAllContext = AnyContext,
   TLoaderDeps extends Record<string, any> = {},
-  TLoaderDataReturn = unknown,
-  TLoaderData = [TLoaderDataReturn] extends [never]
-    ? undefined
-    : TLoaderDataReturn,
+  TLoaderDataReturn = {},
+  TLoaderData = ResolveLoaderData<TLoaderDataReturn>,
 > = BaseRouteOptions<
   TParentRoute,
   TCustomId,
@@ -121,7 +119,7 @@ export type FileBaseRouteOptions<
   TRouterContext = AnyContext,
   TAllContext = AnyContext,
   TLoaderDeps extends Record<string, any> = {},
-  TLoaderDataReturn = unknown,
+  TLoaderDataReturn = {},
 > = {
   validateSearch?: SearchSchemaValidator<TSearchSchemaInput, TSearchSchema>
   shouldReload?:
@@ -187,7 +185,7 @@ export type BaseRouteOptions<
   TRouterContext = AnyContext,
   TAllContext = AnyContext,
   TLoaderDeps extends Record<string, any> = {},
-  TLoaderDataReturn = unknown,
+  TLoaderDataReturn = {},
 > = RoutePathOptions<TCustomId, TPath> &
   FileBaseRouteOptions<
     TParentRoute,
@@ -344,7 +342,7 @@ export type RouteLoaderFn<
   in out TLoaderDeps extends Record<string, any> = {},
   in out TAllContext = AnyContext,
   in out TRouteContext = AnyContext,
-  TLoaderData = unknown,
+  TLoaderData = undefined,
 > = (
   match: LoaderFnContext<TAllParams, TLoaderDeps, TAllContext, TRouteContext>,
 ) => Promise<TLoaderData> | TLoaderData
@@ -629,7 +627,7 @@ export class Route<
   >,
   in out TRouterContext = AnyContext,
   in out TLoaderDeps extends Record<string, any> = {},
-  TLoaderDataReturn = unknown,
+  TLoaderDataReturn = {},
   in out TLoaderData = ResolveLoaderData<TLoaderDataReturn>,
   in out TChildren = unknown,
 > {
@@ -971,7 +969,7 @@ export function createRoute<
   >,
   TRouterContext = AnyContext,
   TLoaderDeps extends Record<string, any> = {},
-  TLoaderDataReturn = unknown,
+  TLoaderDataReturn = {},
   TLoaderData = ResolveLoaderData<TLoaderDataReturn>,
   TChildren = unknown,
 >(
@@ -1029,7 +1027,7 @@ export type RootRouteOptions<
   TRouteContext = ResolveRouteContext<TRouteContextReturn>,
   TRouterContext = {},
   TLoaderDeps extends Record<string, any> = {},
-  TLoaderDataReturn = unknown,
+  TLoaderDataReturn = {},
   TLoaderData = ResolveLoaderData<TLoaderDataReturn>,
 > = Omit<
   RouteOptions<
@@ -1069,10 +1067,8 @@ export function createRootRouteWithContext<TRouterContext extends {}>() {
       ? RouteContext
       : TRouteContextReturn,
     TLoaderDeps extends Record<string, any> = {},
-    TLoaderDataReturn = unknown,
-    TLoaderData = [TLoaderDataReturn] extends [never]
-      ? undefined
-      : TLoaderDataReturn,
+    TLoaderDataReturn = {},
+    TLoaderData = ResolveLoaderData<TLoaderDataReturn>,
   >(
     options?: RootRouteOptions<
       TSearchSchemaInput,
@@ -1116,7 +1112,7 @@ export class RootRoute<
   in out TRouteContext = ResolveRouteContext<TRouteContextReturn>,
   in out TRouterContext = {},
   TLoaderDeps extends Record<string, any> = {},
-  TLoaderDataReturn = unknown,
+  TLoaderDataReturn = {},
   in out TLoaderData = ResolveLoaderData<TLoaderDataReturn>,
 > extends Route<
   any, // TParentRoute
@@ -1168,7 +1164,7 @@ export function createRootRoute<
   TRouteContext = ResolveRouteContext<TRouteContextReturn>,
   TRouterContext = {},
   TLoaderDeps extends Record<string, any> = {},
-  TLoaderDataReturn = unknown,
+  TLoaderDataReturn = {},
   TLoaderData = ResolveLoaderData<TLoaderDataReturn>,
 >(
   options?: Omit<
@@ -1326,7 +1322,7 @@ export class NotFoundRoute<
   >,
   TRouterContext = AnyContext,
   TLoaderDeps extends Record<string, any> = {},
-  TLoaderDataReturn = unknown,
+  TLoaderDataReturn = {},
   TLoaderData = ResolveLoaderData<TLoaderDataReturn>,
   TChildren = unknown,
 > extends Route<
diff --git a/packages/react-router/src/routeInfo.ts b/packages/react-router/src/routeInfo.ts
index f4e9d6ba5..3b33d097a 100644
--- a/packages/react-router/src/routeInfo.ts
+++ b/packages/react-router/src/routeInfo.ts
@@ -1,6 +1,6 @@
-import type { AnyRoute } from './route'
-import type { AnyRouter, Router, TrailingSlashOption } from './router'
-import type { UnionToIntersection, UnionToTuple } from './utils'
+import type { AnyRoute, RootSearchSchema } from './route'
+import type { AnyRouter, TrailingSlashOption } from './router'
+import type { Expand, MergeUnion } from './utils'
 
 export type ParseRoute<TRouteTree, TAcc = TRouteTree> = TRouteTree extends {
   types: { children: infer TChildren }
@@ -114,26 +114,20 @@ export type RouteByToPath<TRouter extends AnyRouter, TTo> = Extract<
   AnyRoute
 >
 
-// eslint-disable-next-line @typescript-eslint/naming-convention
-type UnionizeCollisions<T, U> = {
-  [P in keyof T & keyof U]: T[P] extends U[P] ? T[P] : T[P] | U[P]
-}
-// eslint-disable-next-line @typescript-eslint/naming-convention
-type Reducer<T, U, C = UnionizeCollisions<T, U>> = C &
-  Omit<T, keyof C> &
-  Omit<U, keyof C>
-
-type Reduce<TValue extends Array<any>, TResult = unknown> = TValue extends [
-  infer First,
-  ...infer Rest,
-]
-  ? Reduce<Rest, Reducer<TResult, First>>
-  : TResult
-
-export type FullSearchSchema<TRouteTree extends AnyRoute> = Partial<
-  Reduce<UnionToTuple<ParseRoute<TRouteTree>['types']['fullSearchSchema']>>
+export type FullSearchSchema<TRouteTree extends AnyRoute> = Expand<
+  MergeUnion<
+    Exclude<ParseRoute<TRouteTree>['types']['searchSchema'], RootSearchSchema>
+  >
+>
+
+export type AllParams<TRouteTree extends AnyRoute> = Expand<
+  MergeUnion<ParseRoute<TRouteTree>['types']['params']>
+>
+
+export type AllContext<TRouteTree extends AnyRoute> = Expand<
+  MergeUnion<ParseRoute<TRouteTree>['types']['allContext']>
 >
 
-export type AllParams<TRouteTree extends AnyRoute> = UnionToIntersection<
-  ParseRoute<TRouteTree>['types']['allParams']
+export type AllLoaderData<TRouteTree extends AnyRoute> = Expand<
+  MergeUnion<ParseRoute<TRouteTree>['types']['loaderData']>
 >
diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts
index eca12bb9d..2d1abad62 100644
--- a/packages/react-router/src/router.ts
+++ b/packages/react-router/src/router.ts
@@ -7,7 +7,6 @@ import { defaultParseSearch, defaultStringifySearch } from './searchParams'
 import {
   createControlledPromise,
   deepEqual,
-  escapeJSON,
   functionalUpdate,
   last,
   pick,
@@ -421,6 +420,12 @@ export const componentTypes = [
 ] as const
 
 export type RouterEvents = {
+  onBeforeNavigate: {
+    type: 'onBeforeNavigate'
+    fromLocation: ParsedLocation
+    toLocation: ParsedLocation
+    pathChanged: boolean
+  }
   onBeforeLoad: {
     type: 'onBeforeLoad'
     fromLocation: ParsedLocation
@@ -483,7 +488,6 @@ export class Router<
   shouldViewTransition?: boolean = undefined
   latestLoadPromise: Promise<void> = Promise.resolve()
   subscribers = new Set<RouterListener<RouterEvent>>()
-  injectedHtml: Array<InjectedHtmlEntry> = []
   dehydratedData?: TDehydrated
   viewTransitionPromise?: ControlledPromise<true>
   manifest?: Manifest
@@ -505,7 +509,7 @@ export class Router<
     'stringifySearch' | 'parseSearch' | 'context'
   >
   history!: RouterHistory
-  latestLocation!: ParsedLocation
+  latestLocation!: ParsedLocation<FullSearchSchema<TRouteTree>>
   basepath!: string
   routeTree!: TRouteTree
   routesById!: RoutesById<TRouteTree>
@@ -766,7 +770,7 @@ export class Router<
   }
 
   parseLocation = (
-    previousLocation?: ParsedLocation,
+    previousLocation?: ParsedLocation<FullSearchSchema<TRouteTree>>,
   ): ParsedLocation<FullSearchSchema<TRouteTree>> => {
     const parse = ({
       pathname,
@@ -1375,7 +1379,7 @@ export class Router<
     })
   }
 
-  navigate: NavigateFn = ({ from, to, ...rest }) => {
+  navigate: NavigateFn = ({ from, to, __isRedirect, ...rest }) => {
     // If this link simply reloads the current route,
     // make sure it has a new key so it will trigger a data refresh
 
@@ -1424,13 +1428,6 @@ export class Router<
         // Cancel any pending matches
         this.cancelMatches()
 
-        this.emit({
-          type: 'onBeforeLoad',
-          fromLocation: prevLocation,
-          toLocation: next,
-          pathChanged: pathDidChange,
-        })
-
         let pendingMatches!: Array<AnyRouteMatch>
 
         this.__store.batch(() => {
@@ -1455,6 +1452,22 @@ export class Router<
           }))
         })
 
+        if (!this.state.redirect) {
+          this.emit({
+            type: 'onBeforeNavigate',
+            fromLocation: prevLocation,
+            toLocation: next,
+            pathChanged: pathDidChange,
+          })
+        }
+
+        this.emit({
+          type: 'onBeforeLoad',
+          fromLocation: prevLocation,
+          toLocation: next,
+          pathChanged: pathDidChange,
+        })
+
         await this.loadMatches({
           matches: pendingMatches,
           location: next,
@@ -1517,7 +1530,7 @@ export class Router<
         if (isResolvedRedirect(err)) {
           redirect = err
           if (!this.isServer) {
-            this.navigate({ ...err, replace: true })
+            this.navigate({ ...err, replace: true, __isRedirect: true })
             this.load()
           }
         } else if (isNotFound(err)) {
@@ -1526,8 +1539,9 @@ export class Router<
 
         this.__store.setState((s) => ({
           ...s,
-          statusCode:
-            redirect?.statusCode || notFound
+          statusCode: redirect
+            ? redirect.statusCode
+            : notFound
               ? 404
               : s.matches.some((d) => d.status === 'error')
                 ? 500
@@ -2231,10 +2245,6 @@ export class Router<
     return match
   }
 
-  injectHtml = async (html: string | (() => Promise<string> | string)) => {
-    this.injectedHtml.push(html)
-  }
-
   // We use a token -> weak map to keep track of deferred promises
   // that are registered on the server and need to be resolved
   registeredDeferredsIds = new Map<string, {}>()
@@ -2250,55 +2260,6 @@ export class Router<
     return this.registeredDeferreds.get(token)
   }
 
-  /**
-   * @deprecated Please inject your own html using the `injectHtml` method
-   */
-  dehydrateData = <T>(key: any, getData: T | (() => Promise<T> | T)) => {
-    warning(
-      false,
-      `The dehydrateData method is deprecated. Please use the injectHtml method to inject your own data.`,
-    )
-
-    if (typeof document === 'undefined') {
-      const strKey = typeof key === 'string' ? key : JSON.stringify(key)
-
-      this.injectHtml(async () => {
-        const id = `__TSR_DEHYDRATED__${strKey}`
-        const data =
-          typeof getData === 'function' ? await (getData as any)() : getData
-        return `<script id='${id}' suppressHydrationWarning>
-  window["__TSR_DEHYDRATED__${escapeJSON(
-    strKey,
-  )}"] = ${JSON.stringify(this.options.transformer.stringify(data))}
-</script>`
-      })
-
-      return () => this.hydrateData<T>(key)
-    }
-
-    return () => undefined
-  }
-
-  /**
-   * @deprecated Please extract your own data from scripts injected using the `injectHtml` method
-   */
-  hydrateData = <T = unknown>(key: any) => {
-    warning(
-      false,
-      `The hydrateData method is deprecated. Please use the extractHtml method to extract your own data.`,
-    )
-
-    if (typeof document !== 'undefined') {
-      const strKey = typeof key === 'string' ? key : JSON.stringify(key)
-
-      return this.options.transformer.parse(
-        window[`__TSR_DEHYDRATED__${strKey}` as any] as unknown as string,
-      ) as T
-    }
-
-    return undefined
-  }
-
   dehydrate = (): DehydratedRouter => {
     const pickError =
       this.options.errorSerializer?.serialize ?? defaultSerializeError
diff --git a/packages/react-router/src/useBlocker.tsx b/packages/react-router/src/useBlocker.tsx
index 66a736170..dec62d37a 100644
--- a/packages/react-router/src/useBlocker.tsx
+++ b/packages/react-router/src/useBlocker.tsx
@@ -44,17 +44,6 @@ export function useBlocker(
     reset: () => {},
   })
 
-  const createPromise = () =>
-    new Promise<boolean>((resolve) => {
-      setResolver({
-        status: 'idle',
-        proceed: () => resolve(true),
-        reset: () => resolve(false),
-      })
-    })
-
-  const [promise, setPromise] = React.useState(createPromise)
-
   React.useEffect(() => {
     const blockerFnComposed = async () => {
       // If a function is provided, it takes precedence over the promise blocker
@@ -62,19 +51,27 @@ export function useBlocker(
         return await blockerFn()
       }
 
-      setResolver((prev) => ({
-        ...prev,
-        status: 'blocked',
-      }))
+      const promise = new Promise<boolean>((resolve) => {
+        setResolver({
+          status: 'blocked',
+          proceed: () => resolve(true),
+          reset: () => resolve(false),
+        })
+      })
+
       const canNavigateAsync = await promise
 
-      setPromise(createPromise)
+      setResolver({
+        status: 'idle',
+        proceed: () => {},
+        reset: () => {},
+      })
 
       return canNavigateAsync
     }
 
     return !blockerCondition ? undefined : history.block(blockerFnComposed)
-  }, [blockerFn, blockerCondition, history, promise])
+  }, [blockerFn, blockerCondition, history])
 
   return resolver
 }
diff --git a/packages/react-router/src/useLoaderData.tsx b/packages/react-router/src/useLoaderData.tsx
index 56aa836ef..d2fbd8752 100644
--- a/packages/react-router/src/useLoaderData.tsx
+++ b/packages/react-router/src/useLoaderData.tsx
@@ -5,25 +5,34 @@ import type { MakeRouteMatch } from './Matches'
 import type { RouteIds } from './routeInfo'
 import type { StrictOrFrom } from './utils'
 
+export type UseLoaderDataOptions<
+  TFrom,
+  TStrict extends boolean,
+  TRouteMatch,
+  TSelected,
+> = StrictOrFrom<TFrom, TStrict> & {
+  select?: (match: TRouteMatch) => TSelected
+}
+
 export function useLoaderData<
   TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
   TFrom extends RouteIds<TRouteTree> = RouteIds<TRouteTree>,
-  TRouteMatch extends MakeRouteMatch<TRouteTree, TFrom> = MakeRouteMatch<
+  TStrict extends boolean = true,
+  TRouteMatch extends MakeRouteMatch<
     TRouteTree,
-    TFrom
-  >,
+    TFrom,
+    TStrict
+  > = MakeRouteMatch<TRouteTree, TFrom, TStrict>,
   TSelected = Required<TRouteMatch>['loaderData'],
 >(
-  opts: StrictOrFrom<TFrom> & {
-    select?: (match: TRouteMatch) => TSelected
-  },
+  opts: UseLoaderDataOptions<TFrom, TStrict, TRouteMatch, TSelected>,
 ): TSelected {
-  return useMatch({
+  return useMatch<TRouteTree, TFrom, TStrict, TRouteMatch, TSelected>({
     ...opts,
     select: (s) => {
       return typeof opts.select === 'function'
         ? opts.select(s.loaderData as TRouteMatch)
-        : s.loaderData
+        : (s.loaderData as TSelected)
     },
-  }) as TSelected
+  })
 }
diff --git a/packages/react-router/src/useMatch.tsx b/packages/react-router/src/useMatch.tsx
index 563f45b13..75568df35 100644
--- a/packages/react-router/src/useMatch.tsx
+++ b/packages/react-router/src/useMatch.tsx
@@ -8,17 +8,22 @@ import type { MakeRouteMatch } from './Matches'
 import type { RouteIds } from './routeInfo'
 import type { StrictOrFrom } from './utils'
 
+export type UseMatchOptions<
+  TFrom,
+  TStrict extends boolean,
+  TRouteMatch,
+  TSelected,
+> = StrictOrFrom<TFrom, TStrict> & {
+  select?: (match: TRouteMatch) => TSelected
+}
+
 export function useMatch<
   TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
   TFrom extends RouteIds<TRouteTree> = RouteIds<TRouteTree>,
-  TReturnIntersection extends boolean = false,
-  TRouteMatch = MakeRouteMatch<TRouteTree, TFrom, TReturnIntersection>,
+  TStrict extends boolean = true,
+  TRouteMatch = MakeRouteMatch<TRouteTree, TFrom, TStrict>,
   TSelected = TRouteMatch,
->(
-  opts: StrictOrFrom<TFrom, TReturnIntersection> & {
-    select?: (match: TRouteMatch) => TSelected
-  },
-): TSelected {
+>(opts: UseMatchOptions<TFrom, TStrict, TRouteMatch, TSelected>): TSelected {
   const nearestMatchId = React.useContext(matchContext)
 
   const matchSelection = useRouterState({
diff --git a/packages/react-router/src/useParams.tsx b/packages/react-router/src/useParams.tsx
index e0b82f931..3c2913377 100644
--- a/packages/react-router/src/useParams.tsx
+++ b/packages/react-router/src/useParams.tsx
@@ -2,22 +2,26 @@ import { useMatch } from './useMatch'
 import type { AnyRoute } from './route'
 import type { AllParams, RouteById, RouteIds } from './routeInfo'
 import type { RegisteredRouter } from './router'
-import type { Expand } from './utils'
 import type { StrictOrFrom } from './utils'
 
+export type UseParamsOptions<
+  TFrom,
+  TStrict extends boolean,
+  TParams,
+  TSelected,
+> = StrictOrFrom<TFrom, TStrict> & {
+  select?: (params: TParams) => TSelected
+}
+
 export function useParams<
   TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
   TFrom extends RouteIds<TRouteTree> = RouteIds<TRouteTree>,
-  TReturnIntersection extends boolean = false,
-  TParams = TReturnIntersection extends false
-    ? RouteById<TRouteTree, TFrom>['types']['allParams']
-    : Expand<Partial<AllParams<TRouteTree>>>,
+  TStrict extends boolean = true,
+  TParams = TStrict extends false
+    ? AllParams<TRouteTree>
+    : RouteById<TRouteTree, TFrom>['types']['allParams'],
   TSelected = TParams,
->(
-  opts: StrictOrFrom<TFrom, TReturnIntersection> & {
-    select?: (params: TParams) => TSelected
-  },
-): TSelected {
+>(opts: UseParamsOptions<TFrom, TStrict, TParams, TSelected>): TSelected {
   return useMatch({
     ...opts,
     select: (match) => {
diff --git a/packages/react-router/src/useRouteContext.ts b/packages/react-router/src/useRouteContext.ts
index 81a490600..772f1513e 100644
--- a/packages/react-router/src/useRouteContext.ts
+++ b/packages/react-router/src/useRouteContext.ts
@@ -1,19 +1,29 @@
 import { useMatch } from './useMatch'
 import type { MakeRouteMatch } from './Matches'
 import type { AnyRoute } from './route'
-import type { RouteById, RouteIds } from './routeInfo'
+import type { AllContext, RouteById, RouteIds } from './routeInfo'
 import type { RegisteredRouter } from './router'
-import type { StrictOrFrom } from './utils'
+import type { Expand, StrictOrFrom } from './utils'
+
+export type UseRouteContextOptions<
+  TFrom,
+  TStrict extends boolean,
+  TRouteContext,
+  TSelected,
+> = StrictOrFrom<TFrom, TStrict> & {
+  select?: (search: TRouteContext) => TSelected
+}
 
 export function useRouteContext<
   TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
   TFrom extends RouteIds<TRouteTree> = RouteIds<TRouteTree>,
-  TRouteContext = RouteById<TRouteTree, TFrom>['types']['allContext'],
+  TStrict extends boolean = true,
+  TRouteContext = TStrict extends false
+    ? AllContext<TRouteTree>
+    : Expand<RouteById<TRouteTree, TFrom>['types']['allContext']>,
   TSelected = TRouteContext,
 >(
-  opts: StrictOrFrom<TFrom> & {
-    select?: (search: TRouteContext) => TSelected
-  },
+  opts: UseRouteContextOptions<TFrom, TStrict, TRouteContext, TSelected>,
 ): TSelected {
   return useMatch({
     ...(opts as any),
diff --git a/packages/react-router/src/useSearch.tsx b/packages/react-router/src/useSearch.tsx
index 5c15706b5..4d2fc778c 100644
--- a/packages/react-router/src/useSearch.tsx
+++ b/packages/react-router/src/useSearch.tsx
@@ -5,22 +5,27 @@ import type { RegisteredRouter } from './router'
 import type { MakeRouteMatch } from './Matches'
 import type { StrictOrFrom } from './utils'
 
+export type UseSearchOptions<
+  TFrom,
+  TStrict extends boolean,
+  TSearch,
+  TSelected,
+> = StrictOrFrom<TFrom, TStrict> & {
+  select?: (search: TSearch) => TSelected
+}
+
 export function useSearch<
   TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
   TFrom extends RouteIds<TRouteTree> = RouteIds<TRouteTree>,
-  TReturnIntersection extends boolean = false,
-  TSearch = TReturnIntersection extends false
-    ? Exclude<
+  TStrict extends boolean = true,
+  TSearch = TStrict extends false
+    ? FullSearchSchema<TRouteTree>
+    : Exclude<
         RouteById<TRouteTree, TFrom>['types']['fullSearchSchema'],
         RootSearchSchema
-      >
-    : Partial<Omit<FullSearchSchema<TRouteTree>, keyof RootSearchSchema>>,
+      >,
   TSelected = TSearch,
->(
-  opts: StrictOrFrom<TFrom, TReturnIntersection> & {
-    select?: (search: TSearch) => TSelected
-  },
-): TSelected {
+>(opts: UseSearchOptions<TFrom, TStrict, TSearch, TSelected>): TSelected {
   return useMatch({
     ...opts,
     select: (match: MakeRouteMatch<TRouteTree, TFrom>) => {
diff --git a/packages/react-router/src/utils.ts b/packages/react-router/src/utils.ts
index 6f33f7c18..95ef366f4 100644
--- a/packages/react-router/src/utils.ts
+++ b/packages/react-router/src/utils.ts
@@ -69,18 +69,38 @@ export type NonNullableUpdater<TPrevious, TResult = TPrevious> =
   | TResult
   | ((prev: TPrevious) => TResult)
 
-// from https://github.com/type-challenges/type-challenges/issues/737
-type LastInUnion<T> =
-  UnionToIntersection<T extends unknown ? (x: T) => 0 : never> extends (
-    x: infer L,
-  ) => 0
-    ? L
+export type MergeUnionObjects<TUnion> = TUnion extends MergeUnionPrimitive
+  ? never
+  : TUnion
+
+export type MergeUnionObject<TUnion> =
+  MergeUnionObjects<TUnion> extends infer TObj
+    ? {
+        [TKey in TObj extends any ? keyof TObj : never]?: TObj extends any
+          ? TKey extends keyof TObj
+            ? TObj[TKey]
+            : never
+          : never
+      }
     : never
-export type UnionToTuple<T, TLast = LastInUnion<T>> = [T] extends [never]
-  ? []
-  : [...UnionToTuple<Exclude<T, TLast>>, TLast]
 
-//
+export type MergeUnionPrimitive =
+  | ReadonlyArray<any>
+  | number
+  | string
+  | bigint
+  | boolean
+  | symbol
+
+export type MergeUnionPrimitives<TUnion> = TUnion extends MergeUnionPrimitive
+  ? TUnion
+  : TUnion extends object
+    ? never
+    : TUnion
+
+export type MergeUnion<TUnion> =
+  | MergeUnionPrimitives<TUnion>
+  | MergeUnionObject<MergeUnionObjects<TUnion>>
 
 export function last<T>(arr: Array<T>) {
   return arr[arr.length - 1]
@@ -268,15 +288,17 @@ export type StringLiteral<T> = T extends string
     : T
   : never
 
-export type StrictOrFrom<TFrom, TReturnIntersection extends boolean = false> =
-  | {
-      from: StringLiteral<TFrom> | TFrom
-      strict?: true
-    }
-  | {
+export type StrictOrFrom<
+  TFrom,
+  TStrict extends boolean = true,
+> = TStrict extends false
+  ? {
       from?: never
-      strict: false
-      experimental_returnIntersection?: TReturnIntersection
+      strict: TStrict
+    }
+  : {
+      from: StringLiteral<TFrom> | TFrom
+      strict?: TStrict
     }
 
 export const useLayoutEffect =
diff --git a/packages/router-devtools/package.json b/packages/router-devtools/package.json
index 571472f82..5120933e1 100644
--- a/packages/router-devtools/package.json
+++ b/packages/router-devtools/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@tanstack/router-devtools",
-  "version": "1.34.0",
+  "version": "1.34.6",
   "description": "",
   "author": "Tanner Linsley",
   "license": "MIT",
@@ -56,7 +56,6 @@
     "src"
   ],
   "dependencies": {
-    "@tanstack/react-router": "workspace:*",
     "clsx": "^2.1.0",
     "date-fns": "^2.29.1",
     "goober": "^2.1.14"
@@ -67,6 +66,7 @@
     "react-dom": "^18.2.0"
   },
   "peerDependencies": {
+    "@tanstack/react-router": "workspace:^",
     "react": ">=16.8",
     "react-dom": ">=16.8"
   }
diff --git a/packages/router-vite-plugin/package.json b/packages/router-vite-plugin/package.json
index 42f893c8d..4a27588b6 100644
--- a/packages/router-vite-plugin/package.json
+++ b/packages/router-vite-plugin/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@tanstack/router-vite-plugin",
-  "version": "1.33.12",
+  "version": "1.34.1",
   "description": "",
   "author": "Tanner Linsley",
   "license": "MIT",
diff --git a/packages/router-vite-plugin/src/index.ts b/packages/router-vite-plugin/src/index.ts
index 1544301cd..9eaf1662c 100644
--- a/packages/router-vite-plugin/src/index.ts
+++ b/packages/router-vite-plugin/src/index.ts
@@ -27,7 +27,7 @@ const debug = Boolean(process.env.TSR_VITE_DEBUG)
 const getConfig = async (inlineConfig: Partial<Config>, root: string) => {
   const config = await getGeneratorConfig(inlineConfig, root)
 
-  return configSchema.parse({ ...inlineConfig, ...config })
+  return configSchema.parse({ ...config, ...inlineConfig })
 }
 
 export function TanStackRouterVite(
diff --git a/packages/start/package.json b/packages/start/package.json
index 6f6cd8471..f42692266 100644
--- a/packages/start/package.json
+++ b/packages/start/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@tanstack/start",
-  "version": "1.34.0",
+  "version": "1.34.6",
   "description": "",
   "author": "Tanner Linsley",
   "license": "MIT",
@@ -120,6 +120,7 @@
     "@vinxi/server-components": "^0.3.3",
     "@vinxi/server-functions": "^0.3.2",
     "import-meta-resolve": "^4.0.0",
+    "isbot": "^5.1.7",
     "jsesc": "^3.0.2",
     "tiny-invariant": "^1.3.1",
     "vinxi": "0.3.11",
diff --git a/packages/start/src/client/headers.ts b/packages/start/src/client/headers.ts
new file mode 100644
index 000000000..83c263e56
--- /dev/null
+++ b/packages/start/src/client/headers.ts
@@ -0,0 +1,45 @@
+import type { OutgoingHttpHeaders } from 'http2'
+
+// A utility function to turn HeadersInit into an object
+export function headersInitToObject(
+  headers: HeadersInit,
+): Record<keyof OutgoingHttpHeaders, string> {
+  const obj: Record<keyof OutgoingHttpHeaders, string> = {}
+  const headersInstance = new Headers(headers)
+  for (const [key, value] of headersInstance.entries()) {
+    obj[key] = value
+  }
+  return obj
+}
+
+type AnyHeaders =
+  | Headers
+  | HeadersInit
+  | Record<string, string>
+  | Array<[string, string]>
+  | OutgoingHttpHeaders
+  | undefined
+
+// Helper function to convert various HeaderInit types to a Headers instance
+function toHeadersInstance(init: AnyHeaders) {
+  if (init instanceof Headers) {
+    return new Headers(init)
+  } else if (Array.isArray(init)) {
+    return new Headers(init)
+  } else if (typeof init === 'object') {
+    return new Headers(init as HeadersInit)
+  } else {
+    return new Headers()
+  }
+}
+
+// Function to merge headers with proper overrides
+export function mergeHeaders(...headers: Array<AnyHeaders>) {
+  return headers.reduce((acc: Headers, header) => {
+    const headersInstance = toHeadersInstance(header)
+    for (const [key, value] of headersInstance.entries()) {
+      acc.set(key, value)
+    }
+    return acc
+  }, new Headers())
+}
diff --git a/packages/start/src/client/index.tsx b/packages/start/src/client/index.tsx
index 9381a5d81..65f5efa32 100644
--- a/packages/start/src/client/index.tsx
+++ b/packages/start/src/client/index.tsx
@@ -20,3 +20,4 @@ export {
   serverFnPayloadTypeHeader,
   serverFnReturnTypeHeader,
 } from '../constants'
+export { mergeHeaders } from './headers'
diff --git a/packages/start/src/config/index.ts b/packages/start/src/config/index.ts
index 09cc559e5..caa6c5df4 100644
--- a/packages/start/src/config/index.ts
+++ b/packages/start/src/config/index.ts
@@ -1,6 +1,6 @@
 /* eslint-disable no-shadow */
 import path from 'path'
-import { readFileSync, writeFileSync } from 'fs'
+import { existsSync, readFileSync, writeFileSync } from 'fs'
 import { readFile } from 'fs/promises'
 import reactRefresh from '@vitejs/plugin-react'
 import { resolve } from 'import-meta-resolve'
@@ -20,91 +20,108 @@ import type * as vite from 'vite'
 
 const viteSchema = z
   .object({
-    plugins: z.function().returns(z.array(z.custom<vite.Plugin>())),
+    plugins: z.function().returns(z.array(z.custom<vite.Plugin>())).optional(),
   })
-  .partial()
+  .optional()
+  .default({})
 
 const babelSchema = z
   .object({
-    plugins: z.array(z.union([z.tuple([z.string(), z.any()]), z.string()])),
+    plugins: z
+      .array(z.union([z.tuple([z.string(), z.any()]), z.string()]))
+      .optional(),
   })
-  .partial()
+  .optional()
+  .default({})
 
 const reactSchema = z
   .object({
     babel: babelSchema,
   })
-  .partial()
+  .optional()
+  .default({})
 
 const routersSchema = z
   .object({
     ssr: z
       .object({
-        entry: z.string(),
+        entry: z.string().default('./app/ssr.tsx'),
         vite: viteSchema,
       })
-      .partial(),
+      .optional()
+      .default({}),
     rsc: z
       .object({
         vite: viteSchema,
       })
-      .partial(),
+      .optional()
+      .default({}),
     client: z
       .object({
-        entry: z.string(),
-        base: z.string(),
+        entry: z.string().optional().default('./app/client.tsx'),
+        base: z.string().optional(),
         vite: viteSchema,
       })
-      .partial(),
+      .optional()
+      .default({}),
     server: z
       .object({
         vite: viteSchema,
       })
-      .partial(),
+      .optional()
+      .default({}),
   })
-  .partial()
+  .optional()
+  .default({})
 
 const optsSchema = z
   .object({
-    tsr: configSchema.partial(),
+    tsr: configSchema
+      .partial()
+      .extend({
+        appDirectory: z.string().default('./app'),
+        // Normally these are `./src/___`, but we're using `./app/___` for Start stuff
+        routesDirectory: z.string().default('./app/routes'),
+        generatedRouteTree: z.string().default('./app/routeTree.gen.ts'),
+      })
+      .optional()
+      .default({}),
     react: reactSchema,
     vite: viteSchema,
     routers: routersSchema,
   })
-  .partial()
   .optional()
+  .default({})
 
-export async function defineConfig(opts?: z.infer<typeof optsSchema>) {
-  optsSchema.parse(opts)
+export async function defineConfig(opts_?: z.infer<typeof optsSchema>) {
+  const opts = optsSchema.parse(opts_)
 
-  const tsrConfig = await getConfig(opts?.tsr)
+  const tsrConfig = await getConfig(opts.tsr)
 
   const startVite = () => {
     return config('start-vite', {
       ssr: {
-        // external: ['@tanstack/start/server-runtime'],
-        noExternal: [
-          '@tanstack/start',
-          'tsr:routes-manifest',
-          // '@tanstack/start/server-runtime',
-        ],
+        noExternal: ['@tanstack/start', 'tsr:routes-manifest'],
       },
       optimizeDeps: {
         include: ['@tanstack/start/server-runtime'],
       },
       plugins: [
         TanStackRouterVite({
-          ...opts?.tsr,
+          ...tsrConfig,
           experimental: {
+            ...opts.tsr.experimental,
             enableCodeSplitting: true,
-            ...opts?.tsr?.experimental,
           },
         }),
       ],
     })
   }
 
-  const clientBase = opts?.routers?.client?.base || '/_build'
+  const clientBase = opts.routers.client.base || '/_build'
+
+  const clientEntry = opts.routers.client.entry
+  const ssrEntry = opts.routers.ssr.entry
 
   return createApp({
     server: {
@@ -129,16 +146,16 @@ export async function defineConfig(opts?: z.infer<typeof optsSchema>) {
       //   target: 'server',
       //   plugins: () => [
       //     startVite(),
-      //     ...(opts?.vite?.plugins?.() || []),
-      //     ...(opts?.routers?.rsc?.vite?.plugins?.() || []),
+      //     ...(opts.vite?.plugins?.() || []),
+      //     ...(opts.routers.rsc?.vite?.plugins?.() || []),
       //     serverComponents.server(),
       //     reactRefresh(),
       //   ],
       // }),
-      startRouterProxy({
+      startRouterProxy(tsrConfig)({
         name: 'client',
         type: 'client',
-        handler: opts?.routers?.client?.entry || './app/client.tsx',
+        handler: clientEntry,
         target: 'browser',
         base: clientBase,
         build: {
@@ -146,21 +163,21 @@ export async function defineConfig(opts?: z.infer<typeof optsSchema>) {
         },
         plugins: () => [
           startVite(),
-          ...(opts?.vite?.plugins?.() || []),
-          ...(opts?.routers?.client?.vite?.plugins?.() || []),
+          ...(opts.vite.plugins?.() || []),
+          ...(opts.routers.client.vite.plugins?.() || []),
           serverFunctions.client({
             runtime: '@tanstack/start/client-runtime',
           }),
           reactRefresh({
-            babel: opts?.react?.babel,
+            babel: opts.react.babel,
           }),
           // serverComponents.client(),
         ],
       }),
-      startRouterProxy({
+      startRouterProxy(tsrConfig)({
         name: 'ssr',
         type: 'http',
-        handler: opts?.routers?.ssr?.entry || './app/server.tsx',
+        handler: ssrEntry,
         target: 'server',
         plugins: () => [
           startVite(),
@@ -168,8 +185,8 @@ export async function defineConfig(opts?: z.infer<typeof optsSchema>) {
             tsrConfig,
             clientBase,
           }),
-          ...(opts?.vite?.plugins?.() || []),
-          ...(opts?.routers?.ssr?.vite?.plugins?.() || []),
+          ...(opts.vite.plugins?.() || []),
+          ...(opts.routers.ssr.vite.plugins?.() || []),
           serverTransform({
             runtime: '@tanstack/start/server-runtime',
           }),
@@ -178,13 +195,13 @@ export async function defineConfig(opts?: z.infer<typeof optsSchema>) {
           client: 'client',
         },
       }),
-      startRouterProxy(
+      startRouterProxy(tsrConfig)(
         serverFunctions.router({
           name: 'server',
           plugins: () => [
             startVite(),
-            ...(opts?.vite?.plugins?.() || []),
-            ...(opts?.routers?.server?.vite?.plugins?.() || []),
+            ...(opts.vite.plugins?.() || []),
+            ...(opts.routers.server.vite.plugins?.() || []),
             // serverComponents.serverActions(),
           ],
           // For whatever reason, vinxi expects a path relative
@@ -197,17 +214,21 @@ export async function defineConfig(opts?: z.infer<typeof optsSchema>) {
   })
 }
 
-function startRouterProxy(router: any) {
-  return {
-    ...router,
-    plugins: async () => [
-      TanStackRouterVite({
-        experimental: {
-          enableCodeSplitting: true,
-        },
-      }),
-      ...((await router?.plugins?.()) ?? []),
-    ],
+function startRouterProxy(tsrConfig: z.infer<typeof configSchema>) {
+  return (router: any) => {
+    return {
+      ...router,
+      plugins: async () => [
+        TanStackRouterVite({
+          ...tsrConfig,
+          experimental: {
+            ...tsrConfig.experimental,
+            enableCodeSplitting: true,
+          },
+        }),
+        ...((await router?.plugins?.()) ?? []),
+      ],
+    }
   }
 }
 
diff --git a/packages/start/src/router-manifest/index.ts b/packages/start/src/router-manifest/index.ts
index 57c161ca7..8e37d2e8d 100644
--- a/packages/start/src/router-manifest/index.ts
+++ b/packages/start/src/router-manifest/index.ts
@@ -1,8 +1,69 @@
+import { getManifest } from 'vinxi/manifest'
 // @ts-expect-error
 // eslint-disable-next-line import/no-unresolved
 import tsrGetManifest from 'tsr:routes-manifest'
 import type { Manifest } from '@tanstack/react-router'
 
+/**
+ * @description Returns the full, unfiltered router manifest. This includes relationships
+ * between routes, assets, and preloads and is NOT what you want to serialize and
+ * send to the client.
+ */
+export function getFullRouterManifest() {
+  const routerManifest = tsrGetManifest() as Manifest
+
+  const rootRoute = (routerManifest.routes.__root__ =
+    routerManifest.routes.__root__ || {})
+
+  rootRoute.assets = rootRoute.assets || []
+
+  // Always fake that HMR is ready
+  if (process.env.NODE_ENV === 'development') {
+    rootRoute.assets.push({
+      tag: 'script',
+      children: `window.__vite_plugin_react_preamble_installed__ = true`,
+    })
+  }
+
+  // Get the entry for the client from vinxi
+  const vinxiClientManifest = getManifest('client')
+
+  rootRoute.assets.push({
+    tag: 'script',
+    attrs: {
+      src: vinxiClientManifest.inputs[vinxiClientManifest.handler]?.output.path,
+      type: 'module',
+      async: true,
+      suppressHydrationWarning: true,
+    },
+  })
+
+  return routerManifest
+}
+
+/**
+ * @description Returns the router manifest that should be sent to the client.
+ * This includes only the assets and preloads for the current route and any
+ * special assets that are needed for the client. It does not include relationships
+ * between routes or any other data that is not needed for the client.
+ */
 export function getRouterManifest() {
-  return tsrGetManifest() as Manifest
+  const routerManifest = getFullRouterManifest()
+
+  // Strip out anything that isn't needed for the client
+  return {
+    ...routerManifest,
+    routes: Object.fromEntries(
+      Object.entries(routerManifest.routes).map(([k, v]: any) => {
+        const { preloads, assets } = v
+        return [
+          k,
+          {
+            preloads,
+            assets,
+          },
+        ]
+      }),
+    ),
+  }
 }
diff --git a/packages/start/src/server/createRequestHandler.ts b/packages/start/src/server/createRequestHandler.ts
new file mode 100644
index 000000000..31e8360e2
--- /dev/null
+++ b/packages/start/src/server/createRequestHandler.ts
@@ -0,0 +1,63 @@
+import { eventHandler, toWebRequest } from 'vinxi/http'
+import { type AnyRouter, createMemoryHistory } from '@tanstack/react-router'
+import { getRequestHeaders } from './getRequestHeaders'
+import type { Manifest } from '@tanstack/react-router'
+import type { EventHandler } from 'vinxi/http'
+
+export type RequestHandler<TRouter extends AnyRouter> = (ctx: {
+  request: Request
+  router: TRouter
+  responseHeaders: Headers
+}) => Promise<Response>
+
+export type CustomizeRequestHandler<TRouter extends AnyRouter> = (
+  cb: RequestHandler<TRouter>,
+) => EventHandler
+
+export function createRequestHandler<TRouter extends AnyRouter>({
+  createRouter,
+  getRouterManifest,
+}: {
+  createRouter: () => TRouter
+  getRouterManifest?: () => Manifest
+}): CustomizeRequestHandler<TRouter> {
+  return (cb) => {
+    return eventHandler(async (event) => {
+      const request = toWebRequest(event)
+
+      const url = new URL(request.url)
+      const href = url.href.replace(url.origin, '')
+
+      // Create a history for the router
+      const history = createMemoryHistory({
+        initialEntries: [href],
+      })
+
+      const router = createRouter()
+
+      if (getRouterManifest) {
+        router.manifest = getRouterManifest()
+      }
+
+      // Update the router with the history and context
+      router.update({
+        history,
+      })
+
+      await router.load()
+
+      const responseHeaders = getRequestHeaders({
+        event,
+        router,
+      })
+
+      const response = await cb({
+        request,
+        router,
+        responseHeaders,
+      })
+
+      return response
+    })
+  }
+}
diff --git a/packages/start/src/server/defaultStreamHandler.tsx b/packages/start/src/server/defaultStreamHandler.tsx
new file mode 100644
index 000000000..76b18ed79
--- /dev/null
+++ b/packages/start/src/server/defaultStreamHandler.tsx
@@ -0,0 +1,40 @@
+import { PassThrough } from 'stream'
+import * as React from 'react'
+import { isbot } from 'isbot'
+import { renderToPipeableStream } from 'react-dom/server'
+import { StartServer } from './StartServer'
+import { type RequestHandler } from './createRequestHandler'
+import type { AnyRouter } from '@tanstack/react-router'
+
+export const defaultStreamHandler: RequestHandler<AnyRouter> = async ({
+  request,
+  router,
+  responseHeaders,
+}) => {
+  const passthrough = new PassThrough()
+
+  const pipeable = renderToPipeableStream(<StartServer router={router} />, {
+    ...(isbot(request.headers.get('User-Agent'))
+      ? {
+          onAllReady() {
+            pipeable.pipe(passthrough)
+          },
+        }
+      : {
+          onShellReady() {
+            pipeable.pipe(passthrough)
+          },
+        }),
+    onShellError(err) {
+      throw err
+    },
+  })
+
+  return new Response(passthrough as any, {
+    status: router.state.statusCode,
+    statusText: `${router.state.statusCode}`.startsWith('5')
+      ? 'Internal Server Error'
+      : 'OK',
+    headers: responseHeaders,
+  })
+}
diff --git a/packages/start/src/server/getRequestHeaders.ts b/packages/start/src/server/getRequestHeaders.ts
new file mode 100644
index 000000000..c840f3c63
--- /dev/null
+++ b/packages/start/src/server/getRequestHeaders.ts
@@ -0,0 +1,44 @@
+import {
+  type EventHandlerRequest,
+  type H3Event,
+  getResponseHeaders,
+} from 'vinxi/http'
+import { mergeHeaders } from '../client/headers'
+import {
+  serverFnPayloadTypeHeader,
+  serverFnReturnTypeHeader,
+} from '../constants'
+import type { AnyRouter } from '@tanstack/react-router'
+
+export function getRequestHeaders(opts: {
+  event: H3Event<EventHandlerRequest>
+  router: AnyRouter
+}): Headers {
+  ;(opts.event as any).__tsrHeadersSent = true
+
+  let headers = mergeHeaders(
+    getResponseHeaders(opts.event),
+    {
+      'Content-Type': 'text/html; charset=UTF-8',
+    },
+    ...opts.router.state.matches.map((match) => {
+      return match.headers
+    }),
+  )
+
+  // Handle Redirects
+  const { redirect } = opts.router.state
+
+  if (redirect) {
+    headers = mergeHeaders(headers, redirect.headers, {
+      Location: redirect.href,
+    })
+  }
+
+  // Remove server function headers
+  ;[serverFnReturnTypeHeader, serverFnPayloadTypeHeader].forEach((header) => {
+    headers.delete(header)
+  })
+
+  return headers
+}
diff --git a/packages/start/src/server/index.tsx b/packages/start/src/server/index.tsx
index 2a8c2cb34..211b0ccf4 100644
--- a/packages/start/src/server/index.tsx
+++ b/packages/start/src/server/index.tsx
@@ -1,7 +1,6 @@
 /// <reference types="vinxi/types/server" />
 
 export { StartServer } from './StartServer'
-export {
-  transformStreamWithRouter,
-  transformReadableStreamWithRouter,
-} from './transformStreamWithRouter'
+export { getRequestHeaders } from './getRequestHeaders'
+export { createRequestHandler } from './createRequestHandler'
+export { defaultStreamHandler } from './defaultStreamHandler'
diff --git a/packages/start/src/server/transformStreamWithRouter.tsx b/packages/start/src/server/transformStreamWithRouter.tsx
deleted file mode 100644
index a8fb5f7a9..000000000
--- a/packages/start/src/server/transformStreamWithRouter.tsx
+++ /dev/null
@@ -1,139 +0,0 @@
-import { Transform } from 'stream'
-import type { AnyRouter } from '@tanstack/react-router'
-
-export function transformStreamWithRouter(router: AnyRouter) {
-  const callbacks = transformHtmlCallbacks(injectorFromRouter(router))
-  return new Transform({
-    transform(chunk, _encoding, callback) {
-      callbacks
-        .transform(chunk, this.push.bind(this))
-        .then(() => callback())
-        .catch((err) => callback(err))
-    },
-    flush(callback) {
-      callbacks
-        .flush(this.push.bind(this))
-        .then(() => callback())
-        .catch((err) => callback(err))
-    },
-  })
-}
-
-export function transformReadableStreamWithRouter(router: AnyRouter) {
-  const callbacks = transformHtmlCallbacks(injectorFromRouter(router))
-  return new TransformStream<string>({
-    transform(chunk, controller) {
-      return callbacks.transform(chunk, (chunkToPush) => {
-        controller.enqueue(chunkToPush)
-        return true
-      })
-    },
-    flush(controller) {
-      return callbacks.flush((chunkToPush) => {
-        controller.enqueue(chunkToPush)
-        return true
-      })
-    },
-  })
-}
-
-function injectorFromRouter(router: AnyRouter) {
-  return async () => {
-    const injectorPromises = router.injectedHtml.map((d) =>
-      typeof d === 'function' ? d() : d,
-    )
-    const injectors = await Promise.all(injectorPromises)
-    router.injectedHtml = []
-    return injectors.join('')
-  }
-}
-
-// regex pattern for matching closing body and html tags
-const patternBodyStart = /(<body)/
-const patternBodyEnd = /(<\/body>)/
-const patternHtmlEnd = /(<\/html>)/
-
-// regex pattern for matching closing tags
-const pattern = /(<\/[a-zA-Z][\w:.-]*?>)/g
-
-const textDecoder = new TextDecoder()
-
-function transformHtmlCallbacks(injector: () => Promise<string>) {
-  let bodyStarted = false
-  let leftover = ''
-  // If a closing tag is split across chunks, store the HTML to add after it
-  // This expects that all the HTML that's added is closed properly
-  let leftoverHtml = ''
-
-  return {
-    async transform(chunk: any, push: (chunkToPush: string) => boolean) {
-      const chunkString = leftover + textDecoder.decode(chunk)
-
-      const bodyStartMatch = chunkString.match(patternBodyStart)
-      const bodyEndMatch = chunkString.match(patternBodyEnd)
-      const htmlEndMatch = chunkString.match(patternHtmlEnd)
-
-      try {
-        const html = await injector()
-
-        if (bodyStartMatch) {
-          bodyStarted = true
-        }
-
-        if (!bodyStarted) {
-          push(chunkString)
-          leftover = ''
-          return
-        }
-
-        // If a </body></html> sequence was found
-        if (
-          bodyEndMatch &&
-          htmlEndMatch &&
-          bodyEndMatch.index! < htmlEndMatch.index!
-        ) {
-          const bodyIndex = bodyEndMatch.index! + bodyEndMatch[0].length
-          const htmlIndex = htmlEndMatch.index! + htmlEndMatch[0].length
-
-          // Add the arbitrary HTML before the closing body tag
-          const processed =
-            chunkString.slice(0, bodyIndex) +
-            html +
-            chunkString.slice(bodyIndex, htmlIndex) +
-            chunkString.slice(htmlIndex)
-
-          push(processed)
-          leftover = ''
-        } else {
-          // For all other closing tags, add the arbitrary HTML after them
-          let result
-          let lastIndex = 0
-
-          while ((result = pattern.exec(chunkString)) !== null) {
-            lastIndex = result.index + result[0].length
-          }
-
-          // If a closing tag was found, add the arbitrary HTML and send it through
-          if (lastIndex > 0) {
-            const processed =
-              chunkString.slice(0, lastIndex) + html + leftoverHtml
-            push(processed)
-            leftover = chunkString.slice(lastIndex)
-          } else {
-            // If no closing tag was found, store the chunk to process with the next one
-            leftover = chunkString
-            leftoverHtml += html
-          }
-        }
-      } catch (err) {
-        console.error(err)
-        throw err
-      }
-    },
-    async flush(push: (chunkToPush: string) => boolean) {
-      if (leftover) {
-        push(leftover)
-      }
-    },
-  }
-}
diff --git a/packages/start/vite.config.ts b/packages/start/vite.config.ts
index f90bbcd30..b57f32008 100644
--- a/packages/start/vite.config.ts
+++ b/packages/start/vite.config.ts
@@ -6,39 +6,14 @@ import replace from '@rollup/plugin-replace'
 import react from '@vitejs/plugin-react'
 
 const config = defineConfig({
-  plugins: [
-    react(),
-    // @ts-ignore
-    // replace({
-    //   'import.meta.env': '__import__meta__env__',
-    // }),
-    // (() => {
-    //   return {
-    //     name: 'replace-import-meta-env',
-    //     writeBundle(options, bundle) {
-    //       const basePath = options.dir || path.dirname(options.file || '')
-
-    //       for (const [fileName, fileEntry] of Object.entries(bundle)) {
-    //         if (fileEntry.type === 'chunk') {
-    //           const fullPath = path.resolve(basePath, fileName)
-    //           fs.writeFileSync(
-    //             fullPath,
-    //             fs
-    //               .readFileSync(fullPath, 'utf-8')
-    //               .replace(/__import__meta__env__/g, 'import.meta.env'),
-    //           )
-    //         }
-    //       }
-    //     },
-    //   }
-    // })(),
-  ],
+  plugins: [react()],
 })
 
 export default mergeConfig(
   // @ts-ignore
   config,
   tanstackBuildConfig({
+    externalDeps: ['@tanstack/start/router-manifest'],
     entry: [
       './src/client/index.tsx',
       './src/server/index.tsx',
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1be00e361..23d14d486 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -939,6 +939,9 @@ importers:
       '@vitejs/plugin-react':
         specifier: ^4
         version: 4.2.1(vite@5.2.11)
+      dotenv:
+        specifier: ^16.4.5
+        version: 16.4.5
       isbot:
         specifier: ^3.7.0
         version: 3.8.0
@@ -958,6 +961,15 @@ importers:
         specifier: ^0.3.11
         version: 0.3.11(@opentelemetry/api@1.8.0)(@types/node@20.12.11)
     devDependencies:
+      '@playwright/test':
+        specifier: ^1.44.0
+        version: 1.44.0
+      '@replayio/playwright':
+        specifier: ^3.0.1
+        version: 3.0.1(@playwright/test@1.44.0)
+      '@types/node':
+        specifier: ^20.12.11
+        version: 20.12.11
       '@types/react':
         specifier: ^18.2.65
         version: 18.3.1
@@ -1554,6 +1566,9 @@ importers:
       import-meta-resolve:
         specifier: ^4.0.0
         version: 4.1.0
+      isbot:
+        specifier: ^5.1.7
+        version: 5.1.7
       jsesc:
         specifier: ^3.0.2
         version: 3.0.2
@@ -4723,6 +4738,14 @@ packages:
     requiresBuild: true
     optional: true
 
+  /@playwright/test@1.44.0:
+    resolution: {integrity: sha512-rNX5lbNidamSUorBhB4XZ9SQTjAqfe5M+p37Z8ic0jPFBMo5iCtQz1kRWkEMg+rYOKSlVycpQmpqjSFq7LXOfg==}
+    engines: {node: '>=16'}
+    hasBin: true
+    dependencies:
+      playwright: 1.44.0
+    dev: true
+
   /@radix-ui/primitive@1.0.1:
     resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==}
     dependencies:
@@ -5005,6 +5028,80 @@ packages:
       react: 18.3.1
     dev: false
 
+  /@replayio/playwright@3.0.1(@playwright/test@1.44.0):
+    resolution: {integrity: sha512-mJJatPja2pwtjewk0Zm3Ku021YL/aO7ink4Cs1gpjZ/ZUxDHiI8dqsF82sKzFrT3dmySABlT/N2jv4QVlnVMQQ==}
+    peerDependencies:
+      '@playwright/test': ^1.34.0
+    dependencies:
+      '@playwright/test': 1.44.0
+      '@replayio/replay': 0.22.5
+      '@replayio/test-utils': 2.1.1
+      debug: 4.3.4
+      stack-utils: 2.0.6
+      uuid: 8.3.2
+      ws: 8.16.0
+    transitivePeerDependencies:
+      - bufferutil
+      - encoding
+      - supports-color
+      - utf-8-validate
+    dev: true
+
+  /@replayio/replay@0.22.5:
+    resolution: {integrity: sha512-HRkxqAeZc+y8UcyG/eaJJuDgla8B4IWJjXjf8ilAWfRkZbL5MZDMtU+aLB/Ssf0gNpyw2x1UMmlQ0hS9/uMZJQ==}
+    hasBin: true
+    dependencies:
+      '@replayio/sourcemap-upload': 2.0.3
+      '@types/semver': 7.5.8
+      commander: 12.0.0
+      debug: 4.3.4
+      is-uuid: 1.0.2
+      jsonata: 1.8.7
+      launchdarkly-node-client-sdk: 3.2.1
+      node-fetch: 2.7.0
+      p-map: 4.0.0
+      query-registry: 2.6.0
+      semver: 7.6.2
+      superstruct: 0.15.5
+      text-table: 0.2.0
+      ws: 7.5.9
+    transitivePeerDependencies:
+      - bufferutil
+      - encoding
+      - supports-color
+      - utf-8-validate
+    dev: true
+
+  /@replayio/sourcemap-upload@2.0.3:
+    resolution: {integrity: sha512-ZFKW5ZhSosDjWBXpp3n9+DFwOos8QUv2BqQkKOBYFri6ogmLB6ayctbHkL00wGztfGm1BBh0JQ3yJ4pvDplJGw==}
+    engines: {node: '>=10.13'}
+    dependencies:
+      commander: 7.2.0
+      debug: 4.3.4
+      glob: 7.2.3
+      node-fetch: 2.7.0
+      p-map: 4.0.0
+      string.prototype.matchall: 4.0.11
+    transitivePeerDependencies:
+      - encoding
+      - supports-color
+    dev: true
+
+  /@replayio/test-utils@2.1.1:
+    resolution: {integrity: sha512-+qEs41gIIrXTEALFEoIBVhWSvc56uteNd0TtiGev5WOSYT1BvtA77KYify6QrMR2gYPhxWeMxNu+IbpYJOe5pQ==}
+    dependencies:
+      '@replayio/replay': 0.22.5
+      debug: 4.3.4
+      node-fetch: 2.7.0
+      sha-1: 1.0.0
+      uuid: 8.3.2
+    transitivePeerDependencies:
+      - bufferutil
+      - encoding
+      - supports-color
+      - utf-8-validate
+    dev: true
+
   /@rollup/plugin-alias@5.1.0(rollup@4.17.2):
     resolution: {integrity: sha512-lpA3RZ9PdIG7qqhEfv79tBffNaoDuukFDrmhLqg9ifv99u/ehn+lOg30x2zmhf8AQqQUZaMk/B9fZraQ6/acDQ==}
     engines: {node: '>=14.0.0'}
@@ -6539,6 +6636,14 @@ packages:
       - supports-color
     dev: true
 
+  /aggregate-error@3.1.0:
+    resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
+    engines: {node: '>=8'}
+    dependencies:
+      clean-stack: 2.2.0
+      indent-string: 4.0.0
+    dev: true
+
   /ajv-formats@2.1.1(ajv@8.12.0):
     resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
     peerDependencies:
@@ -7114,6 +7219,12 @@ packages:
     resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==}
     engines: {node: '>=6'}
 
+  /builtins@5.1.0:
+    resolution: {integrity: sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==}
+    dependencies:
+      semver: 7.6.2
+    dev: true
+
   /bytes@3.0.0:
     resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==}
     engines: {node: '>= 0.8'}
@@ -7260,6 +7371,11 @@ packages:
       escape-string-regexp: 1.0.5
     dev: true
 
+  /clean-stack@2.2.0:
+    resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
+    engines: {node: '>=6'}
+    dev: true
+
   /cli-boxes@3.0.0:
     resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==}
     engines: {node: '>=10'}
@@ -7347,6 +7463,11 @@ packages:
     engines: {node: '>= 6'}
     dev: true
 
+  /commander@7.2.0:
+    resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
+    engines: {node: '>= 10'}
+    dev: true
+
   /commander@9.5.0:
     resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
     engines: {node: ^12.20.0 || >=14}
@@ -8214,6 +8335,11 @@ packages:
     resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
     engines: {node: '>=0.8.0'}
 
+  /escape-string-regexp@2.0.0:
+    resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==}
+    engines: {node: '>=8'}
+    dev: true
+
   /escape-string-regexp@4.0.0:
     resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
     engines: {node: '>=10'}
@@ -8714,6 +8840,10 @@ packages:
     resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==}
     dev: false
 
+  /fast-deep-equal@2.0.1:
+    resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==}
+    dev: true
+
   /fast-deep-equal@3.1.3:
     resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
 
@@ -9011,6 +9141,14 @@ packages:
   /fs.realpath@1.0.0:
     resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
 
+  /fsevents@2.3.2:
+    resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+    engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+    os: [darwin]
+    requiresBuild: true
+    dev: true
+    optional: true
+
   /fsevents@2.3.3:
     resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
     engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -9838,6 +9976,10 @@ packages:
       unc-path-regex: 0.1.2
     dev: true
 
+  /is-uuid@1.0.2:
+    resolution: {integrity: sha512-tCByphFcJgf2qmiMo5hMCgNAquNSagOetVetDvBXswGkNfoyEMvGH1yDlF8cbZbKnbVBr4Y5/rlpMz9umxyBkQ==}
+    dev: true
+
   /is-weakmap@2.0.2:
     resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==}
     engines: {node: '>= 0.4'}
@@ -9904,6 +10046,11 @@ packages:
     engines: {node: '>=18'}
     dev: true
 
+  /isbot@5.1.7:
+    resolution: {integrity: sha512-/U9wtTOhYcP1cnDUFKNalTSQHVD3y4ginXstBBQDRwDKTZ2liBb0IEnRu8X+RpX9VnNN/I3MNNgJvKnKQ5+CnA==}
+    engines: {node: '>=18'}
+    dev: false
+
   /isexe@2.0.0:
     resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
 
@@ -9917,6 +10064,15 @@ packages:
     engines: {node: '>=0.10.0'}
     dev: true
 
+  /isomorphic-unfetch@3.1.0:
+    resolution: {integrity: sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==}
+    dependencies:
+      node-fetch: 2.7.0
+      unfetch: 4.2.0
+    transitivePeerDependencies:
+      - encoding
+    dev: true
+
   /iterator.prototype@1.1.2:
     resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==}
     dependencies:
@@ -10071,6 +10227,11 @@ packages:
     engines: {node: '>=6'}
     hasBin: true
 
+  /jsonata@1.8.7:
+    resolution: {integrity: sha512-tOW2/hZ+nR2bcQZs+0T62LVe5CHaNa3laFFWb/262r39utN6whJGBF7IR2Wq1QXrDbhftolk5gggW8uUJYlBTQ==}
+    engines: {node: '>= 8'}
+    dev: true
+
   /jsonc-parser@3.2.0:
     resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
     dev: true
@@ -10137,6 +10298,28 @@ packages:
       language-subtag-registry: 0.3.22
     dev: true
 
+  /launchdarkly-eventsource@2.0.3:
+    resolution: {integrity: sha512-VhFjppK7jXlcEKaS7bxdoibB5j01NKyeDR7a8XfssdDGNWCTsbF0/5IExSmPi44eDncPhkoPNxlSZhEZvrbD5w==}
+    engines: {node: '>=0.12.0'}
+    dev: true
+
+  /launchdarkly-js-sdk-common@5.2.0:
+    resolution: {integrity: sha512-aLv2ZrUv229RIwLtFhdILu2aJS/fqGSJzTk4L/bCDZA8RuIh7PutI3ui/AJeNnzPzjKzdEQZw6wVhkVc84baog==}
+    dependencies:
+      base64-js: 1.5.1
+      fast-deep-equal: 2.0.1
+      uuid: 8.3.2
+    dev: true
+
+  /launchdarkly-node-client-sdk@3.2.1:
+    resolution: {integrity: sha512-vIn1kFCWSX83M2hHIQEw+TyEZFqcXn4DTKja2Vdt9NFgs0I2BA70ENA+zgz8OSt+VqvTciZV0l5X90Uyv+3vsQ==}
+    engines: {node: '>= 12.0.0'}
+    dependencies:
+      launchdarkly-eventsource: 2.0.3
+      launchdarkly-js-sdk-common: 5.2.0
+      node-localstorage: 1.3.1
+    dev: true
+
   /lazystream@1.0.1:
     resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
     engines: {node: '>= 0.6.3'}
@@ -10357,6 +10540,10 @@ packages:
       semver: 6.3.1
     dev: false
 
+  /make-error@1.3.6:
+    resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
+    dev: true
+
   /make-iterator@1.0.1:
     resolution: {integrity: sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==}
     engines: {node: '>=0.10.0'}
@@ -10694,7 +10881,6 @@ packages:
         optional: true
     dependencies:
       whatwg-url: 5.0.0
-    dev: false
 
   /node-fetch@3.3.2:
     resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
@@ -10715,6 +10901,13 @@ packages:
     hasBin: true
     dev: false
 
+  /node-localstorage@1.3.1:
+    resolution: {integrity: sha512-NMWCSWWc6JbHT5PyWlNT2i8r7PgGYXVntmKawY83k/M0UJScZ5jirb61TLnqKwd815DfBQu+lR3sRw08SPzIaQ==}
+    engines: {node: '>=0.12'}
+    dependencies:
+      write-file-atomic: 1.3.4
+    dev: true
+
   /node-machine-id@1.1.12:
     resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==}
     dev: true
@@ -11116,6 +11309,13 @@ packages:
     dependencies:
       p-limit: 3.1.0
 
+  /p-map@4.0.0:
+    resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==}
+    engines: {node: '>=10'}
+    dependencies:
+      aggregate-error: 3.1.0
+    dev: true
+
   /p-try@2.2.0:
     resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
     engines: {node: '>=6'}
@@ -11313,6 +11513,22 @@ packages:
       mlly: 1.7.0
       pathe: 1.1.2
 
+  /playwright-core@1.44.0:
+    resolution: {integrity: sha512-ZTbkNpFfYcGWohvTTl+xewITm7EOuqIqex0c7dNZ+aXsbrLj0qI8XlGKfPpipjm0Wny/4Lt4CJsWJk1stVS5qQ==}
+    engines: {node: '>=16'}
+    hasBin: true
+    dev: true
+
+  /playwright@1.44.0:
+    resolution: {integrity: sha512-F9b3GUCLQ3Nffrfb6dunPOkE5Mh68tR7zN32L4jCk4FjQamgesGay7/dAAe1WaMEGV04DkdJfcJzjoCKygUaRQ==}
+    engines: {node: '>=16'}
+    hasBin: true
+    dependencies:
+      playwright-core: 1.44.0
+    optionalDependencies:
+      fsevents: 2.3.2
+    dev: true
+
   /pluralize@8.0.0:
     resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
     engines: {node: '>=4'}
@@ -11492,6 +11708,19 @@ packages:
     dependencies:
       side-channel: 1.0.6
 
+  /query-registry@2.6.0:
+    resolution: {integrity: sha512-Z5oNq7qH0g96qBTx2jAvS0X71hKP4tETtSJKEl6BdihzYqh9QKiJQBMT7qIQuzxR9lxfiso+aXCFhZ+EcAoppQ==}
+    engines: {node: '>=12'}
+    dependencies:
+      isomorphic-unfetch: 3.1.0
+      make-error: 1.3.6
+      tiny-lru: 8.0.2
+      url-join: 4.0.1
+      validate-npm-package-name: 4.0.0
+    transitivePeerDependencies:
+      - encoding
+    dev: true
+
   /querystringify@2.2.0:
     resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
     dev: true
@@ -12231,6 +12460,10 @@ packages:
   /setprototypeof@1.2.0:
     resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
 
+  /sha-1@1.0.0:
+    resolution: {integrity: sha512-qjFA/+LdT0Gvu/JcmYTGZMvVy6WXJOWv1KQuY7HvSr2oBrMxA8PnZu2mc1/ZS2EvLMokj7lIeQsNPjkRzXrImw==}
+    dev: true
+
   /shebang-command@1.2.0:
     resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==}
     engines: {node: '>=0.10.0'}
@@ -12291,6 +12524,10 @@ packages:
     engines: {node: '>=14.16'}
     dev: false
 
+  /slide@1.1.6:
+    resolution: {integrity: sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==}
+    dev: true
+
   /smob@1.4.1:
     resolution: {integrity: sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==}
     dev: false
@@ -12370,6 +12607,13 @@ packages:
     resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
     dev: true
 
+  /stack-utils@2.0.6:
+    resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==}
+    engines: {node: '>=10'}
+    dependencies:
+      escape-string-regexp: 2.0.0
+    dev: true
+
   /stackback@0.0.2:
     resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
     dev: true
@@ -12588,6 +12832,10 @@ packages:
       copy-anything: 3.0.2
     dev: false
 
+  /superstruct@0.15.5:
+    resolution: {integrity: sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ==}
+    dev: true
+
   /supports-color@5.5.0:
     resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
     engines: {node: '>=4'}
@@ -12773,6 +13021,11 @@ packages:
     resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==}
     dev: false
 
+  /tiny-lru@8.0.2:
+    resolution: {integrity: sha512-ApGvZ6vVvTNdsmt676grvCkUCGwzG9IqXma5Z07xJgiC5L7akUMof5U8G2JTI9Rz/ovtVhJBlY6mNhEvtjzOIg==}
+    engines: {node: '>=6'}
+    dev: true
+
   /tiny-warning@1.0.3:
     resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
     dev: false
@@ -12829,7 +13082,6 @@ packages:
 
   /tr46@0.0.3:
     resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
-    dev: false
 
   /tr46@5.0.0:
     resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==}
@@ -13085,6 +13337,10 @@ packages:
       pathe: 1.1.2
     dev: false
 
+  /unfetch@4.2.0:
+    resolution: {integrity: sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==}
+    dev: true
+
   /unicode-canonical-property-names-ecmascript@2.0.0:
     resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==}
     engines: {node: '>=4'}
@@ -13259,6 +13515,10 @@ packages:
     dependencies:
       punycode: 2.3.1
 
+  /url-join@4.0.1:
+    resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==}
+    dev: true
+
   /url-parse@1.5.10:
     resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
     dependencies:
@@ -13334,6 +13594,11 @@ packages:
     resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
     engines: {node: '>= 0.4.0'}
 
+  /uuid@8.3.2:
+    resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
+    hasBin: true
+    dev: true
+
   /v8flags@4.0.1:
     resolution: {integrity: sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg==}
     engines: {node: '>= 10.13.0'}
@@ -13346,6 +13611,13 @@ packages:
       spdx-expression-parse: 3.0.1
     dev: true
 
+  /validate-npm-package-name@4.0.0:
+    resolution: {integrity: sha512-mzR0L8ZDktZjpX4OB46KT+56MAhl4EIazWP/+G/HPGuvfdaqg4YsCdtOm6U9+LOFyYDoh4dpnpxZRB9MQQns5Q==}
+    engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
+    dependencies:
+      builtins: 5.1.0
+    dev: true
+
   /validator@13.12.0:
     resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==}
     engines: {node: '>= 0.10'}
@@ -13682,7 +13954,6 @@ packages:
 
   /webidl-conversions@3.0.1:
     resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
-    dev: false
 
   /webidl-conversions@7.0.0:
     resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
@@ -13763,7 +14034,6 @@ packages:
     dependencies:
       tr46: 0.0.3
       webidl-conversions: 3.0.1
-    dev: false
 
   /which-boxed-primitive@1.0.2:
     resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==}
@@ -13876,6 +14146,27 @@ packages:
   /wrappy@1.0.2:
     resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
 
+  /write-file-atomic@1.3.4:
+    resolution: {integrity: sha512-SdrHoC/yVBPpV0Xq/mUZQIpW2sWXAShb/V4pomcJXh92RuaO+f3UTWItiR3Px+pLnV2PvC2/bfn5cwr5X6Vfxw==}
+    dependencies:
+      graceful-fs: 4.2.11
+      imurmurhash: 0.1.4
+      slide: 1.1.6
+    dev: true
+
+  /ws@7.5.9:
+    resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==}
+    engines: {node: '>=8.3.0'}
+    peerDependencies:
+      bufferutil: ^4.0.1
+      utf-8-validate: ^5.0.2
+    peerDependenciesMeta:
+      bufferutil:
+        optional: true
+      utf-8-validate:
+        optional: true
+    dev: true
+
   /ws@8.16.0:
     resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==}
     engines: {node: '>=10.0.0'}


Test patch:
diff --git a/examples/react/start-basic/tests/app.spec.ts b/examples/react/start-basic/tests/app.spec.ts
new file mode 100644
index 000000000..2ff6d45b8
--- /dev/null
+++ b/examples/react/start-basic/tests/app.spec.ts
@@ -0,0 +1,25 @@
+import { test, expect } from '@playwright/test'
+
+test('Navigating to post', async ({ page }) => {
+  await page.goto('/')
+  await page.getByRole('link', { name: 'Posts' }).click()
+  await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+  await page.getByRole('link', { name: 'Deep View' }).click()
+  await expect(page.getByRole('heading')).toContainText('sunt aut facere')
+})
+
+test('Navigating nested layouts', async ({ page }) => {
+  await page.goto('/')
+  await page.getByRole('link', { name: 'Layout', exact: true }).click()
+  await page.getByRole('link', { name: 'Layout A' }).click()
+  await expect(page.locator('#root')).toContainText("I'm A!")
+  await page.getByRole('link', { name: 'Layout B' }).click()
+  await expect(page.locator('#root')).toContainText("I'm B!")
+})
+
+test('Navigating to a not-found route', async ({ page }) => {
+  await page.goto('/')
+  await page.getByRole('link', { name: 'This Route Does Not Exist' }).click()
+  await page.getByRole('link', { name: 'Start Over' }).click()
+  await expect(page.getByRole('heading')).toContainText('Welcome Home!')
+})
diff --git a/packages/react-router/tests/redirects.test.-d.tsx b/packages/react-router/tests/redirects.test-d.tsx
similarity index 100%
rename from packages/react-router/tests/redirects.test.-d.tsx
rename to packages/react-router/tests/redirects.test-d.tsx
diff --git a/packages/react-router/tests/useLoaderData.test-d.tsx b/packages/react-router/tests/useLoaderData.test-d.tsx
new file mode 100644
index 000000000..9c3cba78e
--- /dev/null
+++ b/packages/react-router/tests/useLoaderData.test-d.tsx
@@ -0,0 +1,293 @@
+import { expectTypeOf, test } from 'vitest'
+import {
+  createRootRoute,
+  createRoute,
+  createRouter,
+  useLoaderData,
+} from '../src'
+import { type MakeRouteMatch } from '../src/Matches'
+
+test('when there is no loaders', () => {
+  const rootRoute = createRootRoute()
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+  })
+
+  const invoicesRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: 'invoices',
+  })
+
+  const invoicesIndexRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '/',
+  })
+
+  const routeTree = rootRoute.addChildren([
+    invoicesRoute.addChildren([invoicesIndexRoute]),
+    indexRoute,
+  ])
+
+  const defaultRouter = createRouter({
+    routeTree,
+  })
+
+  type DefaultRouter = typeof defaultRouter
+
+  expectTypeOf(useLoaderData<DefaultRouter['routeTree']>)
+    .parameter(0)
+    .toHaveProperty('from')
+    .toEqualTypeOf<'/invoices' | '__root__' | '/invoices/' | '/'>()
+
+  expectTypeOf(useLoaderData<DefaultRouter['routeTree']>)
+    .parameter(0)
+    .toHaveProperty('strict')
+    .toEqualTypeOf<true | undefined>()
+
+  expectTypeOf(useLoaderData<DefaultRouter['routeTree'], '/'>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .parameter(0)
+    .toMatchTypeOf<{
+      loaderData?: {}
+    }>()
+
+  expectTypeOf(useLoaderData<DefaultRouter['routeTree'], '/'>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .returns.toEqualTypeOf<{}>()
+
+  expectTypeOf(
+    useLoaderData<DefaultRouter['routeTree'], '/'>,
+  ).returns.toEqualTypeOf<{}>()
+
+  expectTypeOf(
+    useLoaderData<DefaultRouter['routeTree'], '/', false>({ strict: false }),
+  ).toEqualTypeOf<{}>
+})
+
+test('when there is one loader', () => {
+  const rootRoute = createRootRoute()
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+  })
+
+  const invoicesRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: 'invoices',
+    loader: () => ({ data: ['element1', 'element2'] }),
+  })
+
+  const invoicesIndexRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '/',
+  })
+
+  const invoiceRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '$invoiceId',
+  })
+
+  const routeTree = rootRoute.addChildren([
+    invoicesRoute.addChildren([invoicesIndexRoute, invoiceRoute]),
+    indexRoute,
+  ])
+
+  const defaultRouter = createRouter({
+    routeTree,
+    context: { userId: 'userId' },
+  })
+
+  type DefaultRouter = typeof defaultRouter
+
+  expectTypeOf(
+    useLoaderData<DefaultRouter['routeTree'], '/invoices'>,
+  ).returns.toEqualTypeOf<{ data: Array<string> }>()
+
+  expectTypeOf(
+    useLoaderData<DefaultRouter['routeTree'], '/invoices'>,
+  ).returns.toEqualTypeOf<{ data: Array<string> }>()
+
+  expectTypeOf(useLoaderData<DefaultRouter['routeTree'], '/invoices'>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .returns.toEqualTypeOf<{ data: Array<string> }>()
+
+  expectTypeOf(
+    useLoaderData<DefaultRouter['routeTree'], '/invoices', false>,
+  ).returns.toEqualTypeOf<{ data?: Array<string> }>()
+
+  expectTypeOf(useLoaderData<DefaultRouter['routeTree'], '/invoices', false>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .returns.toEqualTypeOf<{ data?: Array<string> }>()
+
+  expectTypeOf(
+    useLoaderData<
+      DefaultRouter['routeTree'],
+      '/invoices',
+      false,
+      MakeRouteMatch<DefaultRouter['routeTree'], '/invoices', false>,
+      number
+    >,
+  ).returns.toEqualTypeOf<number>()
+})
+
+test('when there are multiple loaders', () => {
+  const rootRoute = createRootRoute()
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+  })
+
+  const invoicesRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: 'invoices',
+    loader: () => ({ data: ['invoice1', 'invoice2'] }) as const,
+  })
+
+  const invoicesIndexRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '/',
+  })
+
+  const postsRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: 'posts',
+    loader: () => ({ data: ['post1', 'post2'] }) as const,
+  })
+
+  const routeTree = rootRoute.addChildren([
+    invoicesRoute.addChildren([invoicesIndexRoute]),
+    postsRoute,
+    indexRoute,
+  ])
+
+  const defaultRouter = createRouter({
+    routeTree,
+  })
+
+  type DefaultRouter = typeof defaultRouter
+
+  expectTypeOf(
+    useLoaderData<DefaultRouter['routeTree'], '/'>,
+  ).returns.toEqualTypeOf<{}>()
+
+  expectTypeOf(
+    useLoaderData<DefaultRouter['routeTree'], '/invoices'>,
+  ).returns.toEqualTypeOf<{
+    readonly data: readonly ['invoice1', 'invoice2']
+  }>()
+
+  expectTypeOf(useLoaderData<DefaultRouter['routeTree'], '/invoices'>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .returns.toEqualTypeOf<{
+      readonly data: readonly ['invoice1', 'invoice2']
+    }>()
+
+  expectTypeOf(
+    useLoaderData<DefaultRouter['routeTree'], '/invoices', false>,
+  ).returns.toEqualTypeOf<{
+    data?:
+      | readonly ['invoice1', 'invoice2']
+      | readonly ['post1', 'post2']
+      | undefined
+  }>()
+
+  expectTypeOf(useLoaderData<DefaultRouter['routeTree'], '/invoices', false>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .returns.toEqualTypeOf<{
+      data?:
+        | readonly ['invoice1', 'invoice2']
+        | readonly ['post1', 'post2']
+        | undefined
+    }>()
+})
+
+test('when there are multiple loaders of objects and primtives', () => {
+  const rootRoute = createRootRoute()
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+  })
+
+  const invoicesRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: 'invoices',
+    loader: () => ['invoice1', 'invoice2'] as const,
+  })
+
+  const invoicesIndexRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '/',
+    loader: () => ({ invoice: { id: 1 } }) as const,
+  })
+
+  const postsRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: 'posts',
+    loader: () => ['post1', 'post2'] as const,
+  })
+
+  const routeTree = rootRoute.addChildren([
+    invoicesRoute.addChildren([invoicesIndexRoute]),
+    indexRoute,
+    postsRoute,
+  ])
+
+  const defaultRouter = createRouter({
+    routeTree,
+  })
+
+  type DefaultRouter = typeof defaultRouter
+
+  expectTypeOf(
+    useLoaderData<DefaultRouter['routeTree'], '/'>,
+  ).returns.toEqualTypeOf<{}>()
+
+  expectTypeOf(
+    useLoaderData<DefaultRouter['routeTree'], '/invoices'>,
+  ).returns.toEqualTypeOf<readonly ['invoice1', 'invoice2']>()
+
+  expectTypeOf(useLoaderData<DefaultRouter['routeTree'], '/invoices'>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .returns.toEqualTypeOf<readonly ['invoice1', 'invoice2']>()
+
+  expectTypeOf(
+    useLoaderData<DefaultRouter['routeTree'], '/invoices', false>,
+  ).returns.toEqualTypeOf<
+    | readonly ['invoice1', 'invoice2']
+    | readonly ['post1', 'post2']
+    | {
+        invoice?:
+          | {
+              readonly id: 1
+            }
+          | undefined
+      }
+  >()
+
+  expectTypeOf(useLoaderData<DefaultRouter['routeTree'], '/invoices', false>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .returns.toEqualTypeOf<
+      | readonly ['invoice1', 'invoice2']
+      | readonly ['post1', 'post2']
+      | {
+          invoice?:
+            | {
+                readonly id: 1
+              }
+            | undefined
+        }
+    >()
+})
diff --git a/packages/react-router/tests/useParams.test-d.tsx b/packages/react-router/tests/useParams.test-d.tsx
new file mode 100644
index 000000000..cf1e94080
--- /dev/null
+++ b/packages/react-router/tests/useParams.test-d.tsx
@@ -0,0 +1,217 @@
+import { expectTypeOf, test } from 'vitest'
+import {
+  type FullSearchSchema,
+  createRootRoute,
+  createRoute,
+  createRouter,
+  useParams,
+} from '../src'
+
+test('when there are no params', () => {
+  const rootRoute = createRootRoute()
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+  })
+
+  const invoicesRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: 'invoices',
+  })
+
+  const invoicesIndexRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '/',
+  })
+
+  const routeTree = rootRoute.addChildren([
+    invoicesRoute.addChildren([invoicesIndexRoute]),
+    indexRoute,
+  ])
+
+  const defaultRouter = createRouter({
+    routeTree,
+  })
+
+  type DefaultRouter = typeof defaultRouter
+
+  expectTypeOf(useParams<DefaultRouter['routeTree']>)
+    .parameter(0)
+    .toHaveProperty('from')
+    .toEqualTypeOf<'/invoices' | '__root__' | '/invoices/' | '/'>()
+
+  expectTypeOf(useParams<DefaultRouter['routeTree']>)
+    .parameter(0)
+    .toHaveProperty('strict')
+    .toEqualTypeOf<true | undefined>()
+
+  expectTypeOf(useParams<DefaultRouter['routeTree'], '/'>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .parameter(0)
+    .toEqualTypeOf<{}>()
+
+  expectTypeOf(useParams<DefaultRouter['routeTree'], '/'>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .returns.toEqualTypeOf<{}>()
+
+  expectTypeOf(
+    useParams<DefaultRouter['routeTree'], '/'>,
+  ).returns.toEqualTypeOf<{}>()
+
+  expectTypeOf(
+    useParams<DefaultRouter['routeTree'], '/', false>({ strict: false }),
+  ).toEqualTypeOf<{}>()
+})
+
+test('when there is one param', () => {
+  const rootRoute = createRootRoute()
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+  })
+
+  const invoicesRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: 'invoices',
+  })
+
+  const invoicesIndexRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '/',
+  })
+
+  const invoiceRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '$invoiceId',
+  })
+
+  const routeTree = rootRoute.addChildren([
+    invoicesRoute.addChildren([invoicesIndexRoute, invoiceRoute]),
+    indexRoute,
+  ])
+
+  const defaultRouter = createRouter({
+    routeTree,
+  })
+
+  type DefaultRouter = typeof defaultRouter
+
+  expectTypeOf(
+    useParams<DefaultRouter['routeTree'], '/'>,
+  ).returns.toEqualTypeOf<{}>()
+
+  expectTypeOf(
+    useParams<DefaultRouter['routeTree'], '/invoices/$invoiceId'>,
+  ).returns.toEqualTypeOf<{ invoiceId: string }>()
+
+  expectTypeOf(useParams<DefaultRouter['routeTree'], '/invoices/$invoiceId'>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .toEqualTypeOf<
+      ((search: { invoiceId: string }) => { invoiceId: string }) | undefined
+    >()
+
+  expectTypeOf(
+    useParams<DefaultRouter['routeTree'], '/invoices', false>,
+  ).returns.toEqualTypeOf<{ invoiceId?: string }>()
+
+  expectTypeOf(useParams<DefaultRouter['routeTree'], '/invoices', false>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .toEqualTypeOf<
+      ((search: { invoiceId?: string }) => { invoiceId?: string }) | undefined
+    >()
+
+  expectTypeOf(
+    useParams<
+      DefaultRouter['routeTree'],
+      '/invoices',
+      false,
+      FullSearchSchema<DefaultRouter['routeTree']>,
+      number
+    >,
+  ).returns.toEqualTypeOf<number>()
+})
+
+test('when there are multiple params', () => {
+  const rootRoute = createRootRoute()
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+  })
+
+  const invoicesRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: 'invoices',
+  })
+
+  const invoicesIndexRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '/',
+  })
+
+  const invoiceRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '$invoiceId',
+  })
+
+  const postsRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: 'posts',
+  })
+
+  const postRoute = createRoute({
+    getParentRoute: () => postsRoute,
+    path: '$postId',
+  })
+
+  const routeTree = rootRoute.addChildren([
+    invoicesRoute.addChildren([
+      invoicesIndexRoute,
+      invoiceRoute,
+      postsRoute.addChildren([postRoute]),
+    ]),
+    indexRoute,
+  ])
+
+  const defaultRouter = createRouter({
+    routeTree,
+  })
+
+  type DefaultRouter = typeof defaultRouter
+
+  expectTypeOf(
+    useParams<DefaultRouter['routeTree'], '/'>,
+  ).returns.toEqualTypeOf<{}>()
+
+  expectTypeOf(
+    useParams<DefaultRouter['routeTree'], '/invoices/$invoiceId'>,
+  ).returns.toEqualTypeOf<{ invoiceId: string }>()
+
+  expectTypeOf(useParams<DefaultRouter['routeTree'], '/invoices/$invoiceId'>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .toEqualTypeOf<
+      ((search: { invoiceId: string }) => { invoiceId: string }) | undefined
+    >()
+
+  expectTypeOf(
+    useParams<DefaultRouter['routeTree'], '/invoices', false>,
+  ).returns.toEqualTypeOf<{ invoiceId?: string; postId?: string }>()
+
+  expectTypeOf(useParams<DefaultRouter['routeTree'], '/invoices', false>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .toEqualTypeOf<
+      | ((search: { invoiceId?: string; postId?: string }) => {
+          invoiceId?: string
+          postId?: string
+        })
+      | undefined
+    >()
+})
diff --git a/packages/react-router/tests/useRouteContext.test-d.tsx b/packages/react-router/tests/useRouteContext.test-d.tsx
new file mode 100644
index 000000000..f29713def
--- /dev/null
+++ b/packages/react-router/tests/useRouteContext.test-d.tsx
@@ -0,0 +1,334 @@
+import { expectTypeOf, test } from 'vitest'
+import {
+  type FullSearchSchema,
+  createRootRoute,
+  createRootRouteWithContext,
+  createRoute,
+  createRouter,
+  useRouteContext,
+} from '../src'
+
+test('when there is no context', () => {
+  const rootRoute = createRootRoute()
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+  })
+
+  const invoicesRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: 'invoices',
+  })
+
+  const invoicesIndexRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '/',
+  })
+
+  const routeTree = rootRoute.addChildren([
+    invoicesRoute.addChildren([invoicesIndexRoute]),
+    indexRoute,
+  ])
+
+  const defaultRouter = createRouter({
+    routeTree,
+  })
+
+  type DefaultRouter = typeof defaultRouter
+
+  expectTypeOf(useRouteContext<DefaultRouter['routeTree']>)
+    .parameter(0)
+    .toHaveProperty('from')
+    .toEqualTypeOf<'/invoices' | '__root__' | '/invoices/' | '/'>()
+
+  expectTypeOf(useRouteContext<DefaultRouter['routeTree']>)
+    .parameter(0)
+    .toHaveProperty('strict')
+    .toEqualTypeOf<true | undefined>()
+
+  expectTypeOf(useRouteContext<DefaultRouter['routeTree'], '/'>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .parameter(0)
+    .toEqualTypeOf<{}>()
+
+  expectTypeOf(useRouteContext<DefaultRouter['routeTree'], '/'>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .returns.toEqualTypeOf<{}>()
+
+  expectTypeOf(
+    useRouteContext<DefaultRouter['routeTree'], '/'>,
+  ).returns.toEqualTypeOf<{}>()
+
+  expectTypeOf(
+    useRouteContext<DefaultRouter['routeTree'], '/', false>({ strict: false }),
+  ).toEqualTypeOf<{}>()
+})
+
+test('when there is the root context', () => {
+  interface Context {
+    userId: string
+  }
+
+  const rootRoute = createRootRouteWithContext<Context>()()
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+  })
+
+  const invoicesRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: 'invoices',
+  })
+
+  const invoicesIndexRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '/',
+  })
+
+  const invoiceRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '$invoiceId',
+  })
+
+  const routeTree = rootRoute.addChildren([
+    invoicesRoute.addChildren([invoicesIndexRoute, invoiceRoute]),
+    indexRoute,
+  ])
+
+  const defaultRouter = createRouter({
+    routeTree,
+    context: { userId: 'userId' },
+  })
+
+  type DefaultRouter = typeof defaultRouter
+
+  expectTypeOf(
+    useRouteContext<DefaultRouter['routeTree'], '/'>,
+  ).returns.toEqualTypeOf<{ userId: string }>()
+
+  expectTypeOf(
+    useRouteContext<DefaultRouter['routeTree'], '/invoices/$invoiceId'>,
+  ).returns.toEqualTypeOf<{ userId: string }>()
+
+  expectTypeOf(
+    useRouteContext<DefaultRouter['routeTree'], '/invoices/$invoiceId'>,
+  )
+    .parameter(0)
+    .toHaveProperty('select')
+    .toEqualTypeOf<
+      ((search: { userId: string }) => { userId: string }) | undefined
+    >()
+
+  expectTypeOf(
+    useRouteContext<DefaultRouter['routeTree'], '/invoices', false>,
+  ).returns.toEqualTypeOf<{ userId?: string }>()
+
+  expectTypeOf(useRouteContext<DefaultRouter['routeTree'], '/invoices', false>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .toEqualTypeOf<
+      ((search: { userId?: string }) => { userId?: string }) | undefined
+    >()
+
+  expectTypeOf(
+    useRouteContext<
+      DefaultRouter['routeTree'],
+      '/invoices',
+      false,
+      FullSearchSchema<DefaultRouter['routeTree']>,
+      number
+    >,
+  ).returns.toEqualTypeOf<number>()
+})
+
+test('when there are multiple contexts', () => {
+  interface Context {
+    userId: string
+  }
+
+  const rootRoute = createRootRouteWithContext<Context>()()
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+  })
+
+  const invoicesRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: 'invoices',
+  })
+
+  const invoicesIndexRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '/',
+  })
+
+  const invoiceRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '$invoiceId',
+  })
+
+  const postsRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: 'posts',
+  })
+
+  const postRoute = createRoute({
+    getParentRoute: () => postsRoute,
+    path: '$postId',
+    beforeLoad: () => ({ username: 'username' }),
+  })
+
+  const routeTree = rootRoute.addChildren([
+    invoicesRoute.addChildren([
+      invoicesIndexRoute,
+      invoiceRoute,
+      postsRoute.addChildren([postRoute]),
+    ]),
+    indexRoute,
+  ])
+
+  const defaultRouter = createRouter({
+    routeTree,
+    context: { userId: 'userId' },
+  })
+
+  type DefaultRouter = typeof defaultRouter
+
+  expectTypeOf(
+    useRouteContext<DefaultRouter['routeTree'], '/'>,
+  ).returns.toEqualTypeOf<{ userId: string }>()
+
+  expectTypeOf(
+    useRouteContext<DefaultRouter['routeTree'], '/invoices/$invoiceId'>,
+  ).returns.toEqualTypeOf<{ userId: string }>()
+
+  expectTypeOf(
+    useRouteContext<DefaultRouter['routeTree'], '/invoices/$invoiceId'>,
+  )
+    .parameter(0)
+    .toHaveProperty('select')
+    .toEqualTypeOf<
+      ((search: { userId: string }) => { userId: string }) | undefined
+    >()
+
+  expectTypeOf(
+    useRouteContext<DefaultRouter['routeTree'], '/invoices', false>,
+  ).returns.toEqualTypeOf<{ userId?: string; username?: string }>()
+
+  expectTypeOf(useRouteContext<DefaultRouter['routeTree'], '/invoices', false>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .toEqualTypeOf<
+      | ((search: { userId?: string; username?: string }) => {
+          userId?: string
+          username?: string
+        })
+      | undefined
+    >()
+})
+
+test('when there are overlapping contexts', () => {
+  interface Context {
+    userId: string
+  }
+
+  const rootRoute = createRootRouteWithContext<Context>()()
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+  })
+
+  const invoicesRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: 'invoices',
+    validateSearch: () => ({ page: 0 }),
+  })
+
+  const invoicesIndexRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '/',
+  })
+
+  const invoiceRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '$invoiceId',
+    beforeLoad: () => ({ username: 'username2' }) as const,
+  })
+
+  const postsRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: 'posts',
+  })
+
+  const postRoute = createRoute({
+    getParentRoute: () => postsRoute,
+    path: '$postId',
+    beforeLoad: () => ({ username: 'username1' }) as const,
+  })
+
+  const routeTree = rootRoute.addChildren([
+    invoicesRoute.addChildren([
+      invoicesIndexRoute,
+      invoiceRoute,
+      postsRoute.addChildren([postRoute]),
+    ]),
+    indexRoute,
+  ])
+
+  const defaultRouter = createRouter({
+    routeTree,
+    context: { userId: 'userId' },
+  })
+
+  type DefaultRouter = typeof defaultRouter
+
+  expectTypeOf(useRouteContext<DefaultRouter['routeTree'], '/'>).returns
+    .toEqualTypeOf<{ userId: string }>
+
+  expectTypeOf(
+    useRouteContext<DefaultRouter['routeTree'], '/invoices/$invoiceId'>,
+  ).returns.toEqualTypeOf<{
+    userId: string
+    readonly username: 'username2'
+  }>()
+
+  expectTypeOf(
+    useRouteContext<DefaultRouter['routeTree'], '/invoices/$invoiceId'>,
+  )
+    .parameter(0)
+    .toHaveProperty('select')
+    .toEqualTypeOf<
+      | ((search: { userId: string; readonly username: 'username2' }) => {
+          userId: string
+          readonly username: 'username2'
+        })
+      | undefined
+    >()
+
+  expectTypeOf(
+    useRouteContext<DefaultRouter['routeTree'], '/invoices', false>,
+  ).returns.toEqualTypeOf<{
+    userId?: string
+    username?: 'username1' | 'username2'
+  }>()
+
+  expectTypeOf(useRouteContext<DefaultRouter['routeTree'], '/invoices', false>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .toEqualTypeOf<
+      | ((search: {
+          userId?: string
+          username?: 'username2' | 'username1'
+        }) => {
+          userId?: string
+          username?: 'username2' | 'username1'
+        })
+      | undefined
+    >()
+})
diff --git a/packages/react-router/tests/useSearch.test-d.tsx b/packages/react-router/tests/useSearch.test-d.tsx
new file mode 100644
index 000000000..9687bfb5d
--- /dev/null
+++ b/packages/react-router/tests/useSearch.test-d.tsx
@@ -0,0 +1,281 @@
+import { expectTypeOf, test } from 'vitest'
+import {
+  type FullSearchSchema,
+  createRootRoute,
+  createRoute,
+  createRouter,
+  useSearch,
+} from '../src'
+
+test('when there are no search params', () => {
+  const rootRoute = createRootRoute()
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+  })
+
+  const invoicesRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: 'invoices',
+  })
+
+  const invoicesIndexRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '/',
+  })
+
+  const invoiceRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '$invoiceId',
+  })
+
+  const routeTree = rootRoute.addChildren([
+    invoicesRoute.addChildren([invoicesIndexRoute, invoiceRoute]),
+    indexRoute,
+  ])
+
+  const defaultRouter = createRouter({
+    routeTree,
+  })
+
+  type DefaultRouter = typeof defaultRouter
+
+  expectTypeOf(useSearch<DefaultRouter['routeTree']>)
+    .parameter(0)
+    .toHaveProperty('from')
+    .toEqualTypeOf<
+      '/invoices' | '__root__' | '/invoices/$invoiceId' | '/invoices/' | '/'
+    >()
+
+  expectTypeOf(useSearch<DefaultRouter['routeTree']>)
+    .parameter(0)
+    .toHaveProperty('strict')
+    .toEqualTypeOf<true | undefined>()
+
+  expectTypeOf(useSearch<DefaultRouter['routeTree'], '/'>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .parameter(0)
+    .toEqualTypeOf<{}>()
+
+  expectTypeOf(useSearch<DefaultRouter['routeTree'], '/'>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .returns.toEqualTypeOf<{}>()
+
+  expectTypeOf(
+    useSearch<DefaultRouter['routeTree'], '/'>,
+  ).returns.toEqualTypeOf<{}>()
+
+  expectTypeOf(
+    useSearch<DefaultRouter['routeTree'], '/', false>({ strict: false }),
+  ).toEqualTypeOf<{}>()
+})
+
+test('when there is one search params', () => {
+  const rootRoute = createRootRoute()
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+  })
+
+  const invoicesRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: 'invoices',
+    validateSearch: () => ({ page: 0 }),
+  })
+
+  const invoicesIndexRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '/',
+  })
+
+  const invoiceRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '$invoiceId',
+  })
+
+  const routeTree = rootRoute.addChildren([
+    invoicesRoute.addChildren([invoicesIndexRoute, invoiceRoute]),
+    indexRoute,
+  ])
+
+  const defaultRouter = createRouter({
+    routeTree,
+  })
+
+  type DefaultRouter = typeof defaultRouter
+
+  expectTypeOf(
+    useSearch<DefaultRouter['routeTree'], '/'>,
+  ).returns.toEqualTypeOf<{}>()
+
+  expectTypeOf(
+    useSearch<DefaultRouter['routeTree'], '/invoices'>,
+  ).returns.toEqualTypeOf<{ page: number }>()
+
+  expectTypeOf(useSearch<DefaultRouter['routeTree'], '/invoices'>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .toEqualTypeOf<
+      ((search: { page: number }) => { page: number }) | undefined
+    >()
+
+  expectTypeOf(
+    useSearch<DefaultRouter['routeTree'], '/invoices', false>,
+  ).returns.toEqualTypeOf<{ page?: number }>()
+
+  expectTypeOf(useSearch<DefaultRouter['routeTree'], '/invoices', false>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .toEqualTypeOf<
+      ((search: { page?: number }) => { page?: number }) | undefined
+    >()
+
+  expectTypeOf(
+    useSearch<
+      DefaultRouter['routeTree'],
+      '/invoices',
+      false,
+      FullSearchSchema<DefaultRouter['routeTree']>,
+      number
+    >,
+  ).returns.toEqualTypeOf<number>()
+})
+
+test('when there are multiple search params', () => {
+  const rootRoute = createRootRoute()
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+  })
+
+  const invoicesRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: 'invoices',
+    validateSearch: () => ({ page: 0 }),
+  })
+
+  const invoicesIndexRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '/',
+  })
+
+  const invoiceRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '$invoiceId',
+    validateSearch: () => ({ detail: 'detail' }),
+  })
+
+  const routeTree = rootRoute.addChildren([
+    invoicesRoute.addChildren([invoicesIndexRoute, invoiceRoute]),
+    indexRoute,
+  ])
+
+  const defaultRouter = createRouter({
+    routeTree,
+  })
+
+  type DefaultRouter = typeof defaultRouter
+
+  expectTypeOf(
+    useSearch<DefaultRouter['routeTree'], '/'>,
+  ).returns.toEqualTypeOf<{}>()
+
+  expectTypeOf(
+    useSearch<DefaultRouter['routeTree'], '/invoices'>,
+  ).returns.toEqualTypeOf<{ page: number }>()
+
+  expectTypeOf(useSearch<DefaultRouter['routeTree'], '/invoices'>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .toEqualTypeOf<
+      ((search: { page: number }) => { page: number }) | undefined
+    >()
+
+  expectTypeOf(
+    useSearch<DefaultRouter['routeTree'], '/invoices', false>,
+  ).returns.toEqualTypeOf<{ page?: number; detail?: string }>()
+
+  expectTypeOf(useSearch<DefaultRouter['routeTree'], '/invoices', false>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .toEqualTypeOf<
+      | ((search: { page?: number; detail?: string }) => {
+          page?: number
+          detail?: string
+        })
+      | undefined
+    >()
+})
+
+test('when there are overlapping search params', () => {
+  const rootRoute = createRootRoute()
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+  })
+
+  const invoicesRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: 'invoices',
+    validateSearch: () => ({ page: 0 }),
+  })
+
+  const invoicesIndexRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '/',
+    validateSearch: () => ({ detail: 50 }) as const,
+  })
+
+  const invoiceRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '$invoiceId',
+    validateSearch: () => ({ detail: 'detail' }) as const,
+  })
+
+  const routeTree = rootRoute.addChildren([
+    invoicesRoute.addChildren([invoicesIndexRoute, invoiceRoute]),
+    indexRoute,
+  ])
+
+  const defaultRouter = createRouter({
+    routeTree,
+  })
+
+  type DefaultRouter = typeof defaultRouter
+
+  expectTypeOf(
+    useSearch<DefaultRouter['routeTree'], '/'>,
+  ).returns.toEqualTypeOf<{}>()
+
+  expectTypeOf(
+    useSearch<DefaultRouter['routeTree'], '/invoices'>,
+  ).returns.toEqualTypeOf<{ page: number }>()
+
+  expectTypeOf(useSearch<DefaultRouter['routeTree'], '/invoices'>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .toEqualTypeOf<
+      ((search: { page: number }) => { page: number }) | undefined
+    >()
+
+  expectTypeOf(
+    useSearch<DefaultRouter['routeTree'], '/invoices', false>,
+  ).returns.toEqualTypeOf<{ page?: number; detail?: 'detail' | 50 }>()
+
+  expectTypeOf(useSearch<DefaultRouter['routeTree'], '/invoices', false>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .toEqualTypeOf<
+      | ((search: { page?: number; detail?: 'detail' | 50 }) => {
+          page?: number
+          detail?: 'detail' | 50
+        })
+      | undefined
+    >()
+})