Repository: TanStack/router (ts)

Issue:
Blank screen / loads indefinitely when using validateSearch and loader/beforeLoad on __root
### Describe the bug

A bug occurs when using the `validateSearch `method with a custom schema created with `zod `in the latest version of `tanstack/router`. Specifically, the bug manifests when the route is accessed without search parameters, causing the page to load indefinitely. However, if the URL includes the search parameters, the route works as expected. This issue arises only when `loader `or `beforeLoad `is used on the `__root.tsx`.

It seems things were working correctly on version `1.29.2`, but I couldn't find where the regression occurs.

### Your Example Website or App

https://stackblitz.com/edit/tanstack-router-svgwoy?file=src%2Froutes%2F__root.tsx

### Steps to Reproduce the Bug or Issue


1. Setup a custom schema with zod and parse it on the `validateSearch`
```tsx
// routes/about.tsx
export const aboutSearchSchema = z.object({
  data: z.string().array().default([]),
});

export type AboutSearch = z.infer<typeof aboutSearchSchema>;
export type AboutSearchInput = z.input<typeof aboutSearchSchema>;

export const Route = createFileRoute('/about')({
  validateSearch: (search: AboutSearchInput & SearchSchemaInput): AboutSearch =>
    aboutSearchSchema.parse(search),
  component: AboutComponent,
});
```
2. Set up the root route with loader or beforeLoad:
```tsx
// routes/__root.tsx
export const Route = createRootRoute({
  component: RootComponent,
  // This cause the bug with validateSearch
  // loader: () => {
  beforeLoad: () => {
    console.log('beforeload called');
  },
});
```
3. Open the `/about` route in a new tab without any search parameters. The page goes blank and loads indefinitely.

4. Open the `/about?data=%5B%5D` route. The page loads as expected.

### Expected behavior

The route should load correctly regardless of whether search parameters are included in the URL.

**Actual Behavior:**

- Without search parameters (`/about`), the page goes blank and loads indefinitely.
- With search parameters (`/about?data=%5B%5D`), the page loads correctly.

### Screenshots or Videos

_No response_

### Platform

- OS: Windows / Linux
- Browser: Chrome / Firefox
- Version: 1.45.11


### Additional context

_No response_
Using  special characters in URLs causes 100% CPU usage and causes pages to fail to load
### Describe the bug

When upgrading to the latest version the Tanstack router an issue was discovered where by the browser would max out the CPU when special characters are used in the URL. This is only an issue when the URL is accessed directly not through client side routing and a `beforeLoad` method is provided to the root route.

### Your Example Website or App

https://stackblitz.com/edit/vitejs-vite-ymskas

### Steps to Reproduce the Bug or Issue

Goto https://vitejsviteymskas-ogdh-iqwsx4wk--5173--41692973.local-credentialless.webcontainer.io/
Click on ΚΑΠΠΑ
The CPU goes to 100% usage

### Expected behavior

As a user I expect the page to load and the post id to be displayed

### Screenshots or Videos

_No response_

### Platform

- OS: [e.g. macOS, Windows, Linux]
- Browser: [e.g. Chrome, Safari, Firefox]
- Version: [e.g. 91.1]


### Additional context

_No response_

Pull request description:
fix(react-router): update match in effect instead of during render
fixes #2031
fixes #2053

Patch:
diff --git a/packages/react-router/src/Match.tsx b/packages/react-router/src/Match.tsx
index 449617426..54b117108 100644
--- a/packages/react-router/src/Match.tsx
+++ b/packages/react-router/src/Match.tsx
@@ -141,6 +141,45 @@ export const MatchInner = React.memo(function MatchInnerImpl({
     return Comp ? <Comp key={matchId} /> : <Outlet />
   }, [matchId, route.options.component, router.options.defaultComponent])
 
+  React.useEffect(() => {
+    if (match.status === 'pending') {
+      // We're pending, and if we have a minPendingMs, we need to wait for it
+      const pendingMinMs =
+        route.options.pendingMinMs ?? router.options.defaultPendingMinMs
+
+      if (pendingMinMs && !match.minPendingPromise) {
+        // Create a promise that will resolve after the minPendingMs
+        if (!router.isServer) {
+          const minPendingPromise = createControlledPromise<void>()
+
+          router.updateMatch(match.id, (prev) => ({
+            ...prev,
+            minPendingPromise,
+          }))
+
+          const id = setTimeout(() => {
+            minPendingPromise.resolve()
+
+            // We've handled the minPendingPromise, so we can delete it
+            router.updateMatch(match.id, (prev) => ({
+              ...prev,
+              minPendingPromise: undefined,
+            }))
+          }, pendingMinMs)
+          return () => clearTimeout(id)
+        }
+      }
+    }
+    return undefined
+  }, [
+    match.id,
+    match.loadPromise,
+    match.minPendingPromise,
+    match.status,
+    route.options.pendingMinMs,
+    router,
+  ])
+
   // function useChangedDiff(value: any) {
   //   const ref = React.useRef(value)
   //   const changed = ref.current !== value
@@ -219,34 +258,6 @@ export const MatchInner = React.memo(function MatchInnerImpl({
   }
 
   if (match.status === 'pending') {
-    // We're pending, and if we have a minPendingMs, we need to wait for it
-    const pendingMinMs =
-      route.options.pendingMinMs ?? router.options.defaultPendingMinMs
-
-    if (pendingMinMs && !match.minPendingPromise) {
-      // Create a promise that will resolve after the minPendingMs
-      if (!router.isServer) {
-        const minPendingPromise = createControlledPromise<void>()
-
-        Promise.resolve().then(() => {
-          router.updateMatch(match.id, (prev) => ({
-            ...prev,
-            minPendingPromise,
-          }))
-        })
-
-        setTimeout(() => {
-          minPendingPromise.resolve()
-
-          // We've handled the minPendingPromise, so we can delete it
-          router.updateMatch(match.id, (prev) => ({
-            ...prev,
-            minPendingPromise: undefined,
-          }))
-        }, pendingMinMs)
-      }
-    }
-
     throw match.loadPromise
   }
 


Test patch:
diff --git a/packages/react-router/tests/loaders.test.tsx b/packages/react-router/tests/loaders.test.tsx
index b6cb486fd..277dc9094 100644
--- a/packages/react-router/tests/loaders.test.tsx
+++ b/packages/react-router/tests/loaders.test.tsx
@@ -8,6 +8,7 @@ import {
 
 import { afterEach, describe, expect, test, vi } from 'vitest'
 
+import { z } from 'zod'
 import {
   Link,
   Outlet,
@@ -165,3 +166,61 @@ describe('loaders parentMatchPromise', () => {
     expect(nestedLoaderMock.mock.calls[0][0]).toBeInstanceOf(Promise)
   })
 })
+
+test('reproducer for #2031', async () => {
+  const rootRoute = createRootRoute({
+    beforeLoad: () => {
+      console.log('beforeload called')
+    },
+  })
+
+  const searchSchema = z.object({
+    data: z.string().array().default([]),
+  })
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+    component: () => <div>Index page</div>,
+
+    validateSearch: searchSchema,
+  })
+
+  const routeTree = rootRoute.addChildren([indexRoute])
+  const router = createRouter({ routeTree })
+
+  render(<RouterProvider router={router} />)
+
+  const indexElement = await screen.findByText('Index page')
+  expect(indexElement).toBeInTheDocument()
+})
+
+test('reproducer for #2053', async () => {
+  const rootRoute = createRootRoute({
+    beforeLoad: () => {
+      console.log('beforeload called')
+    },
+  })
+
+  const fooRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/foo/$fooId',
+    component: () => {
+      const { fooId } = fooRoute.useParams()
+      return <div>fooId: {fooId}</div>
+    },
+  })
+
+  window.history.replaceState(null, 'root', '/foo/3ΚΑΠΠΑ')
+
+  const routeTree = rootRoute.addChildren([fooRoute])
+
+  const router = createRouter({
+    routeTree,
+  })
+
+  render(<RouterProvider router={router} />)
+
+  const fooElement = await screen.findByText('fooId: 3ΚΑΠΠΑ')
+  expect(fooElement).toBeInTheDocument()
+})