Repository: TanStack/router
Pull request #2592 (2024-10-20 00:29:19)

feat(react-router): allow to explicitly check if a search prop is not set
implements the feature requested in #2566

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

CI is running for commit 3e93bca684e1db0fb2b80c6e7a340b376762ab4e.

[📂 Click to track the progress, see the status, the terminal output, and the build insights.](https://cloud.nx.app/cipes/67144ef293239d47a57586bd?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/75a7a128-30a1-45c4-b68c-fb610870aa62)
<details><summary><b>More templates</b></summary><p>

- [tanstack-router-react-example-authenticated-routes](https://pkg.pr.new/template/22f533ab-9d39-43c2-ab88-367bf3021352)
- [tanstack-router-react-example-basic](https://pkg.pr.new/template/6c431e88-0ee6-4505-9a9e-a9f4c7b715ea)
- [tanstack-router-react-example-basic-default-search-params](https://pkg.pr.new/template/e05a3ef7-c388-4003-af2a-ca4b963aa58a)
- [tanstack-router-react-example-basic-file-based](https://pkg.pr.new/template/5f78b9a9-29f3-4ea7-8dee-a55ed04999d6)
- [tanstack-router-react-example-basic-file-based-codesplitting](https://pkg.pr.new/template/ace5c76a-7bda-4a19-936a-6b5583263d90)
- [tanstack-router-react-example-react-query](https://pkg.pr.new/template/3b10bc0a-ae87-4301-9630-23659a383762)
- [tanstack-router-react-example-basic-react-query-file-based](https://pkg.pr.new/template/130b1337-5d94-4b72-9ad1-7b8bfdff0017)
- [tanstack-router-react-example-basic-ssr-file-based](https://pkg.pr.new/template/578b077e-3f5d-48c3-af9b-6b82cacf0e7a)
- [tanstack-router-react-example-basic-ssr-streaming-file-based](https://pkg.pr.new/template/e0d92e06-1d5b-4fdd-be3a-8e486db12924)
- [tanstack-router-react-example-basic-virtual-file-based](https://pkg.pr.new/template/d1d56ee7-503b-4596-8262-742af254cf3f)
- [tanstack-router-react-example-basic-virtual-inside-file-based](https://pkg.pr.new/template/9db4b686-6638-4646-ad9a-1f1e9f3732c6)
- [tanstack-router-react-example-deferred-data](https://pkg.pr.new/template/f620de12-0703-4b25-b10b-2bd97d9443c5)
- [tanstack-router-react-example-kitchen-sink](https://pkg.pr.new/template/c75243d9-62bd-48be-8e43-f0cc41ce2084)
- [tanstack-router-react-example-kitchen-sink-file-based](https://pkg.pr.new/template/c257babd-03da-4807-9b05-d4f2fecd9660)
- [tanstack-router-react-example-kitchen-sink-react-query](https://pkg.pr.new/template/312c2585-3549-4fbb-80df-ebe81413e25f)
- [tanstack-router-react-example-kitchen-sink-react-query-file-based](https://pkg.pr.new/template/f09064da-af3c-41b8-9c4e-d82e29b9fd77)
- [tanstack-router-react-example-large-file-based](https://pkg.pr.new/template/c3ce7b8c-3c80-452d-a606-3c878b467190)
- [tanstack-router-react-example-location-masking](https://pkg.pr.new/template/ac89feb4-01a7-4c20-a49b-5d15ed85e7e8)
- [tanstack-router-react-example-navigation-blocking](https://pkg.pr.new/template/26c189cf-6651-4a15-a3bf-729e7a33f2ee)
- [tanstack-router-react-example-quickstart](https://pkg.pr.new/template/16bbbbef-a8cd-40a7-a150-1ac1c1a4a62c)
- [tanstack-router-react-example-quickstart-file-based](https://pkg.pr.new/template/331497db-0f5e-4afa-83b0-655b5d421c50)
- [tanstack-router-react-example-quickstart-rspack-file-based](https://pkg.pr.new/template/02ade7d7-b558-4f91-87bf-817bb379eb2b)
- [tanstack-router-react-example-quickstart-webpack-file-based](https://pkg.pr.new/template/1ca8fc7f-048c-433d-b57d-52dde263fea8)
- [router-monorepo-react-query](https://pkg.pr.new/template/42c685ca-90b4-4792-b9bd-1a38d8344fa8)
- [router-mono-simple](https://pkg.pr.new/template/eec5d685-d790-4457-ad17-001ec282c251)
- [tanstack-router-react-example-scroll-restoration](https://pkg.pr.new/template/1ea10eec-5408-4e1e-83e6-ae82f2f9d5de)
- [tanstack-router-search-validator-adapters](https://pkg.pr.new/template/4019efd0-6d7f-4bac-9456-a71a8b287c92)
- [tanstack-start-example-basic-auth](https://pkg.pr.new/template/5cffdc96-1af4-45f0-84c4-be42b2518637)
- [tanstack-start-example-basic](https://pkg.pr.new/template/e703b1fd-07f1-48c1-a7cf-413fe62defce)
- [tanstack-start-example-basic-react-query](https://pkg.pr.new/template/15072f03-f31e-47e5-a6ab-9ef2ea168b0e)
- [tanstack-start-example-basic-rsc](https://pkg.pr.new/template/3666012d-0953-4822-815f-1f821d86baef)
- [tanstack-start-example-clerk-basic](https://pkg.pr.new/template/f008d253-adca-4049-b180-c8f94054e6a3)
- [tanstack-start-example-convex-trellaux](https://pkg.pr.new/template/09bc36b4-514a-4a3d-b7cc-cd804c7c98df)
- [tanstack-start-example-counter](https://pkg.pr.new/template/a12ee98f-6df8-4f38-aeba-ef581e9826d5)
- [tanstack-start-example-supabase-basic](https://pkg.pr.new/template/c3223d9b-ff13-4aeb-94d2-e3e048b93e8b)
- [tanstack-start-example-trellaux](https://pkg.pr.new/template/9a60b298-3cf1-4fec-95d7-b4993b447119)
- [tanstack-router-react-example-with-framer-motion](https://pkg.pr.new/template/cb5ac7cc-cbc5-44ba-a200-6a53332d4f8b)
- [tanstack-router-react-example-with-trpc](https://pkg.pr.new/template/bd71b783-8703-42a0-b06a-a0c6c5833269)
- [tanstack-router-react-example-with-trpc-react-query](https://pkg.pr.new/template/02fd5122-0c82-40b5-b745-5a58f43036fd)

</p></details>
      
    


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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

_commit: <a href="https://github.com/TanStack/router/runs/31779300196"><code>3e93bca</code></a>_


Patch:
diff --git a/docs/framework/react/guide/navigation.md b/docs/framework/react/guide/navigation.md
index 561bd5db5..b6fb0c7c5 100644
--- a/docs/framework/react/guide/navigation.md
+++ b/docs/framework/react/guide/navigation.md
@@ -88,6 +88,7 @@ export type LinkOptions<
     exact?: boolean
     includeHash?: boolean
     includeSearch?: boolean
+    explicitUndefined?: boolean
   }
   // If set, will preload the linked route on hover and cache it for this many milliseconds in hopes that the user will eventually navigate there.
   preload?: false | 'intent'
@@ -282,6 +283,10 @@ export interface ActiveOptions {
   // If true, the link will only be active if the current URL search params inclusively match the `search` prop
   // Defaults to `true`
   includeSearch?: boolean
+  // This modifies the `includeSearch` behavior.
+  // If true,  properties in `search` that are explicitly `undefined` must NOT be present in the current URL search params for the link to be active.
+  // defaults to `false`
+  explicitUndefined?: boolean
 }
 ```
 
diff --git a/packages/react-router/src/link.tsx b/packages/react-router/src/link.tsx
index d670354e8..dc478fafb 100644
--- a/packages/react-router/src/link.tsx
+++ b/packages/react-router/src/link.tsx
@@ -470,6 +470,7 @@ export interface ActiveOptions {
   exact?: boolean
   includeHash?: boolean
   includeSearch?: boolean
+  explicitUndefined?: boolean
 }
 
 export type LinkOptions<
@@ -685,11 +686,10 @@ export function useLinkProps<
       }
 
       if (activeOptions?.includeSearch ?? true) {
-        const searchTest = deepEqual(
-          s.location.search,
-          next.search,
-          !activeOptions?.exact,
-        )
+        const searchTest = deepEqual(s.location.search, next.search, {
+          partial: !activeOptions?.exact,
+          ignoreUndefined: !activeOptions?.explicitUndefined,
+        })
         if (!searchTest) {
           return false
         }
diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts
index 9263e99aa..edeb675b2 100644
--- a/packages/react-router/src/router.ts
+++ b/packages/react-router/src/router.ts
@@ -2725,13 +2725,15 @@ export class Router<
       return false
     }
     if (location.params) {
-      if (!deepEqual(match, location.params, true)) {
+      if (!deepEqual(match, location.params, { partial: true })) {
         return false
       }
     }
 
     if (match && (opts?.includeSearch ?? true)) {
-      return deepEqual(baseLocation.search, next.search, true) ? match : false
+      return deepEqual(baseLocation.search, next.search, { partial: true })
+        ? match
+        : false
     }
 
     return match
diff --git a/packages/react-router/src/utils.ts b/packages/react-router/src/utils.ts
index d780aec80..6c836c335 100644
--- a/packages/react-router/src/utils.ts
+++ b/packages/react-router/src/utils.ts
@@ -218,7 +218,19 @@ export function isPlainArray(value: unknown): value is Array<unknown> {
   return Array.isArray(value) && value.length === Object.keys(value).length
 }
 
-export function deepEqual(a: any, b: any, partial: boolean = false): boolean {
+function getObjectKeys(obj: any, ignoreUndefined: boolean) {
+  let keys = Object.keys(obj)
+  if (ignoreUndefined) {
+    keys = keys.filter((key) => obj[key] !== undefined)
+  }
+  return keys
+}
+
+export function deepEqual(
+  a: any,
+  b: any,
+  opts?: { partial?: boolean; ignoreUndefined?: boolean },
+): boolean {
   if (a === b) {
     return true
   }
@@ -228,23 +240,22 @@ export function deepEqual(a: any, b: any, partial: boolean = false): boolean {
   }
 
   if (isPlainObject(a) && isPlainObject(b)) {
-    const aKeys = Object.keys(a).filter((key) => a[key] !== undefined)
-    const bKeys = Object.keys(b).filter((key) => b[key] !== undefined)
+    const ignoreUndefined = opts?.ignoreUndefined ?? true
+    const aKeys = getObjectKeys(a, ignoreUndefined)
+    const bKeys = getObjectKeys(b, ignoreUndefined)
 
-    if (!partial && aKeys.length !== bKeys.length) {
+    if (!opts?.partial && aKeys.length !== bKeys.length) {
       return false
     }
 
-    return !bKeys.some(
-      (key) => !(key in a) || !deepEqual(a[key], b[key], partial),
-    )
+    return !bKeys.some((key) => !(key in a) || !deepEqual(a[key], b[key], opts))
   }
 
   if (Array.isArray(a) && Array.isArray(b)) {
     if (a.length !== b.length) {
       return false
     }
-    return !a.some((item, index) => !deepEqual(item, b[index], partial))
+    return !a.some((item, index) => !deepEqual(item, b[index], opts))
   }
 
   return false


Test patch:
diff --git a/packages/react-router/tests/link.test.tsx b/packages/react-router/tests/link.test.tsx
index 7b6becc23..bce18a322 100644
--- a/packages/react-router/tests/link.test.tsx
+++ b/packages/react-router/tests/link.test.tsx
@@ -214,132 +214,197 @@ describe('Link', () => {
     expect(postsLink).not.toHaveAttribute('data-status', 'active')
   })
 
-  test('when the current route has a search fields with undefined values', async () => {
-    const rootRoute = createRootRoute()
-    const indexRoute = createRoute({
-      getParentRoute: () => rootRoute,
-      path: '/',
-      component: () => {
-        return (
-          <>
-            <h1>Index</h1>
-            <Link
-              to="/"
-              activeOptions={{ exact: true }}
-              inactiveProps={{ className: 'inactive' }}
-            >
-              Index exact
-            </Link>
-            <Link
-              to="/"
-              search={{ foo: undefined }}
-              inactiveProps={{ className: 'inactive' }}
-            >
-              Index foo=undefined
-            </Link>
-            <Link
-              to="/"
-              search={{ foo: undefined }}
-              activeOptions={{ exact: true }}
-              inactiveProps={{ className: 'inactive' }}
-            >
-              Index foo=undefined-exact
-            </Link>
-            <Link
-              to="/"
-              search={{ foo: 'bar' }}
-              inactiveProps={{
-                className: 'inactive',
-              }}
-            >
-              Index foo=bar
-            </Link>
-          </>
-        )
-      },
-    })
+  describe('when the current route has a search fields with undefined values', () => {
+    async function runTest(opts: { explicitUndefined: boolean | undefined }) {
+      const rootRoute = createRootRoute()
+      const indexRoute = createRoute({
+        getParentRoute: () => rootRoute,
+        path: '/',
+        component: () => {
+          return (
+            <>
+              <h1>Index</h1>
+              <Link
+                to="/"
+                activeOptions={{ exact: true }}
+                inactiveProps={{ className: 'inactive' }}
+              >
+                Index exact
+              </Link>
+              <Link
+                to="/"
+                search={{ foo: undefined }}
+                inactiveProps={{ className: 'inactive' }}
+                activeOptions={{ explicitUndefined: opts.explicitUndefined }}
+              >
+                Index foo=undefined
+              </Link>
+              <Link
+                to="/"
+                search={{ foo: undefined }}
+                activeOptions={{
+                  exact: true,
+                  explicitUndefined: opts.explicitUndefined,
+                }}
+                inactiveProps={{ className: 'inactive' }}
+              >
+                Index foo=undefined-exact
+              </Link>
+              <Link
+                to="/"
+                search={{ foo: 'bar' }}
+                inactiveProps={{
+                  className: 'inactive',
+                }}
+              >
+                Index foo=bar
+              </Link>
+            </>
+          )
+        },
+      })
 
-    const router = createRouter({
-      routeTree: rootRoute.addChildren([indexRoute]),
-    })
+      const router = createRouter({
+        routeTree: rootRoute.addChildren([indexRoute]),
+      })
 
-    render(<RouterProvider router={router} />)
+      render(<RouterProvider router={router} />)
 
-    // round 1
-    const indexExactLink = await screen.findByRole('link', {
-      name: 'Index exact',
-    })
+      // round 1
+      const indexExactLink = await screen.findByRole('link', {
+        name: 'Index exact',
+      })
 
-    const indexFooUndefinedLink = await screen.findByRole('link', {
-      name: 'Index foo=undefined',
-    })
+      const indexFooUndefinedLink = await screen.findByRole('link', {
+        name: 'Index foo=undefined',
+      })
 
-    const indexFooUndefinedExactLink = await screen.findByRole('link', {
-      name: 'Index foo=undefined-exact',
-    })
+      const indexFooUndefinedExactLink = await screen.findByRole('link', {
+        name: 'Index foo=undefined-exact',
+      })
 
-    const indexFooBarLink = await screen.findByRole('link', {
-      name: 'Index foo=bar',
-    })
+      const indexFooBarLink = await screen.findByRole('link', {
+        name: 'Index foo=bar',
+      })
 
-    expect(window.location.pathname).toBe('/')
+      expect(window.location.pathname).toBe('/')
 
-    expect(indexExactLink).toHaveClass('active')
-    expect(indexExactLink).not.toHaveClass('inactive')
-    expect(indexExactLink).toHaveAttribute('href', '/')
-    expect(indexExactLink).toHaveAttribute('aria-current', 'page')
-    expect(indexExactLink).toHaveAttribute('data-status', 'active')
-
-    expect(indexFooUndefinedLink).toHaveClass('active')
-    expect(indexFooUndefinedLink).not.toHaveClass('inactive')
-    expect(indexFooUndefinedLink).toHaveAttribute('href', '/')
-    expect(indexFooUndefinedLink).toHaveAttribute('aria-current', 'page')
-    expect(indexFooUndefinedLink).toHaveAttribute('data-status', 'active')
-
-    expect(indexFooUndefinedExactLink).toHaveClass('active')
-    expect(indexFooUndefinedExactLink).not.toHaveClass('inactive')
-    expect(indexFooUndefinedExactLink).toHaveAttribute('href', '/')
-    expect(indexFooUndefinedExactLink).toHaveAttribute('aria-current', 'page')
-    expect(indexFooUndefinedExactLink).toHaveAttribute('data-status', 'active')
-
-    expect(indexFooBarLink).toHaveClass('inactive')
-    expect(indexFooBarLink).not.toHaveClass('active')
-    expect(indexFooBarLink).toHaveAttribute('href', '/?foo=bar')
-    expect(indexFooBarLink).not.toHaveAttribute('aria-current', 'page')
-    expect(indexFooBarLink).not.toHaveAttribute('data-status', 'active')
-
-    // navigate to /?foo=bar
-    fireEvent.click(indexFooBarLink)
-
-    expect(indexExactLink).toHaveClass('inactive')
-    expect(indexExactLink).not.toHaveClass('active')
-    expect(indexExactLink).toHaveAttribute('href', '/')
-    expect(indexExactLink).not.toHaveAttribute('aria-current', 'page')
-    expect(indexExactLink).not.toHaveAttribute('data-status', 'active')
-
-    expect(indexFooUndefinedLink).toHaveClass('active')
-    expect(indexFooUndefinedLink).not.toHaveClass('inactive')
-    expect(indexFooUndefinedLink).toHaveAttribute('href', '/')
-    expect(indexFooUndefinedLink).toHaveAttribute('aria-current', 'page')
-    expect(indexFooUndefinedLink).toHaveAttribute('data-status', 'active')
-
-    expect(indexFooUndefinedExactLink).toHaveClass('inactive')
-    expect(indexFooUndefinedExactLink).not.toHaveClass('active')
-    expect(indexFooUndefinedExactLink).toHaveAttribute('href', '/')
-    expect(indexFooUndefinedExactLink).not.toHaveAttribute(
-      'aria-current',
-      'page',
-    )
-    expect(indexFooUndefinedExactLink).not.toHaveAttribute(
-      'data-status',
-      'active',
+      expect(indexExactLink).toHaveClass('active')
+      expect(indexExactLink).not.toHaveClass('inactive')
+      expect(indexExactLink).toHaveAttribute('href', '/')
+      expect(indexExactLink).toHaveAttribute('aria-current', 'page')
+      expect(indexExactLink).toHaveAttribute('data-status', 'active')
+
+      if (opts.explicitUndefined) {
+        expect(indexFooUndefinedLink).not.toHaveClass('active')
+        expect(indexFooUndefinedLink).toHaveClass('inactive')
+        expect(indexFooUndefinedLink).not.toHaveAttribute(
+          'aria-current',
+          'page',
+        )
+        expect(indexFooUndefinedLink).not.toHaveAttribute(
+          'data-status',
+          'active',
+        )
+      } else {
+        expect(indexFooUndefinedLink).toHaveClass('active')
+        expect(indexFooUndefinedLink).not.toHaveClass('inactive')
+        expect(indexFooUndefinedLink).toHaveAttribute('aria-current', 'page')
+        expect(indexFooUndefinedLink).toHaveAttribute('data-status', 'active')
+      }
+
+      expect(indexFooUndefinedLink).toHaveAttribute('href', '/')
+
+      if (opts.explicitUndefined) {
+        expect(indexFooUndefinedExactLink).not.toHaveClass('active')
+        expect(indexFooUndefinedExactLink).toHaveClass('inactive')
+        expect(indexFooUndefinedExactLink).not.toHaveAttribute(
+          'aria-current',
+          'page',
+        )
+        expect(indexFooUndefinedExactLink).not.toHaveAttribute(
+          'data-status',
+          'active',
+        )
+      } else {
+        expect(indexFooUndefinedExactLink).toHaveClass('active')
+        expect(indexFooUndefinedExactLink).not.toHaveClass('inactive')
+        expect(indexFooUndefinedExactLink).toHaveAttribute(
+          'aria-current',
+          'page',
+        )
+        expect(indexFooUndefinedExactLink).toHaveAttribute(
+          'data-status',
+          'active',
+        )
+      }
+
+      expect(indexFooUndefinedExactLink).toHaveAttribute('href', '/')
+
+      expect(indexFooBarLink).toHaveClass('inactive')
+      expect(indexFooBarLink).not.toHaveClass('active')
+      expect(indexFooBarLink).toHaveAttribute('href', '/?foo=bar')
+      expect(indexFooBarLink).not.toHaveAttribute('aria-current', 'page')
+      expect(indexFooBarLink).not.toHaveAttribute('data-status', 'active')
+
+      // navigate to /?foo=bar
+      fireEvent.click(indexFooBarLink)
+
+      expect(indexExactLink).toHaveClass('inactive')
+      expect(indexExactLink).not.toHaveClass('active')
+      expect(indexExactLink).toHaveAttribute('href', '/')
+      expect(indexExactLink).not.toHaveAttribute('aria-current', 'page')
+      expect(indexExactLink).not.toHaveAttribute('data-status', 'active')
+
+      if (opts.explicitUndefined) {
+        expect(indexFooUndefinedLink).not.toHaveClass('active')
+        expect(indexFooUndefinedLink).toHaveClass('inactive')
+        expect(indexFooUndefinedLink).not.toHaveAttribute(
+          'aria-current',
+          'page',
+        )
+        expect(indexFooUndefinedLink).not.toHaveAttribute(
+          'data-status',
+          'active',
+        )
+      } else {
+        expect(indexFooUndefinedLink).toHaveClass('active')
+        expect(indexFooUndefinedLink).not.toHaveClass('inactive')
+        expect(indexFooUndefinedLink).toHaveAttribute('aria-current', 'page')
+        expect(indexFooUndefinedLink).toHaveAttribute('data-status', 'active')
+      }
+
+      expect(indexFooUndefinedLink).toHaveAttribute('href', '/')
+
+      expect(indexFooUndefinedExactLink).toHaveClass('inactive')
+      expect(indexFooUndefinedExactLink).not.toHaveClass('active')
+      expect(indexFooUndefinedExactLink).toHaveAttribute('href', '/')
+      expect(indexFooUndefinedExactLink).not.toHaveAttribute(
+        'aria-current',
+        'page',
+      )
+      expect(indexFooUndefinedExactLink).not.toHaveAttribute(
+        'data-status',
+        'active',
+      )
+
+      expect(indexFooBarLink).toHaveClass('active')
+      expect(indexFooBarLink).not.toHaveClass('inactive')
+      expect(indexFooBarLink).toHaveAttribute('href', '/?foo=bar')
+      expect(indexFooBarLink).toHaveAttribute('aria-current', 'page')
+      expect(indexFooBarLink).toHaveAttribute('data-status', 'active')
+    }
+
+    test.each([undefined, false])(
+      'activeOptions.explicitUndefined=%s',
+      async (explicitUndefined) => {
+        await runTest({ explicitUndefined })
+      },
     )
 
-    expect(indexFooBarLink).toHaveClass('active')
-    expect(indexFooBarLink).not.toHaveClass('inactive')
-    expect(indexFooBarLink).toHaveAttribute('href', '/?foo=bar')
-    expect(indexFooBarLink).toHaveAttribute('aria-current', 'page')
-    expect(indexFooBarLink).toHaveAttribute('data-status', 'active')
+    test('activeOptions.explicitUndefined=true', async () => {
+      await runTest({ explicitUndefined: true })
+    })
   })
 
   test('when the current route is the root with beforeLoad that throws', async () => {
diff --git a/packages/react-router/tests/utils.test.ts b/packages/react-router/tests/utils.test.ts
index 367831e65..9140d5017 100644
--- a/packages/react-router/tests/utils.test.ts
+++ b/packages/react-router/tests/utils.test.ts
@@ -305,51 +305,95 @@ describe('deepEqual', () => {
     it('should return `true` for equal objects', () => {
       const a = { a: { b: 'b' }, c: 'c', d: [{ d: 'd ' }] }
       const b = { a: { b: 'b' }, c: 'c', d: [{ d: 'd ' }] }
-      expect(deepEqual(a, b, partial)).toEqual(true)
-      expect(deepEqual(b, a, partial)).toEqual(true)
+      expect(deepEqual(a, b, { partial })).toEqual(true)
+      expect(deepEqual(b, a, { partial })).toEqual(true)
     })
 
     it('should return `false` for non equal objects', () => {
       const a = { a: { b: 'b' }, c: 'c' }
       const b = { a: { b: 'c' }, c: 'c' }
-      expect(deepEqual(a, b, partial)).toEqual(false)
-      expect(deepEqual(b, a, partial)).toEqual(false)
+      expect(deepEqual(a, b, { partial })).toEqual(false)
+      expect(deepEqual(b, a, { partial })).toEqual(false)
     })
 
     it('should return `true` for equal objects and ignore `undefined` properties', () => {
       const a = { a: 'a', b: undefined, c: 'c' }
       const b = { a: 'a', c: 'c' }
-      expect(deepEqual(a, b, partial)).toEqual(true)
-      expect(deepEqual(b, a, partial)).toEqual(true)
+      expect(deepEqual(a, b, { partial })).toEqual(true)
+      expect(deepEqual(b, a, { partial })).toEqual(true)
     })
 
     it('should return `true` for equal objects and ignore `undefined` nested properties', () => {
       const a = { a: { b: 'b', x: undefined }, c: 'c' }
       const b = { a: { b: 'b' }, c: 'c', d: undefined }
-      expect(deepEqual(a, b, partial)).toEqual(true)
-      expect(deepEqual(b, a, partial)).toEqual(true)
+      expect(deepEqual(a, b, { partial })).toEqual(true)
+      expect(deepEqual(b, a, { partial })).toEqual(true)
     })
 
     it('should return `true` for equal arrays and ignore `undefined` object properties', () => {
+      const a = { a: { b: 'b' }, c: undefined }
+      const b = { a: { b: 'b' } }
+      expect(deepEqual([a], [b], { partial })).toEqual(true)
+      expect(deepEqual([b], [a], { partial })).toEqual(true)
+    })
+
+    it('should return `true` for equal arrays and ignore nested `undefined` object properties', () => {
       const a = { a: { b: 'b', x: undefined }, c: 'c' }
       const b = { a: { b: 'b' }, c: 'c' }
-      expect(deepEqual([a], [b], partial)).toEqual(true)
-      expect(deepEqual([b], [a], partial)).toEqual(true)
+      expect(deepEqual([a], [b], { partial })).toEqual(true)
+      expect(deepEqual([b], [a], { partial })).toEqual(true)
     })
   })
+
+  describe('ignoreUndefined = false', () => {
+    const ignoreUndefined = false
+    describe('partial = false', () => {
+      const partial = false
+      it('should return `false` for objects', () => {
+        const a = { a: { b: 'b', x: undefined }, c: 'c' }
+        const b = { a: { b: 'b' }, c: 'c', d: undefined }
+        expect(deepEqual(a, b, { partial, ignoreUndefined })).toEqual(false)
+        expect(deepEqual(b, a, { partial, ignoreUndefined })).toEqual(false)
+      })
+
+      it('should return `false` for arrays', () => {
+        const a = { a: { b: 'b', x: undefined }, c: 'c' }
+        const b = { a: { b: 'b' }, c: 'c' }
+        expect(deepEqual([a], [b], { partial, ignoreUndefined })).toEqual(false)
+        expect(deepEqual([b], [a], { partial, ignoreUndefined })).toEqual(false)
+      })
+    })
+    describe('partial = true', () => {
+      const partial = true
+      it('should return `true` for objects', () => {
+        const a = { a: { b: 'b' }, c: 'c' }
+        const b = { a: { b: 'b' }, c: 'c', d: undefined }
+        expect(deepEqual(a, b, { partial, ignoreUndefined })).toEqual(false)
+        expect(deepEqual(b, a, { partial, ignoreUndefined })).toEqual(true)
+      })
+
+      it('should return `true` for arrays', () => {
+        const a = { a: { b: 'b', x: undefined }, c: 'c' }
+        const b = { a: { b: 'b' }, c: 'c' }
+        expect(deepEqual([a], [b], { partial, ignoreUndefined })).toEqual(true)
+        expect(deepEqual([b], [a], { partial, ignoreUndefined })).toEqual(false)
+      })
+    })
+  })
+
   describe('partial comparison', () => {
     it('correctly compares partially equal objects', () => {
       const a = { a: { b: 'b' }, c: 'c', d: [{ d: 'd ' }] }
       const b = { a: { b: 'b' }, c: 'c' }
-      expect(deepEqual(a, b, true)).toEqual(true)
-      expect(deepEqual(b, a, true)).toEqual(false)
+      expect(deepEqual(a, b, { partial: true })).toEqual(true)
+      expect(deepEqual(b, a, { partial: true })).toEqual(false)
     })
 
     it('correctly compares partially equal objects and ignores `undefined` object properties', () => {
       const a = { a: { b: 'b' }, c: 'c', d: [{ d: 'd ' }], e: undefined }
       const b = { a: { b: 'b' }, c: 'c', d: undefined }
-      expect(deepEqual(a, b, true)).toEqual(true)
-      expect(deepEqual(b, a, true)).toEqual(false)
+      expect(deepEqual(a, b, { partial: true })).toEqual(true)
+      expect(deepEqual(b, a, { partial: true })).toEqual(false)
     })
   })
 })