Repository: TanStack/router
Pull request #2551 (2024-10-15 19:58:35)

feat(react-router): allow to selectively invalidate matches

nx-cloud[bot]: # ☁️ Nx Cloud Report

CI is running for commit 414f1109afcf613aeefecac6ab5206dc825eb533.

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


---

Sent with 💌 from [NxCloud.](https://nx.app?utm_source=pull-request&utm_medium=comment)
pkg-pr-new[bot]: 
[Open in Stackblitz](https://pkg.pr.new/template/f3786b3f-4ee9-4813-92b1-cc04541aa779)
<details><summary><b>More templates</b></summary><p>

- [tanstack-router-react-example-authenticated-routes](https://pkg.pr.new/template/9c01291d-e537-4d7a-8053-ba8cd4cc840f)
- [tanstack-router-react-example-basic](https://pkg.pr.new/template/ccf2f11f-af9d-4468-8ce0-3d0fde124f84)
- [tanstack-router-react-example-basic-default-search-params](https://pkg.pr.new/template/de73838f-252d-427f-89f5-1bfdae3be451)
- [tanstack-router-react-example-basic-file-based](https://pkg.pr.new/template/20acff18-b946-473a-b6d0-b75767f154fd)
- [tanstack-router-react-example-basic-file-based-codesplitting](https://pkg.pr.new/template/3011c9f6-0901-4e4c-96e2-4610de90c850)
- [tanstack-router-react-example-react-query](https://pkg.pr.new/template/4139f471-93cf-4371-b87a-bc2316fa41c6)
- [tanstack-router-react-example-basic-react-query-file-based](https://pkg.pr.new/template/78a68c50-4a33-4607-b96c-b5fd63580679)
- [tanstack-router-react-example-basic-ssr-file-based](https://pkg.pr.new/template/3a5087ff-45ce-4012-9598-fa7c502d569c)
- [tanstack-router-react-example-basic-ssr-streaming-file-based](https://pkg.pr.new/template/e0b11f7a-4e81-439a-9f80-d084a09b4ae0)
- [tanstack-router-react-example-basic-virtual-file-based](https://pkg.pr.new/template/70faa8f2-357a-4fad-b6a4-229a2e4d7145)
- [tanstack-router-react-example-basic-virtual-inside-file-based](https://pkg.pr.new/template/7a618aa1-30cf-4eb9-a223-dc6d579254e8)
- [tanstack-router-react-example-deferred-data](https://pkg.pr.new/template/2cfc8491-ad08-41e1-bf85-eb6fdc4cfe78)
- [tanstack-router-react-example-kitchen-sink](https://pkg.pr.new/template/2499fbe9-2ccb-4734-8ae1-bb1f7a7e0034)
- [tanstack-router-react-example-kitchen-sink-file-based](https://pkg.pr.new/template/a07fbd20-b1ac-4026-a375-d60fc42ce101)
- [tanstack-router-react-example-kitchen-sink-react-query](https://pkg.pr.new/template/3782399f-6f53-40ab-a4b4-8fd4021f7045)
- [tanstack-router-react-example-kitchen-sink-react-query-file-based](https://pkg.pr.new/template/4abeb0ee-a3b8-4402-9d23-3e7598953b94)
- [tanstack-router-react-example-large-file-based](https://pkg.pr.new/template/24528507-feed-44cc-92a1-f0d42848da46)
- [tanstack-router-react-example-location-masking](https://pkg.pr.new/template/8dcefcec-9f95-4124-a218-ddc93f6ca0cc)
- [tanstack-router-react-example-navigation-blocking](https://pkg.pr.new/template/f6692714-407f-423c-b442-80fe88e29436)
- [tanstack-router-react-example-quickstart](https://pkg.pr.new/template/206355ee-42f8-4dc9-a574-416f316dc57b)
- [tanstack-router-react-example-quickstart-file-based](https://pkg.pr.new/template/5fe34817-7f85-444a-bf7e-644240058caa)
- [tanstack-router-react-example-quickstart-rspack-file-based](https://pkg.pr.new/template/331273ec-102f-414d-8561-af78a30c8fe4)
- [tanstack-router-react-example-quickstart-webpack-file-based](https://pkg.pr.new/template/cae845b3-efb7-4bbf-95ed-d094879356c7)
- [tanstack-router-search-validator-adapters](https://pkg.pr.new/template/3a111997-6ee0-46c1-9ada-dd7f5c08363c)
- [tanstack-start-example-basic](https://pkg.pr.new/template/810d59f1-feb9-460a-bc90-497760e15617)
- [tanstack-start-example-basic-auth](https://pkg.pr.new/template/5b125dd7-a124-44a8-9c79-ab0ae85e72d2)
- [tanstack-start-example-basic-react-query](https://pkg.pr.new/template/4f492232-48e6-45cf-bb64-163a01c3cf19)
- [tanstack-start-example-basic-rsc](https://pkg.pr.new/template/b592ce09-ef6c-42b5-9a37-47026bf2ef93)
- [tanstack-start-example-clerk-basic](https://pkg.pr.new/template/415aed03-a47f-41d8-b79e-f3aa3f08df11)
- [tanstack-start-example-convex-trellaux](https://pkg.pr.new/template/6488bd47-76ff-4b2f-aa28-29202e03e592)
- [tanstack-start-example-counter](https://pkg.pr.new/template/92f2c1d8-0368-4572-9e27-0df88dd0a0d3)
- [tanstack-start-example-supabase-basic](https://pkg.pr.new/template/eea3be08-e133-40af-abd7-58d0f98a8ef1)
- [tanstack-router-react-example-with-framer-motion](https://pkg.pr.new/template/20dc907c-f28b-4f2c-9050-5619ad51e552)
- [tanstack-start-example-trellaux](https://pkg.pr.new/template/ca461d55-16a4-459f-9974-554505a10613)
- [tanstack-router-react-example-with-trpc](https://pkg.pr.new/template/c8e6819e-2ad2-4675-a4ee-b3cdf49311d5)
- [tanstack-router-react-example-with-trpc-react-query](https://pkg.pr.new/template/6dba1eae-2cb8-436d-95a2-860f237cf8d9)
- [tanstack-router-react-example-scroll-restoration](https://pkg.pr.new/template/c8886a06-1a38-4637-abbd-c71481a6405c)

</p></details>
      
    


<details><summary><b>@tanstack/create-router</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/create-router@2551
```

</p></details>
      
    

<details><summary><b>@tanstack/eslint-plugin-router</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/eslint-plugin-router@2551
```

</p></details>
      
    

<details><summary><b>@tanstack/react-cross-context</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/react-cross-context@2551
```

</p></details>
      
    

<details><summary><b>@tanstack/history</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/history@2551
```

</p></details>
      
    

<details><summary><b>@tanstack/react-router</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/react-router@2551
```

</p></details>
      
    

<details><summary><b>@tanstack/react-router-with-query</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/react-router-with-query@2551
```

</p></details>
      
    

<details><summary><b>@tanstack/router-arktype-adapter</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/router-arktype-adapter@2551
```

</p></details>
      
    

<details><summary><b>@tanstack/router-cli</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/router-cli@2551
```

</p></details>
      
    

<details><summary><b>@tanstack/router-devtools</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/router-devtools@2551
```

</p></details>
      
    

<details><summary><b>@tanstack/router-generator</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/router-generator@2551
```

</p></details>
      
    

<details><summary><b>@tanstack/router-plugin</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/router-plugin@2551
```

</p></details>
      
    

<details><summary><b>@tanstack/router-vite-plugin</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/router-vite-plugin@2551
```

</p></details>
      
    

<details><summary><b>@tanstack/router-valibot-adapter</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/router-valibot-adapter@2551
```

</p></details>
      
    

<details><summary><b>@tanstack/router-zod-adapter</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/router-zod-adapter@2551
```

</p></details>
      
    

<details><summary><b>@tanstack/start</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/start@2551
```

</p></details>
      
    

<details><summary><b>@tanstack/start-vite-plugin</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/start-vite-plugin@2551
```

</p></details>
      
    

<details><summary><b>@tanstack/virtual-file-routes</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/virtual-file-routes@2551
```

</p></details>
      
    

_commit: <a href="https://github.com/TanStack/router/runs/31578554215"><code>414f110</code></a>_


Patch:
diff --git a/docs/eslint/create-route-property-order.md b/docs/eslint/create-route-property-order.md
index 28d00fb67..56f27821f 100644
--- a/docs/eslint/create-route-property-order.md
+++ b/docs/eslint/create-route-property-order.md
@@ -10,14 +10,14 @@ For the following functions, the property order of the passed in object matters
 - `createRootRoute`
 - `createRootRouteWithContext`
 
-The correct property order is as follows:
+The correct property order is as follows
 
-- `params`
-- `validateSearch`
-- `loaderDeps`
+- `params`, `validateSearch`
+- `loaderDeps`, `search.middlewares`
 - `context`
 - `beforeLoad`
 - `loader`
+- `onEnter`, `onStay`, `onLeave`, `meta`, `links`, `scripts`, `headers`
 
 All other properties are insensitive to the order as they do not depend on type inference.
 
diff --git a/docs/framework/react/api/router/RouterOptionsType.md b/docs/framework/react/api/router/RouterOptionsType.md
index 2625db731..2b1a41f0e 100644
--- a/docs/framework/react/api/router/RouterOptionsType.md
+++ b/docs/framework/react/api/router/RouterOptionsType.md
@@ -35,6 +35,15 @@ The `RouterOptions` type accepts an object with the following properties and met
 - A function that will be used to parse search params when parsing the current location.
 - Defaults to `defaultParseSearch`.
 
+### `search.strict` property
+
+- Type: `boolean`
+- Optional
+- Defaults to `false`
+- Configures how unknown search params (= not returned by any `validateSearch`) are treated.
+- If `false`, unknown search params will be kept.
+- If `true`, unknown search params will be removed.
+
 ### `defaultPreload` property
 
 - Type: `undefined | false | 'intent' | 'viewport'`
diff --git a/docs/framework/react/api/router/RouterType.md b/docs/framework/react/api/router/RouterType.md
index 7b79451c6..8cc148557 100644
--- a/docs/framework/react/api/router/RouterType.md
+++ b/docs/framework/react/api/router/RouterType.md
@@ -129,10 +129,12 @@ Navigates to a new location.
 
 ### `.invalidate` method
 
-Invalidates all route matches by forcing their `beforeLoad` and `load` functions to be called again.
+Invalidates route matches by forcing their `beforeLoad` and `load` functions to be called again.
 
-- Type: `() => Promise<void>`
+- Type: `(opts?: {filter?: (d: MakeRouteMatch<TRouteTree>) => boolean}) => Promise<void>`
 - This is useful any time your loader data might be out of date or stale. For example, if you have a route that displays a list of posts, and you have a loader function that fetches the list of posts from an API, you might want to invalidate the route matches for that route any time a new post is created so that the list of posts is always up-to-date.
+- if `filter` is not supplied, all matches will be invalidated
+- if `filter` is supplied, only matches for which `filter` returns `true` will be invalidate.
 - You might also want to invalidate the Router if you imperatively `reset` the router's `CatchBoundary` to trigger loaders again.
 
 ### `.load` method
diff --git a/docs/framework/react/start/api-routes.md b/docs/framework/react/start/api-routes.md
index 7d298b6f2..3dd077af9 100644
--- a/docs/framework/react/start/api-routes.md
+++ b/docs/framework/react/start/api-routes.md
@@ -236,19 +236,19 @@ You can set the status code of the response by either:
   })
   ```
 
-- Using the `setStatus` helper function from `vinxi/http`
+- Using the `setResponseStatus` helper function from `vinxi/http`
 
   ```ts
   // routes/api/hello.ts
   import { json } from '@tanstack/start'
   import { createAPIFileRoute } from '@tanstack/start/api'
-  import { setStatus } from 'vinxi/http'
+  import { setResponseStatus } from 'vinxi/http'
 
   export const Route = createAPIFileRoute('/users/$id')({
     GET: async ({ request, params }) => {
       const user = await findUser(params.id)
       if (!user) {
-        setStatus(404)
+        setResponseStatus(404)
         return new Response('User not found')
       }
       return json(user)
diff --git a/docs/framework/react/start/server-functions.md b/docs/framework/react/start/server-functions.md
index 4a1d412b2..70ea8e62c 100644
--- a/docs/framework/react/start/server-functions.md
+++ b/docs/framework/react/start/server-functions.md
@@ -245,14 +245,14 @@ export const getServerTime = createServerFn('GET', async () => {
 
 ## Responding with Custom Status Codes
 
-To respond with a custom status code, you can use Vinxi's `setStatus` function:
+To respond with a custom status code, you can use Vinxi's `setResponseStatus` function:
 
 ```tsx
 import { createServerFn } from '@tanstack/start'
-import { setStatus } from 'vinxi/http'
+import { setResponseStatus } from 'vinxi/http'
 
 export const getServerTime = createServerFn('GET', async () => {
-  setStatus(201)
+  setResponseStatus(201)
   return new Date().toISOString()
 })
 ```
diff --git a/examples/react/basic-ssr-file-based/package.json b/examples/react/basic-ssr-file-based/package.json
index 0b9588ee7..9aca1a897 100644
--- a/examples/react/basic-ssr-file-based/package.json
+++ b/examples/react/basic-ssr-file-based/package.json
@@ -14,7 +14,7 @@
     "@tanstack/react-router": "^1.69.1",
     "@tanstack/router-devtools": "^1.69.1",
     "@tanstack/router-plugin": "^1.69.1",
-    "@tanstack/start": "^1.69.1",
+    "@tanstack/start": "^1.69.3",
     "get-port": "^7.1.0",
     "react": "^18.2.0",
     "react-dom": "^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 17e887bff..5df140122 100644
--- a/examples/react/basic-ssr-streaming-file-based/package.json
+++ b/examples/react/basic-ssr-streaming-file-based/package.json
@@ -14,7 +14,7 @@
     "@tanstack/react-router": "^1.69.1",
     "@tanstack/router-devtools": "^1.69.1",
     "@tanstack/router-plugin": "^1.69.1",
-    "@tanstack/start": "^1.69.1",
+    "@tanstack/start": "^1.69.3",
     "get-port": "^7.1.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
diff --git a/examples/react/start-basic-auth/package.json b/examples/react/start-basic-auth/package.json
index fe0bfb253..5d0c48cab 100644
--- a/examples/react/start-basic-auth/package.json
+++ b/examples/react/start-basic-auth/package.json
@@ -13,7 +13,7 @@
     "@prisma/client": "5.20.0",
     "@tanstack/react-router": "^1.69.1",
     "@tanstack/router-devtools": "^1.69.1",
-    "@tanstack/start": "^1.69.1",
+    "@tanstack/start": "^1.69.3",
     "dotenv": "^16.4.5",
     "prisma": "^5.20.0",
     "react": "^18.3.1",
diff --git a/examples/react/start-basic-react-query/package.json b/examples/react/start-basic-react-query/package.json
index ddecac112..546388f7b 100644
--- a/examples/react/start-basic-react-query/package.json
+++ b/examples/react/start-basic-react-query/package.json
@@ -14,7 +14,7 @@
     "@tanstack/react-router": "^1.69.1",
     "@tanstack/react-router-with-query": "^1.69.1",
     "@tanstack/router-devtools": "^1.69.1",
-    "@tanstack/start": "^1.69.1",
+    "@tanstack/start": "^1.69.3",
     "react": "^18.3.1",
     "react-dom": "^18.3.1",
     "redaxios": "^0.5.1",
diff --git a/examples/react/start-basic-rsc/package.json b/examples/react/start-basic-rsc/package.json
index eb8c1ba74..47cc5983d 100644
--- a/examples/react/start-basic-rsc/package.json
+++ b/examples/react/start-basic-rsc/package.json
@@ -12,7 +12,7 @@
     "@babel/plugin-syntax-typescript": "^7.25.7",
     "@tanstack/react-router": "^1.69.1",
     "@tanstack/router-devtools": "^1.69.1",
-    "@tanstack/start": "^1.69.1",
+    "@tanstack/start": "^1.69.3",
     "redaxios": "^0.5.1",
     "tailwind-merge": "^2.5.3",
     "vinxi": "0.4.3"
diff --git a/examples/react/start-basic/package.json b/examples/react/start-basic/package.json
index 5a46e0c69..d3c0bc2b8 100644
--- a/examples/react/start-basic/package.json
+++ b/examples/react/start-basic/package.json
@@ -11,7 +11,7 @@
   "dependencies": {
     "@tanstack/react-router": "^1.69.1",
     "@tanstack/router-devtools": "^1.69.1",
-    "@tanstack/start": "^1.69.1",
+    "@tanstack/start": "^1.69.3",
     "react": "^18.3.1",
     "react-dom": "^18.3.1",
     "redaxios": "^0.5.1",
diff --git a/examples/react/start-clerk-basic/package.json b/examples/react/start-clerk-basic/package.json
index a60bbf129..6c4bdeb6e 100644
--- a/examples/react/start-clerk-basic/package.json
+++ b/examples/react/start-clerk-basic/package.json
@@ -12,7 +12,7 @@
     "@clerk/tanstack-start": "0.4.12",
     "@tanstack/react-router": "^1.69.1",
     "@tanstack/router-devtools": "^1.69.1",
-    "@tanstack/start": "^1.69.1",
+    "@tanstack/start": "^1.69.3",
     "dotenv": "^16.4.5",
     "react": "^18.3.1",
     "react-dom": "^18.3.1",
diff --git a/examples/react/start-convex-trellaux/package.json b/examples/react/start-convex-trellaux/package.json
index a14b3b5ec..9ea7d8aba 100644
--- a/examples/react/start-convex-trellaux/package.json
+++ b/examples/react/start-convex-trellaux/package.json
@@ -16,7 +16,7 @@
     "@tanstack/react-router": "^1.69.1",
     "@tanstack/react-router-with-query": "^1.69.1",
     "@tanstack/router-devtools": "^1.69.1",
-    "@tanstack/start": "^1.69.1",
+    "@tanstack/start": "^1.69.3",
     "@convex-dev/react-query": "0.0.0-alpha.5",
     "concurrently": "^8.2.2",
     "convex": "^1.16.5",
diff --git a/examples/react/start-counter/package.json b/examples/react/start-counter/package.json
index fda774ba7..e733c489e 100644
--- a/examples/react/start-counter/package.json
+++ b/examples/react/start-counter/package.json
@@ -10,7 +10,7 @@
   },
   "dependencies": {
     "@tanstack/react-router": "^1.69.1",
-    "@tanstack/start": "^1.69.1",
+    "@tanstack/start": "^1.69.3",
     "react": "^18.3.1",
     "react-dom": "^18.3.1",
     "vinxi": "0.4.3"
diff --git a/examples/react/start-supabase-basic/package.json b/examples/react/start-supabase-basic/package.json
index b7ac17d48..6e277fba5 100644
--- a/examples/react/start-supabase-basic/package.json
+++ b/examples/react/start-supabase-basic/package.json
@@ -17,7 +17,7 @@
     "@supabase/supabase-js": "^2.45.4",
     "@tanstack/react-router": "^1.69.1",
     "@tanstack/router-devtools": "^1.69.1",
-    "@tanstack/start": "^1.69.1",
+    "@tanstack/start": "^1.69.3",
     "@vitejs/plugin-react": "^4.3.2",
     "react": "^18.3.1",
     "react-dom": "^18.3.1",
diff --git a/examples/react/start-trellaux/package.json b/examples/react/start-trellaux/package.json
index 26d9b54ca..497205f15 100644
--- a/examples/react/start-trellaux/package.json
+++ b/examples/react/start-trellaux/package.json
@@ -14,7 +14,7 @@
     "@tanstack/react-router": "^1.69.1",
     "@tanstack/react-router-with-query": "^1.69.1",
     "@tanstack/router-devtools": "^1.69.1",
-    "@tanstack/start": "^1.69.1",
+    "@tanstack/start": "^1.69.3",
     "ky": "^1.7.2",
     "msw": "^2.4.11",
     "react": "^18.3.1",
diff --git a/packages/eslint-plugin-router/src/rules/create-route-property-order/constants.ts b/packages/eslint-plugin-router/src/rules/create-route-property-order/constants.ts
index 74885cd10..1bcd28257 100644
--- a/packages/eslint-plugin-router/src/rules/create-route-property-order/constants.ts
+++ b/packages/eslint-plugin-router/src/rules/create-route-property-order/constants.ts
@@ -1,3 +1,5 @@
+import { getCheckedProperties } from './create-route-property-order.utils'
+
 export const createRouteFunctionsIndirect = [
   'createFileRoute',
   'createRootRouteWithContext',
@@ -14,18 +16,17 @@ export const createRouteFunctions = [
 
 export type CreateRouteFunction = (typeof createRouteFunctions)[number]
 
-export const checkedProperties = [
-  'params',
-  'validateSearch',
-  'loaderDeps',
-  'context',
-  'beforeLoad',
-  'loader',
-] as const
-
 export const sortRules = [
-  [['params', 'validateSearch'], ['loaderDeps']],
+  [['params', 'validateSearch'], ['search']],
+  [['search'], ['loaderDeps']],
   [['loaderDeps'], ['context']],
   [['context'], ['beforeLoad']],
   [['beforeLoad'], ['loader']],
+  [
+    ['loader'],
+    ['onEnter', 'onStay', 'onLeave', 'meta', 'links', 'scripts', 'headers'],
+  ],
 ] as const
+
+export type CheckedProperties = (typeof sortRules)[number][number][number]
+export const checkedProperties = getCheckedProperties(sortRules)
diff --git a/packages/eslint-plugin-router/src/rules/create-route-property-order/create-route-property-order.utils.ts b/packages/eslint-plugin-router/src/rules/create-route-property-order/create-route-property-order.utils.ts
index 390c703e8..87891e7ab 100644
--- a/packages/eslint-plugin-router/src/rules/create-route-property-order/create-route-property-order.utils.ts
+++ b/packages/eslint-plugin-router/src/rules/create-route-property-order/create-route-property-order.utils.ts
@@ -65,3 +65,9 @@ export function sortDataByOrder<T, TKey extends keyof T>(
   }
   return result
 }
+
+export function getCheckedProperties<T>(
+  orderRules: ReadonlyArray<ReadonlyArray<ReadonlyArray<T>>>,
+): ReadonlyArray<T> {
+  return [...new Set<T>(orderRules.flat(2))]
+}
diff --git a/packages/react-router/src/Matches.tsx b/packages/react-router/src/Matches.tsx
index 8736c0244..36dc9ae25 100644
--- a/packages/react-router/src/Matches.tsx
+++ b/packages/react-router/src/Matches.tsx
@@ -142,7 +142,6 @@ export interface RouteMatch<
   paramsError: unknown
   searchError: unknown
   updatedAt: number
-  componentsPromise?: Promise<Array<void>>
   loadPromise?: ControlledPromise<void>
   beforeLoadPromise?: ControlledPromise<void>
   loaderPromise?: ControlledPromise<void>
diff --git a/packages/react-router/src/Transitioner.tsx b/packages/react-router/src/Transitioner.tsx
index 88189592b..f3d3d3f4e 100644
--- a/packages/react-router/src/Transitioner.tsx
+++ b/packages/react-router/src/Transitioner.tsx
@@ -39,6 +39,7 @@ export function Transitioner() {
       params: true,
       hash: true,
       state: true,
+      _includeValidateSearch: true,
     })
 
     if (
diff --git a/packages/react-router/src/route.ts b/packages/react-router/src/route.ts
index 4129444c2..60ca66c61 100644
--- a/packages/react-router/src/route.ts
+++ b/packages/react-router/src/route.ts
@@ -945,6 +945,7 @@ export class Route<
   rank!: number
   lazyFn?: () => Promise<LazyRoute<any>>
   _lazyPromise?: Promise<void>
+  _componentsPromise?: Promise<Array<void>>
 
   /**
    * @deprecated Use the `createRoute` function instead.
diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts
index 09450a689..66e12a7ca 100644
--- a/packages/react-router/src/router.ts
+++ b/packages/react-router/src/router.ts
@@ -447,11 +447,21 @@ export interface RouterOptions<
    * While usually automatic, sometimes it can be useful to force the router into a server-side state, e.g. when using the router in a non-browser environment that has access to a global.document object.
    *
    * @default typeof document !== 'undefined'
-   * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#isserver property)
+   * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#isserver-property)
    */
   isServer?: boolean
 
   defaultSsr?: boolean
+
+  search?: {
+    /**
+     * Configures how unknown search params (= not returned by any `validateSearch`) are treated.
+     *
+     * @default false
+     * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#search.strict-property)
+     */
+    strict?: boolean
+  }
 }
 
 export interface RouterErrorSerializer<TSerializedError> {
@@ -1511,6 +1521,7 @@ export class Router<
       let search = applyMiddlewares()
 
       if (opts._includeValidateSearch) {
+        let validatedSearch = this.options.search?.strict ? {} : search
         matchedRoutesResult?.matchedRoutes.forEach((route) => {
           try {
             if (route.options.validateSearch) {
@@ -1518,12 +1529,16 @@ export class Router<
                 typeof route.options.validateSearch === 'object'
                   ? route.options.validateSearch.parse
                   : route.options.validateSearch
-              search = { ...search, ...validator(search) }
+              validatedSearch = {
+                ...validatedSearch,
+                ...validator({ ...validatedSearch, ...search }),
+              }
             }
           } catch (e) {
             // ignore errors here because they are already handled in matchRoutes
           }
         })
+        search = validatedSearch
       }
       search = replaceEqualDeep(fromSearch, search)
       const searchStr = this.options.stringifySearch(search)
@@ -2336,37 +2351,41 @@ export class Router<
 
                         // Actually run the loader and handle the result
                         try {
-                          route._lazyPromise =
-                            route._lazyPromise ||
-                            (route.lazyFn
-                              ? route.lazyFn().then((lazyRoute) => {
-                                  const { id, ...options } = lazyRoute.options
+                          if (route._lazyPromise === undefined) {
+                            if (route.lazyFn) {
+                              route._lazyPromise = route
+                                .lazyFn()
+                                .then((lazyRoute) => {
+                                  // explicitly don't copy over the lazy route's id
+                                  const { id: _id, ...options } =
+                                    lazyRoute.options
                                   Object.assign(route.options, options)
                                 })
-                              : Promise.resolve())
+                            } else {
+                              route._lazyPromise = Promise.resolve()
+                            }
+                          }
 
                           // If for some reason lazy resolves more lazy components...
                           // We'll wait for that before pre attempt to preload any
                           // components themselves.
-                          const componentsPromise =
-                            this.getMatch(matchId)!.componentsPromise ||
-                            route._lazyPromise.then(() =>
-                              Promise.all(
-                                componentTypes.map(async (type) => {
-                                  const component = route.options[type]
-
-                                  if ((component as any)?.preload) {
-                                    await (component as any).preload()
-                                  }
-                                }),
-                              ),
+                          if (route._componentsPromise === undefined) {
+                            route._componentsPromise = route._lazyPromise.then(
+                              () =>
+                                Promise.all(
+                                  componentTypes.map(async (type) => {
+                                    const component = route.options[type]
+                                    if ((component as any)?.preload) {
+                                      await (component as any).preload()
+                                    }
+                                  }),
+                                ),
                             )
+                          }
 
-                          // Otherwise, load the route
                           updateMatch(matchId, (prev) => ({
                             ...prev,
                             isFetching: 'loader',
-                            componentsPromise,
                           }))
 
                           // Kick off the loader!
@@ -2442,9 +2461,9 @@ export class Router<
                           }))
                         }
 
-                        // Last but not least, wait for the the component
+                        // Last but not least, wait for the the components
                         // to be preloaded before we resolve the match
-                        await this.getMatch(matchId)!.componentsPromise
+                        await route._componentsPromise
                       } catch (err) {
                         handleRedirectAndNotFound(this.getMatch(matchId)!, err)
                       }
@@ -2505,14 +2524,21 @@ export class Router<
     return matches
   }
 
-  invalidate = () => {
-    const invalidate = (d: MakeRouteMatch<TRouteTree>) => ({
-      ...d,
-      invalid: true,
-      ...(d.status === 'error'
-        ? ({ status: 'pending', error: undefined } as const)
-        : {}),
-    })
+  invalidate = (opts?: {
+    filter?: (d: MakeRouteMatch<TRouteTree>) => boolean
+  }) => {
+    const invalidate = (d: MakeRouteMatch<TRouteTree>) => {
+      if (opts?.filter?.(d) ?? true) {
+        return {
+          ...d,
+          invalid: true,
+          ...(d.status === 'error'
+            ? ({ status: 'pending', error: undefined } as const)
+            : {}),
+        }
+      }
+      return d
+    }
 
     this.__store.setState((s) => ({
       ...s,
diff --git a/packages/start/package.json b/packages/start/package.json
index 1a01883ef..977494809 100644
--- a/packages/start/package.json
+++ b/packages/start/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@tanstack/start",
-  "version": "1.69.1",
+  "version": "1.69.3",
   "description": "Modern and scalable routing for React applications",
   "author": "Tanner Linsley",
   "license": "MIT",
diff --git a/packages/start/src/server-handler/index.tsx b/packages/start/src/server-handler/index.tsx
index 955f2738c..17b585d06 100644
--- a/packages/start/src/server-handler/index.tsx
+++ b/packages/start/src/server-handler/index.tsx
@@ -5,7 +5,12 @@ import {
   isRedirect,
 } from '@tanstack/react-router'
 import invariant from 'tiny-invariant'
-import { eventHandler, toWebRequest } from 'vinxi/http'
+import {
+  eventHandler,
+  getEvent,
+  getResponseStatus,
+  toWebRequest,
+} from 'vinxi/http'
 import { getManifest } from 'vinxi/manifest'
 import {
   serverFnPayloadTypeHeader,
@@ -117,7 +122,7 @@ export async function handleServerRequest(request: Request, event?: H3Event) {
       return new Response(
         result !== undefined ? JSON.stringify(result) : undefined,
         {
-          status: 200,
+          status: getResponseStatus(getEvent()),
           headers: {
             'Content-Type': 'application/json',
             [serverFnReturnTypeHeader]: 'json',
@@ -143,7 +148,7 @@ export async function handleServerRequest(request: Request, event?: H3Event) {
       console.info()
 
       return new Response(JSON.stringify(error), {
-        status: 500,
+        status: getResponseStatus(getEvent()),
         headers: {
           'Content-Type': 'application/json',
           [serverFnReturnTypeHeader]: 'error',
@@ -155,10 +160,7 @@ export async function handleServerRequest(request: Request, event?: H3Event) {
   if (process.env.NODE_ENV === 'development')
     console.info(`ServerFn Response: ${response.status}`)
 
-  if (
-    response.status === 200 &&
-    response.headers.get('Content-Type') === 'application/json'
-  ) {
+  if (response.headers.get('Content-Type') === 'application/json') {
     const cloned = response.clone()
     const text = await cloned.text()
     const payload = text ? JSON.stringify(JSON.parse(text)) : 'undefined'


Test patch:
diff --git a/e2e/start/basic/app/routeTree.gen.ts b/e2e/start/basic/app/routeTree.gen.ts
index cebc80abe..41fd6a65f 100644
--- a/e2e/start/basic/app/routeTree.gen.ts
+++ b/e2e/start/basic/app/routeTree.gen.ts
@@ -12,6 +12,7 @@
 
 import { Route as rootRoute } from './routes/__root'
 import { Route as UsersImport } from './routes/users'
+import { Route as StatusImport } from './routes/status'
 import { Route as SearchParamsImport } from './routes/search-params'
 import { Route as RedirectImport } from './routes/redirect'
 import { Route as PostsImport } from './routes/posts'
@@ -34,6 +35,11 @@ const UsersRoute = UsersImport.update({
   getParentRoute: () => rootRoute,
 } as any)
 
+const StatusRoute = StatusImport.update({
+  path: '/status',
+  getParentRoute: () => rootRoute,
+} as any)
+
 const SearchParamsRoute = SearchParamsImport.update({
   path: '/search-params',
   getParentRoute: () => rootRoute,
@@ -150,6 +156,13 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof SearchParamsImport
       parentRoute: typeof rootRoute
     }
+    '/status': {
+      id: '/status'
+      path: '/status'
+      fullPath: '/status'
+      preLoaderRoute: typeof StatusImport
+      parentRoute: typeof rootRoute
+    }
     '/users': {
       id: '/users'
       path: '/users'
@@ -274,6 +287,7 @@ export interface FileRoutesByFullPath {
   '/posts': typeof PostsRouteWithChildren
   '/redirect': typeof RedirectRoute
   '/search-params': typeof SearchParamsRoute
+  '/status': typeof StatusRoute
   '/users': typeof UsersRouteWithChildren
   '/posts/$postId': typeof PostsPostIdRoute
   '/users/$userId': typeof UsersUserIdRoute
@@ -290,6 +304,7 @@ export interface FileRoutesByTo {
   '/deferred': typeof DeferredRoute
   '/redirect': typeof RedirectRoute
   '/search-params': typeof SearchParamsRoute
+  '/status': typeof StatusRoute
   '/posts/$postId': typeof PostsPostIdRoute
   '/users/$userId': typeof UsersUserIdRoute
   '/posts': typeof PostsIndexRoute
@@ -307,6 +322,7 @@ export interface FileRoutesById {
   '/posts': typeof PostsRouteWithChildren
   '/redirect': typeof RedirectRoute
   '/search-params': typeof SearchParamsRoute
+  '/status': typeof StatusRoute
   '/users': typeof UsersRouteWithChildren
   '/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
   '/posts/$postId': typeof PostsPostIdRoute
@@ -327,6 +343,7 @@ export interface FileRouteTypes {
     | '/posts'
     | '/redirect'
     | '/search-params'
+    | '/status'
     | '/users'
     | '/posts/$postId'
     | '/users/$userId'
@@ -342,6 +359,7 @@ export interface FileRouteTypes {
     | '/deferred'
     | '/redirect'
     | '/search-params'
+    | '/status'
     | '/posts/$postId'
     | '/users/$userId'
     | '/posts'
@@ -357,6 +375,7 @@ export interface FileRouteTypes {
     | '/posts'
     | '/redirect'
     | '/search-params'
+    | '/status'
     | '/users'
     | '/_layout/_layout-2'
     | '/posts/$postId'
@@ -376,6 +395,7 @@ export interface RootRouteChildren {
   PostsRoute: typeof PostsRouteWithChildren
   RedirectRoute: typeof RedirectRoute
   SearchParamsRoute: typeof SearchParamsRoute
+  StatusRoute: typeof StatusRoute
   UsersRoute: typeof UsersRouteWithChildren
   PostsPostIdDeepRoute: typeof PostsPostIdDeepRoute
 }
@@ -387,6 +407,7 @@ const rootRouteChildren: RootRouteChildren = {
   PostsRoute: PostsRouteWithChildren,
   RedirectRoute: RedirectRoute,
   SearchParamsRoute: SearchParamsRoute,
+  StatusRoute: StatusRoute,
   UsersRoute: UsersRouteWithChildren,
   PostsPostIdDeepRoute: PostsPostIdDeepRoute,
 }
@@ -409,6 +430,7 @@ export const routeTree = rootRoute
         "/posts",
         "/redirect",
         "/search-params",
+        "/status",
         "/users",
         "/posts/$postId/deep"
       ]
@@ -438,6 +460,9 @@ export const routeTree = rootRoute
     "/search-params": {
       "filePath": "search-params.tsx"
     },
+    "/status": {
+      "filePath": "status.tsx"
+    },
     "/users": {
       "filePath": "users.tsx",
       "children": [
diff --git a/e2e/start/basic/app/routes/status.tsx b/e2e/start/basic/app/routes/status.tsx
new file mode 100644
index 000000000..bd8ec12f3
--- /dev/null
+++ b/e2e/start/basic/app/routes/status.tsx
@@ -0,0 +1,30 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { createServerFn, useServerFn } from '@tanstack/start'
+
+import { setResponseStatus } from 'vinxi/http'
+
+export const helloFn = createServerFn('GET', async () => {
+  setResponseStatus(225, `hello`)
+  return {
+    hello: 'world',
+  }
+})
+export const Route = createFileRoute('/status')({
+  component: StatusComponent,
+})
+
+function StatusComponent() {
+  const hello = useServerFn(helloFn)
+
+  return (
+    <div className="p-2">
+      <button
+        data-testid="invoke-server-fn"
+        className="px-4 py-2 bg-blue-500 text-white rounded-md"
+        onClick={() => hello()}
+      >
+        click me
+      </button>
+    </div>
+  )
+}
diff --git a/e2e/start/basic/tests/app.spec.ts b/e2e/start/basic/tests/app.spec.ts
index 225205b4b..8303f3cf5 100644
--- a/e2e/start/basic/tests/app.spec.ts
+++ b/e2e/start/basic/tests/app.spec.ts
@@ -78,3 +78,23 @@ test('Directly visiting the search-params route with search param set', async ({
   await expect(page.getByTestId('search-param')).toContainText('b')
   expect(page.url().endsWith('/search-params?step=b'))
 })
+
+test('invoking a server function with custom response status code', async ({
+  page,
+}) => {
+  await page.goto('/status')
+
+  await page.waitForLoadState('networkidle')
+  await page.getByTestId('invoke-server-fn').click()
+
+  const requestPromise = new Promise<void>((resolve) => {
+    page.on('response', async (response) => {
+      expect(response.status()).toBe(225)
+      expect(response.statusText()).toBe('hello')
+      expect(response.headers()['content-type']).toBe('application/json')
+      expect(await response.json()).toEqual({ hello: 'world' })
+      resolve()
+    })
+  })
+  await requestPromise
+})
diff --git a/packages/eslint-plugin-router/src/__tests__/create-route-property-order.rule.test.ts b/packages/eslint-plugin-router/src/__tests__/create-route-property-order.rule.test.ts
index 51a0743f4..227530ad2 100644
--- a/packages/eslint-plugin-router/src/__tests__/create-route-property-order.rule.test.ts
+++ b/packages/eslint-plugin-router/src/__tests__/create-route-property-order.rule.test.ts
@@ -21,10 +21,10 @@ const ruleTester = new RuleTester()
 
 // reduce the number of test cases by only testing a subset of the checked properties
 const testedCheckedProperties = [
-  checkedProperties[0],
-  checkedProperties[1],
-  checkedProperties[2],
-  checkedProperties[3],
+  checkedProperties[0]!,
+  checkedProperties[1]!,
+  checkedProperties[2]!,
+  checkedProperties[3]!,
 ]
 type TestedCheckedProperties = (typeof testedCheckedProperties)[number]
 const orderIndependentProps = ['gcTime', '...foo'] as const
@@ -165,6 +165,19 @@ const validTestCases = validTestMatrix.map(
   }),
 )
 
+invalidTestMatrix.push({
+  createRouteFunction: 'createFileRoute',
+  properties: {
+    invalid: ['loader', 'loaderDeps'],
+    valid: ['loaderDeps', 'loader'],
+  },
+})
+
+invalidTestMatrix.push({
+  createRouteFunction: 'createFileRoute',
+  properties: { invalid: ['meta', 'loader'], valid: ['loader', 'meta'] },
+})
+
 const invalidTestCases = invalidTestMatrix.map(
   ({ createRouteFunction, properties }) => ({
     name: `incorrect property order is detected for ${createRouteFunction} with order: ${properties.invalid.join(', ')}`,
diff --git a/packages/eslint-plugin-router/src/__tests__/create-route-property-order.utils.test.ts b/packages/eslint-plugin-router/src/__tests__/create-route-property-order.utils.test.ts
index b9edd6420..4dfb782e6 100644
--- a/packages/eslint-plugin-router/src/__tests__/create-route-property-order.utils.test.ts
+++ b/packages/eslint-plugin-router/src/__tests__/create-route-property-order.utils.test.ts
@@ -1,5 +1,8 @@
 import { describe, expect, test } from 'vitest'
-import { sortDataByOrder } from '../rules/create-route-property-order/create-route-property-order.utils'
+import {
+  getCheckedProperties,
+  sortDataByOrder,
+} from '../rules/create-route-property-order/create-route-property-order.utils'
 
 describe('create-route-property-order utils', () => {
   describe('sortDataByOrder', () => {
@@ -67,6 +70,63 @@ describe('create-route-property-order utils', () => {
         key: 'key',
         expected: [{ key: 'd' }, { key: 'a' }, { key: 'b' }, { key: 'c' }],
       },
+      {
+        data: [{ key: 'd' }, { key: 'b' }, { key: 'a' }, { key: 'c' }],
+        orderArray: [
+          [['a', 'b'], ['d']],
+          [['d'], ['c']],
+        ],
+        key: 'key',
+        expected: [{ key: 'b' }, { key: 'a' }, { key: 'd' }, { key: 'c' }],
+      },
+      {
+        data: [
+          { key: 'd' },
+          { key: 'b' },
+          { key: 'a' },
+          { key: 'c' },
+          { key: 'f' },
+        ],
+        orderArray: [
+          [
+            ['a', 'b'],
+            ['d', 'f'],
+          ],
+          [['d'], ['c']],
+        ],
+        key: 'key',
+        expected: [
+          { key: 'b' },
+          { key: 'a' },
+          { key: 'd' },
+          { key: 'f' },
+          { key: 'c' },
+        ],
+      },
+      {
+        data: [
+          { key: 'd' },
+          { key: 'b' },
+          { key: 'a' },
+          { key: 'c' },
+          { key: 'f' },
+        ],
+        orderArray: [
+          [
+            ['a', 'b'],
+            ['d', 'f'],
+          ],
+          [['d', 'f'], ['c']],
+        ],
+        key: 'key',
+        expected: [
+          { key: 'b' },
+          { key: 'a' },
+          { key: 'd' },
+          { key: 'f' },
+          { key: 'c' },
+        ],
+      },
     ] as const
     test.each(testCases)(
       '$data $orderArray $key $expected',
@@ -77,3 +137,43 @@ describe('create-route-property-order utils', () => {
     )
   })
 })
+
+describe('getCheckedProperties', () => {
+  const testCases = [
+    {
+      orderRules: [
+        [['a', 'b'], ['c']],
+        [['c'], ['d']],
+      ],
+      expected: ['a', 'b', 'c', 'd'],
+    },
+    {
+      orderRules: [
+        [['a', 'b'], ['c']],
+        [['d'], ['e']],
+      ],
+      expected: ['a', 'b', 'c', 'd', 'e'],
+    },
+    {
+      orderRules: [
+        [['a', 'b'], ['c']],
+        [['d'], ['e']],
+        [['c'], ['f']],
+      ],
+      expected: ['a', 'b', 'c', 'd', 'e', 'f'],
+    },
+    {
+      orderRules: [
+        [['a', 'b'], ['c']],
+        [['d'], ['e']],
+        [['c'], ['f']],
+        [['f'], ['g']],
+      ],
+      expected: ['a', 'b', 'c', 'd', 'e', 'f', 'g'],
+    },
+  ] as const
+  test.each(testCases)('$orderRules $expected', ({ orderRules, expected }) => {
+    const checkedProperties = getCheckedProperties(orderRules)
+    expect(checkedProperties).toEqual(expected)
+  })
+})
diff --git a/packages/react-router/tests/router.test.tsx b/packages/react-router/tests/router.test.tsx
index d9fe6585c..25e669663 100644
--- a/packages/react-router/tests/router.test.tsx
+++ b/packages/react-router/tests/router.test.tsx
@@ -7,6 +7,7 @@ import {
   screen,
   waitFor,
 } from '@testing-library/react'
+import { z } from 'zod'
 import {
   Link,
   Outlet,
@@ -16,7 +17,7 @@ import {
   createRoute,
   createRouter,
 } from '../src'
-import type { RouterHistory } from '../src'
+import type { AnyRoute, RouterOptions } from '../src'
 
 afterEach(() => {
   vi.resetAllMocks()
@@ -26,14 +27,19 @@ afterEach(() => {
 
 const mockFn1 = vi.fn()
 
-function createTestRouter(initialHistory?: RouterHistory) {
-  let history
-
-  if (initialHistory) {
-    history = initialHistory
-  }
-
-  const rootRoute = createRootRoute({})
+function createTestRouter(options?: RouterOptions<AnyRoute, 'never'>) {
+  const rootRoute = createRootRoute({
+    validateSearch: z.object({ root: z.string().optional() }),
+    component: () => {
+      const search = rootRoute.useSearch()
+      return (
+        <>
+          <div data-testid="search-root">{search.root ?? '$undefined'}</div>
+          <Outlet />
+        </>
+      )
+    },
+  })
   const indexRoute = createRoute({ getParentRoute: () => rootRoute, path: '/' })
   const postsRoute = createRoute({
     getParentRoute: () => rootRoute,
@@ -121,6 +127,19 @@ function createTestRouter(initialHistory?: RouterHistory) {
       f.FileRoute('/_protected/_fileBasedLayout/fileBasedParent/child'),
     ),
   )
+  const searchRoute = createRoute({
+    validateSearch: z.object({ search: z.string().optional() }),
+    getParentRoute: () => rootRoute,
+    path: 'search',
+    component: () => {
+      const search = searchRoute.useSearch()
+      return (
+        <>
+          <div data-testid="search-search">{search.search ?? '$undefined'}</div>
+        </>
+      )
+    },
+  })
 
   const routeTree = rootRoute.addChildren([
     indexRoute,
@@ -145,9 +164,10 @@ function createTestRouter(initialHistory?: RouterHistory) {
         ]),
       ]),
     ]),
+    searchRoute,
   ])
 
-  const router = createRouter({ routeTree, history })
+  const router = createRouter({ routeTree, ...options })
 
   return {
     router,
@@ -164,9 +184,9 @@ function createTestRouter(initialHistory?: RouterHistory) {
 
 describe('encoding: URL param segment for /posts/$slug', () => {
   it('state.location.pathname, should have the params.slug value of "tanner"', async () => {
-    const { router } = createTestRouter(
-      createMemoryHistory({ initialEntries: ['/posts/tanner'] }),
-    )
+    const { router } = createTestRouter({
+      history: createMemoryHistory({ initialEntries: ['/posts/tanner'] }),
+    })
 
     await act(() => router.load())
 
@@ -174,9 +194,9 @@ describe('encoding: URL param segment for /posts/$slug', () => {
   })
 
   it('state.location.pathname, should have the params.slug value of "🚀"', async () => {
-    const { router } = createTestRouter(
-      createMemoryHistory({ initialEntries: ['/posts/🚀'] }),
-    )
+    const { router } = createTestRouter({
+      history: createMemoryHistory({ initialEntries: ['/posts/🚀'] }),
+    })
 
     await act(() => router.load())
 
@@ -184,9 +204,9 @@ describe('encoding: URL param segment for /posts/$slug', () => {
   })
 
   it('state.location.pathname, should have the params.slug value of "%F0%9F%9A%80"', async () => {
-    const { router } = createTestRouter(
-      createMemoryHistory({ initialEntries: ['/posts/%F0%9F%9A%80'] }),
-    )
+    const { router } = createTestRouter({
+      history: createMemoryHistory({ initialEntries: ['/posts/%F0%9F%9A%80'] }),
+    })
 
     await act(() => router.load())
 
@@ -194,13 +214,13 @@ describe('encoding: URL param segment for /posts/$slug', () => {
   })
 
   it('state.location.pathname, should have the params.slug value of "framework%2Freact%2Fguide%2Ffile-based-routing%20tanstack"', async () => {
-    const { router } = createTestRouter(
-      createMemoryHistory({
+    const { router } = createTestRouter({
+      history: createMemoryHistory({
         initialEntries: [
           '/posts/framework%2Freact%2Fguide%2Ffile-based-routing%20tanstack',
         ],
       }),
-    )
+    })
 
     await act(() => router.load())
 
@@ -210,9 +230,9 @@ describe('encoding: URL param segment for /posts/$slug', () => {
   })
 
   it('params.slug for the matched route, should be "tanner"', async () => {
-    const { router, routes } = createTestRouter(
-      createMemoryHistory({ initialEntries: ['/posts/tanner'] }),
-    )
+    const { router, routes } = createTestRouter({
+      history: createMemoryHistory({ initialEntries: ['/posts/tanner'] }),
+    })
 
     await act(() => router.load())
 
@@ -228,9 +248,9 @@ describe('encoding: URL param segment for /posts/$slug', () => {
   })
 
   it('params.slug for the matched route, should be "🚀"', async () => {
-    const { router, routes } = createTestRouter(
-      createMemoryHistory({ initialEntries: ['/posts/🚀'] }),
-    )
+    const { router, routes } = createTestRouter({
+      history: createMemoryHistory({ initialEntries: ['/posts/🚀'] }),
+    })
 
     await act(() => router.load())
 
@@ -246,9 +266,9 @@ describe('encoding: URL param segment for /posts/$slug', () => {
   })
 
   it('params.slug for the matched route, should be "🚀" instead of it being "%F0%9F%9A%80"', async () => {
-    const { router, routes } = createTestRouter(
-      createMemoryHistory({ initialEntries: ['/posts/%F0%9F%9A%80'] }),
-    )
+    const { router, routes } = createTestRouter({
+      history: createMemoryHistory({ initialEntries: ['/posts/%F0%9F%9A%80'] }),
+    })
 
     await act(() => router.load())
 
@@ -264,13 +284,13 @@ describe('encoding: URL param segment for /posts/$slug', () => {
   })
 
   it('params.slug for the matched route, should be "framework/react/guide/file-based-routing tanstack" instead of it being "framework%2Freact%2Fguide%2Ffile-based-routing%20tanstack"', async () => {
-    const { router, routes } = createTestRouter(
-      createMemoryHistory({
+    const { router, routes } = createTestRouter({
+      history: createMemoryHistory({
         initialEntries: [
           '/posts/framework%2Freact%2Fguide%2Ffile-based-routing%20tanstack',
         ],
       }),
-    )
+    })
 
     await act(() => router.load())
 
@@ -290,9 +310,9 @@ describe('encoding: URL param segment for /posts/$slug', () => {
 
 describe('encoding: URL splat segment for /$', () => {
   it('state.location.pathname, should have the params._splat value of "tanner"', async () => {
-    const { router } = createTestRouter(
-      createMemoryHistory({ initialEntries: ['/tanner'] }),
-    )
+    const { router } = createTestRouter({
+      history: createMemoryHistory({ initialEntries: ['/tanner'] }),
+    })
 
     await router.load()
 
@@ -300,9 +320,9 @@ describe('encoding: URL splat segment for /$', () => {
   })
 
   it('state.location.pathname, should have the params._splat value of "🚀"', async () => {
-    const { router } = createTestRouter(
-      createMemoryHistory({ initialEntries: ['/🚀'] }),
-    )
+    const { router } = createTestRouter({
+      history: createMemoryHistory({ initialEntries: ['/🚀'] }),
+    })
 
     await router.load()
 
@@ -310,9 +330,9 @@ describe('encoding: URL splat segment for /$', () => {
   })
 
   it('state.location.pathname, should have the params._splat value of "%F0%9F%9A%80"', async () => {
-    const { router } = createTestRouter(
-      createMemoryHistory({ initialEntries: ['/%F0%9F%9A%80'] }),
-    )
+    const { router } = createTestRouter({
+      history: createMemoryHistory({ initialEntries: ['/%F0%9F%9A%80'] }),
+    })
 
     await router.load()
 
@@ -320,13 +340,13 @@ describe('encoding: URL splat segment for /$', () => {
   })
 
   it('state.location.pathname, should have the params._splat value of "framework%2Freact%2Fguide%2Ffile-based-routing%20tanstack"', async () => {
-    const { router } = createTestRouter(
-      createMemoryHistory({
+    const { router } = createTestRouter({
+      history: createMemoryHistory({
         initialEntries: [
           '/framework%2Freact%2Fguide%2Ffile-based-routing%20tanstack',
         ],
       }),
-    )
+    })
 
     await router.load()
 
@@ -336,11 +356,11 @@ describe('encoding: URL splat segment for /$', () => {
   })
 
   it('state.location.pathname, should have the params._splat value of "framework/react/guide/file-based-routing tanstack"', async () => {
-    const { router } = createTestRouter(
-      createMemoryHistory({
+    const { router } = createTestRouter({
+      history: createMemoryHistory({
         initialEntries: ['/framework/react/guide/file-based-routing tanstack'],
       }),
-    )
+    })
 
     await router.load()
 
@@ -350,9 +370,9 @@ describe('encoding: URL splat segment for /$', () => {
   })
 
   it('params._splat for the matched route, should be "tanner"', async () => {
-    const { router, routes } = createTestRouter(
-      createMemoryHistory({ initialEntries: ['/tanner'] }),
-    )
+    const { router, routes } = createTestRouter({
+      history: createMemoryHistory({ initialEntries: ['/tanner'] }),
+    })
 
     await router.load()
 
@@ -368,9 +388,9 @@ describe('encoding: URL splat segment for /$', () => {
   })
 
   it('params._splat for the matched route, should be "🚀"', async () => {
-    const { router, routes } = createTestRouter(
-      createMemoryHistory({ initialEntries: ['/🚀'] }),
-    )
+    const { router, routes } = createTestRouter({
+      history: createMemoryHistory({ initialEntries: ['/🚀'] }),
+    })
 
     await router.load()
 
@@ -386,9 +406,9 @@ describe('encoding: URL splat segment for /$', () => {
   })
 
   it('params._splat for the matched route, should be "🚀" instead of it being "%F0%9F%9A%80"', async () => {
-    const { router, routes } = createTestRouter(
-      createMemoryHistory({ initialEntries: ['/%F0%9F%9A%80'] }),
-    )
+    const { router, routes } = createTestRouter({
+      history: createMemoryHistory({ initialEntries: ['/%F0%9F%9A%80'] }),
+    })
 
     await router.load()
 
@@ -404,11 +424,11 @@ describe('encoding: URL splat segment for /$', () => {
   })
 
   it('params._splat for the matched route, should be "framework/react/guide/file-based-routing tanstack"', async () => {
-    const { router, routes } = createTestRouter(
-      createMemoryHistory({
+    const { router, routes } = createTestRouter({
+      history: createMemoryHistory({
         initialEntries: ['/framework/react/guide/file-based-routing tanstack'],
       }),
-    )
+    })
 
     await router.load()
 
@@ -461,9 +481,9 @@ describe('encoding: URL path segment', () => {
   ])(
     'should resolve $input to $output when the path segment is $type',
     async ({ input, output }) => {
-      const { router } = createTestRouter(
-        createMemoryHistory({ initialEntries: [input] }),
-      )
+      const { router } = createTestRouter({
+        history: createMemoryHistory({ initialEntries: [input] }),
+      })
 
       render(<RouterProvider router={router} />)
       await act(() => router.load())
@@ -475,9 +495,9 @@ describe('encoding: URL path segment', () => {
 
 describe('router emits events during rendering', () => {
   it('during initial load, should emit the "onResolved" event', async () => {
-    const { router } = createTestRouter(
-      createMemoryHistory({ initialEntries: ['/'] }),
-    )
+    const { router } = createTestRouter({
+      history: createMemoryHistory({ initialEntries: ['/'] }),
+    })
 
     const unsub = router.subscribe('onResolved', mockFn1)
     await router.load()
@@ -488,9 +508,9 @@ describe('router emits events during rendering', () => {
   })
 
   it('after a navigation, should have emitted the "onResolved" event twice', async () => {
-    const { router } = createTestRouter(
-      createMemoryHistory({ initialEntries: ['/'] }),
-    )
+    const { router } = createTestRouter({
+      history: createMemoryHistory({ initialEntries: ['/'] }),
+    })
 
     const unsub = router.subscribe('onResolved', mockFn1)
     await router.load()
@@ -587,9 +607,9 @@ describe('transformer functions are defined', () => {
 
 describe('router matches URLs to route definitions', () => {
   it('solo splat route matches index route', async () => {
-    const { router } = createTestRouter(
-      createMemoryHistory({ initialEntries: ['/solo-splat'] }),
-    )
+    const { router } = createTestRouter({
+      history: createMemoryHistory({ initialEntries: ['/solo-splat'] }),
+    })
 
     await act(() => router.load())
 
@@ -600,9 +620,9 @@ describe('router matches URLs to route definitions', () => {
   })
 
   it('solo splat route matches with splat', async () => {
-    const { router } = createTestRouter(
-      createMemoryHistory({ initialEntries: ['/solo-splat/test'] }),
-    )
+    const { router } = createTestRouter({
+      history: createMemoryHistory({ initialEntries: ['/solo-splat/test'] }),
+    })
 
     await act(() => router.load())
 
@@ -613,9 +633,9 @@ describe('router matches URLs to route definitions', () => {
   })
 
   it('layout splat route matches with splat', async () => {
-    const { router } = createTestRouter(
-      createMemoryHistory({ initialEntries: ['/layout-splat/test'] }),
-    )
+    const { router } = createTestRouter({
+      history: createMemoryHistory({ initialEntries: ['/layout-splat/test'] }),
+    })
 
     await act(() => router.load())
 
@@ -627,9 +647,9 @@ describe('router matches URLs to route definitions', () => {
   })
 
   it('layout splat route matches without splat', async () => {
-    const { router } = createTestRouter(
-      createMemoryHistory({ initialEntries: ['/layout-splat'] }),
-    )
+    const { router } = createTestRouter({
+      history: createMemoryHistory({ initialEntries: ['/layout-splat'] }),
+    })
 
     await act(() => router.load())
 
@@ -643,9 +663,107 @@ describe('router matches URLs to route definitions', () => {
 
 describe('invalidate', () => {
   it('after router.invalid(), routes should be `valid` again after loading', async () => {
-    const { router } = createTestRouter(
-      createMemoryHistory({ initialEntries: ['/'] }),
-    )
+    const { router } = createTestRouter({
+      history: createMemoryHistory({ initialEntries: ['/'] }),
+    })
+
+    await act(() => router.load())
+
+    router.state.matches.forEach((match) => {
+      expect(match.invalid).toBe(false)
+    })
+
+    await act(() => router.invalidate())
+
+    router.state.matches.forEach((match) => {
+      expect(match.invalid).toBe(false)
+    })
+  })
+})
+
+describe('search params in URL', () => {
+  const testCases = [
+    { route: '/', search: { root: 'world' } },
+    { route: '/', search: { root: 'world', unknown: 'asdf' } },
+    { route: '/search', search: { search: 'foo' } },
+    { route: '/search', search: { root: 'world', search: 'foo' } },
+    {
+      route: '/search',
+      search: { root: 'world', search: 'foo', unknown: 'asdf' },
+    },
+  ]
+  describe.each([undefined, false])(
+    'does not modify the search params in the URL when search.strict=%s',
+    (strict) => {
+      it.each(testCases)(
+        'at $route with search params $search',
+        async ({ route, search }) => {
+          const { router } = createTestRouter({ search: { strict } })
+          window.history.replaceState(
+            null,
+            '',
+            `${route}?${new URLSearchParams(search as Record<string, string>).toString()}`,
+          )
+
+          render(<RouterProvider router={router} />)
+          await act(() => router.load())
+
+          expect(await screen.findByTestId('search-root')).toHaveTextContent(
+            search.root ?? '$undefined',
+          )
+          if (route === '/search') {
+            expect(
+              await screen.findByTestId('search-search'),
+            ).toHaveTextContent(search.search ?? '$undefined')
+          }
+          expect(router.state.location.search).toEqual(search)
+          const parsedSearch = new URLSearchParams(window.location.search)
+          expect(parsedSearch.size).toBe(Object.keys(search).length)
+          for (const [key, value] of Object.entries(search)) {
+            expect(parsedSearch.get(key)).toBe(value)
+          }
+        },
+      )
+    },
+  )
+
+  describe('removes unknown search params in the URL when search.strict=true', () => {
+    it.each(testCases)('%j', async ({ route, search }) => {
+      const { router } = createTestRouter({ search: { strict: true } })
+      window.history.replaceState(
+        null,
+        '',
+        `${route}?${new URLSearchParams(search as Record<string, string>).toString()}`,
+      )
+      render(<RouterProvider router={router} />)
+      await act(() => router.load())
+      await expect(await screen.findByTestId('search-root')).toHaveTextContent(
+        search.root ?? 'undefined',
+      )
+      if (route === '/search') {
+        expect(await screen.findByTestId('search-search')).toHaveTextContent(
+          search.search ?? 'undefined',
+        )
+      }
+
+      expect(window.location.pathname).toEqual(route)
+      const expectedSearch = { ...search }
+      delete expectedSearch.unknown
+      expect(router.state.location.search).toEqual(expectedSearch)
+      const parsedSearch = new URLSearchParams(window.location.search)
+      expect(parsedSearch.size).toBe(Object.keys(expectedSearch).length)
+      for (const [key, value] of Object.entries(expectedSearch)) {
+        expect(parsedSearch.get(key)).toBe(value)
+      }
+    })
+  })
+})
+
+/*
+  it('updates search params in URL upon initial load according to the result of validateSearch', async () => {
+    const { router } = createTestRouter({
+      history: createMemoryHistory({ initialEntries: ['/'] }),
+    })
 
     await act(() => router.load())
 
@@ -666,12 +784,13 @@ describe('invalidate', () => {
     })
   })
 })
+  */
 
 describe('route ids should be consistent after rebuilding the route tree', () => {
   it('should have the same route ids after rebuilding the route tree', async () => {
-    const { router } = createTestRouter(
-      createMemoryHistory({ initialEntries: ['/'] }),
-    )
+    const { router } = createTestRouter({
+      history: createMemoryHistory({ initialEntries: ['/'] }),
+    })
 
     const originalRouteIds = Object.keys(router.routesById)
 
@@ -703,9 +822,9 @@ describe('route ids should be consistent after rebuilding the route tree', () =>
 
 describe('route id uniqueness', () => {
   it('flatRoute should not have routes with duplicated route ids', () => {
-    const { router } = createTestRouter(
-      createMemoryHistory({ initialEntries: ['/'] }),
-    )
+    const { router } = createTestRouter({
+      history: createMemoryHistory({ initialEntries: ['/'] }),
+    })
     const routeIdSet = new Set<string>()
 
     router.flatRoutes.forEach((route) => {
@@ -715,9 +834,9 @@ describe('route id uniqueness', () => {
   })
 
   it('routesById should not have routes duplicated route ids', () => {
-    const { router } = createTestRouter(
-      createMemoryHistory({ initialEntries: ['/'] }),
-    )
+    const { router } = createTestRouter({
+      history: createMemoryHistory({ initialEntries: ['/'] }),
+    })
 
     const routeIdSet = new Set<string>()
 
@@ -728,9 +847,9 @@ describe('route id uniqueness', () => {
   })
 
   it('routesByPath should not have routes duplicated route ids', () => {
-    const { router } = createTestRouter(
-      createMemoryHistory({ initialEntries: ['/'] }),
-    )
+    const { router } = createTestRouter({
+      history: createMemoryHistory({ initialEntries: ['/'] }),
+    })
 
     const routeIdSet = new Set<string>()
 
diff --git a/packages/react-router/tests/useNavigate.test.tsx b/packages/react-router/tests/useNavigate.test.tsx
index c00b108ea..06d8c196f 100644
--- a/packages/react-router/tests/useNavigate.test.tsx
+++ b/packages/react-router/tests/useNavigate.test.tsx
@@ -1238,9 +1238,12 @@ test('when setting search params with 2 parallel navigate calls', async () => {
 
   const IndexComponent = () => {
     const navigate = useNavigate()
+    const search = indexRoute.useSearch()
     return (
       <React.Fragment>
         <h1>Index</h1>
+        <div data-testid="param1">{search.param1}</div>
+        <div data-testid="param2">{search.param2}</div>
         <button
           onClick={() => {
             navigate({
@@ -1284,5 +1287,10 @@ test('when setting search params with 2 parallel navigate calls', async () => {
 
   fireEvent.click(postsButton)
 
+  expect(await screen.findByTestId('param1')).toHaveTextContent('foo')
+  expect(await screen.findByTestId('param2')).toHaveTextContent('bar')
   expect(router.state.location.search).toEqual({ param1: 'foo', param2: 'bar' })
+  const search = new URLSearchParams(window.location.search)
+  expect(search.get('param1')).toEqual('foo')
+  expect(search.get('param2')).toEqual('bar')
 })