Repository: TanStack/router
Pull request #2945 (2024-12-07 21:00:07)

fix(react-router): preloads

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

CI is running for commit 4157f7156eaaf0a8ef5df71db3f23bd85ed1783d.

[📂 Click to track the progress, see the status, the terminal output, and the build insights.](https://cloud.nx.app/cipes/6754b776c11171718d211198?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/3f99edfe-2197-45a1-9669-ed14d29e433d)
<details><summary><b>More templates</b></summary><p>

- [tanstack-router-react-example-authenticated-routes](https://pkg.pr.new/template/9c47bc6f-2a8a-4f37-8c43-973676e4d907)
- [tanstack-router-react-example-basic-default-search-params](https://pkg.pr.new/template/19e658bc-49c2-44f1-b42d-f2d959f6fe40)
- [tanstack-router-react-example-basic-file-based](https://pkg.pr.new/template/00b247f6-2871-43b5-8890-ae539ac0d1fb)
- [tanstack-router-react-example-react-query](https://pkg.pr.new/template/bb5d0fde-54c5-40e6-af7c-6cf5d0264516)
- [tanstack-router-react-example-basic-file-based-codesplitting](https://pkg.pr.new/template/bce4010b-cf18-4596-b851-8fab76cb839c)
- [tanstack-router-react-example-basic-react-query-file-based](https://pkg.pr.new/template/33069553-643c-4578-8de4-367dc00e32a7)
- [tanstack-router-react-example-basic-ssr-file-based](https://pkg.pr.new/template/afd4cd0a-266c-4568-886f-785b9653fc83)
- [tanstack-router-react-example-basic](https://pkg.pr.new/template/f4082312-ba75-48fe-ad6c-e6f63c6b43af)
- [tanstack-router-react-example-basic-ssr-streaming-file-based](https://pkg.pr.new/template/4b25c15a-0e75-467c-9aa1-1313550e191d)
- [tanstack-router-react-example-basic-virtual-file-based](https://pkg.pr.new/template/f4525027-c5b9-4020-af1b-a2197264ffd6)
- [tanstack-router-react-example-deferred-data](https://pkg.pr.new/template/f63e48d3-1cea-44cf-8364-f8ac977924f3)
- [tanstack-router-react-example-kitchen-sink](https://pkg.pr.new/template/5de073a5-ef1e-42c8-9665-5b954d54785e)
- [tanstack-router-react-example-basic-virtual-inside-file-based](https://pkg.pr.new/template/8f542260-7256-4eec-9929-a265dee23337)
- [tanstack-router-react-example-kitchen-sink-file-based](https://pkg.pr.new/template/481e8c3d-0807-4638-bd17-885aff52b26a)
- [tanstack-router-react-example-kitchen-sink-react-query](https://pkg.pr.new/template/9762093a-312f-4595-9922-be1615bc0c26)
- [tanstack-router-react-example-kitchen-sink-react-query-file-based](https://pkg.pr.new/template/0c323076-425f-40b7-91e0-974ceed086b9)
- [tanstack-router-react-example-location-masking](https://pkg.pr.new/template/86452463-537d-46b3-9bbc-b166575bad0b)
- [tanstack-router-react-example-large-file-based](https://pkg.pr.new/template/9ba39f6d-4cf8-420c-80a4-10a61f9b57eb)
- [tanstack-router-react-example-navigation-blocking](https://pkg.pr.new/template/4f050026-4d47-48c3-8094-f3fdb3f7a0a4)
- [tanstack-router-react-example-quickstart](https://pkg.pr.new/template/4b23ee2f-b995-4ff8-9949-e1904959a4cc)
- [tanstack-router-react-example-quickstart-esbuild-file-based](https://pkg.pr.new/template/dc8212b4-487e-492b-ade5-9305aec5a1c3)
- [tanstack-router-react-example-quickstart-file-based](https://pkg.pr.new/template/30278b81-391e-424b-b4fe-05ade2812e49)
- [tanstack-router-react-example-quickstart-webpack-file-based](https://pkg.pr.new/template/7543d003-0671-4823-aded-1ad748b91589)
- [tanstack-router-react-example-quickstart-rspack-file-based](https://pkg.pr.new/template/0307fa57-3b48-495c-a407-13d8a073562c)
- [router-monorepo-react-query](https://pkg.pr.new/template/0b845c7e-5916-447c-91d7-907ea5b92471)
- [router-mono-simple](https://pkg.pr.new/template/dc4ce0ff-3047-46a5-a255-3dad9d5ea33c)
- [tanstack-router-react-example-scroll-restoration](https://pkg.pr.new/template/00208373-5ef3-4b7d-878a-ca7ce0256e39)
- [tanstack-search-validator-adapters](https://pkg.pr.new/template/4a1133e2-4af5-47df-8b55-d1d1944a922d)
- [tanstack-start-example-basic](https://pkg.pr.new/template/bc239229-1728-420a-92a9-e7ab66d96bc0)
- [tanstack-start-example-basic-rsc](https://pkg.pr.new/template/0a1c3c23-8931-4c0c-93e7-a4305762fc7e)
- [tanstack-start-example-basic-react-query](https://pkg.pr.new/template/60840f80-bc80-4391-bf68-33eaf117df8c)
- [tanstack-start-example-basic-auth](https://pkg.pr.new/template/8a26ec6b-e593-4bce-8e79-052baccdefa1)
- [tanstack-start-example-clerk-basic](https://pkg.pr.new/template/2c8116b8-677d-4965-a7d1-bf094545d828)
- [tanstack-start-example-convex-trellaux](https://pkg.pr.new/template/31e8d79c-0692-4774-a436-e8c583dff8ba)
- [tanstack-start-example-counter](https://pkg.pr.new/template/e6810112-76c9-4722-a14f-465dfa36b587)
- [tanstack-start-example-large](https://pkg.pr.new/template/72b25193-c703-47e1-8108-b634e7d35467)
- [tanstack-start-example-supabase-basic](https://pkg.pr.new/template/58292369-2ced-4031-ba62-5363f7521287)
- [tanstack-start-example-trellaux](https://pkg.pr.new/template/3038f917-c1d5-4918-90f2-d74aeaf3c0e1)
- [tanstack-router-react-example-with-framer-motion](https://pkg.pr.new/template/64ce7f6e-c436-4c05-9c14-99f0a94eafda)
- [tanstack-router-react-example-with-trpc](https://pkg.pr.new/template/1430d4dc-3462-49a4-b6ba-9e3554c65bad)
- [tanstack-router-react-example-with-trpc-react-query](https://pkg.pr.new/template/58bcaeac-9ff7-4806-8047-6e7c46db1ed7)

</p></details>
      
    


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

```
npm i https://pkg.pr.new/@tanstack/arktype-adapter@2945
```

</p></details>
      
    

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

```
npm i https://pkg.pr.new/@tanstack/create-router@2945
```

</p></details>
      
    

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

```
npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@2945
```

</p></details>
      
    

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

```
npm i https://pkg.pr.new/@tanstack/history@2945
```

</p></details>
      
    

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

```
npm i https://pkg.pr.new/@tanstack/react-cross-context@2945
```

</p></details>
      
    

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

```
npm i https://pkg.pr.new/@tanstack/react-router@2945
```

</p></details>
      
    

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

```
npm i https://pkg.pr.new/@tanstack/react-router-with-query@2945
```

</p></details>
      
    

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

```
npm i https://pkg.pr.new/@tanstack/router-cli@2945
```

</p></details>
      
    

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

```
npm i https://pkg.pr.new/@tanstack/router-devtools@2945
```

</p></details>
      
    

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

```
npm i https://pkg.pr.new/@tanstack/router-generator@2945
```

</p></details>
      
    

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

```
npm i https://pkg.pr.new/@tanstack/router-plugin@2945
```

</p></details>
      
    

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

```
npm i https://pkg.pr.new/@tanstack/router-vite-plugin@2945
```

</p></details>
      
    

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

```
npm i https://pkg.pr.new/@tanstack/start@2945
```

</p></details>
      
    

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

```
npm i https://pkg.pr.new/@tanstack/start-vite-plugin@2945
```

</p></details>
      
    

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

```
npm i https://pkg.pr.new/@tanstack/valibot-adapter@2945
```

</p></details>
      
    

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

```
npm i https://pkg.pr.new/@tanstack/virtual-file-routes@2945
```

</p></details>
      
    

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

```
npm i https://pkg.pr.new/@tanstack/zod-adapter@2945
```

</p></details>
      
    

_commit: <a href="https://github.com/TanStack/router/runs/34078782051"><code>4157f71</code></a>_


Patch:
diff --git a/docs/framework/react/start/server-functions.md b/docs/framework/react/start/server-functions.md
index a8eca49ef..1c910d4c7 100644
--- a/docs/framework/react/start/server-functions.md
+++ b/docs/framework/react/start/server-functions.md
@@ -28,7 +28,7 @@ However, they are similar to regular API routes in that:
 Server functions can be defined anywhere in your application, but must be defined at the top level of a file. They can be called from anywhere in your application, including loaders, hooks, etc. Traditionally, this pattern is known as a Remote Procedure Call (RPC), but due to the isomorphic nature of these functions, we refer to them as server functions.
 
 - On the server bundle, server functions logic is left alone. Nothing needs to be done since they are already in the correct place.
-- On the client, server functions are modified to remove server-only logic out of the client bundle and replaced with a function that, when called, makes a `fetch` request to the server instructing it to execute the server function in the server bundle and then send the response back to the client.
+- On the client, server functions will be removed; they exist only on the server. Any calls to the server function on the client will be replaced with a `fetch` request to the server to execute the server function, and send the response back to the client.
 
 ## Server Function Middleware
 
@@ -38,7 +38,7 @@ Server functions can use middleware to share logic, context, common operations,
 
 > We'd like to thank the [tRPC](https://trpc.io/) team for both the inspiration of TanStack Start's server function design and guidance while implementing it. We love (and recommend) using tRPC for API routes so much that we insisted on server functions getting the same 1st class treatment and developer experience. Thank you!
 
-Server functions are defined using the `createServerFn` function, exported from the `@tanstack/start` package. This function must be called with an HTTP verb, and an async function that will be executed on the server. Here's an example:
+Server functions are defined with the `createServerFn` function, from the `@tanstack/start` package. This function takes an optional `options` argument for specifying the http verb, and allows you to chain off the result to define things like the body of the server function, input validation, middleware, etc. Here's a simple example:
 
 ```tsx
 // getServerTime.ts
@@ -63,7 +63,7 @@ export const getServerTime = createServerFn().handler(async () => {
 
 Server functions accept a single parameter, which can be a variety of types:
 
-- Primitives
+- Standard JavaScript types
   - `string`
   - `number`
   - `boolean`
@@ -92,15 +92,13 @@ greet({
 })
 ```
 
-## Validation / Runtime Type Safety
+## Runtime Input Validation / Type Safety
 
-Server functions can be configured to validate their input data at runtime. This is useful for ensuring that the input is of the correct type before executing the server function, can provide more friendly error messages, and even power type-safety if configured correctly.
+Server functions can be configured to validate their input data at runtime, while adding type safety. This is useful for ensuring the input is of the correct type before executing the server function, and providing more friendly error messages.
 
-To enable validation, call the `validator` method on the server function. You can pass a variety of things:
+This is done with the `validator` method. It will accept whatever input is passed to the server function. The value (and type) you return from this function will become the input to the passed to the actual server function handler.
 
-- A function that returns the valid value and type
-- A thrown error, to indicate that the input is invalid
-- Schema objects that define the shape of the input data (the exact shape of the schema object depends on the validation library you choose to use)
+Validators also integrate seamlessly with external validators, if you want to use something like Zod.
 
 ### Basic Validation
 
@@ -132,7 +130,7 @@ export const greet = createServerFn({ method: 'GET' })
 
 ### Using a Validation Library
 
-You can also use a validation library like Zod to validate the input parameter:
+Validation libraries like Zod can be used like so:
 
 ```tsx
 import { createServerFn } from '@tanstack/start'
@@ -160,7 +158,7 @@ greet({
 
 ## Type Safety
 
-Since server-functions cross the network boundary, it's important to ensure that the data being passed to them is not only just the right type, but also validated at runtime. This is especially important wheen dealing with user input, as it can be unpredictable. To help ensure developers validate their I/O data, types are reliant on using the `input` validation, which is then used to infer the input and output types of the server function.
+Since server-functions cross the network boundary, it's important to ensure the data being passed to them is not only the right type, but also validated at runtime. This is especially important when dealing with user input, as it can be unpredictable. To ensure developers validate their I/O data, types are reliant on validation. The return type of the `validator` function will be the input to the server function's handler.
 
 ```tsx
 import { createServerFn } from '@tanstack/start'
@@ -197,7 +195,7 @@ function test() {
 
 ## Inference
 
-Server functions infer their input and output types based on the `validator` handler and the return type of the `handler` function. In fact, the `validator` you pass can even have it's own separate input/output types, which can be useful if your validator performs some kind of transformation on the input data.
+Server functions infer their input, and output types based on the input to the `validator`, and return value of `handler` functions, respectively. In fact, the `validator` you define can even have its own separate input/output types, which can be useful if your validator performs transformations on the input data.
 
 To illustrate this, let's take a look at an example using the `zod` validation library:
 
@@ -224,7 +222,7 @@ createTransaction({
 
 ## Non-Validated Inference
 
-While we highly recommend using a validation library to validate your network I/O data, you may for whatever reason _not_ want to validate your data, but still have the type-safety. To do this, you can still provide type information to the server function using an identity function as the `validator` handler that casts the input and or output to the correct type:
+While we highly recommend using a validation library to validate your network I/O data, you may, for whatever reason _not_ want to validate your data, but still have type safety. To do this, provide type information to the server function using an identity function as the `validator`, that types the input, and or output to the correct types:
 
 ```tsx
 import { createServerFn } from '@tanstack/start'
@@ -321,7 +319,7 @@ function Test() {
 
 ## Server Function Context
 
-In addition to the single parameter that server functions accept, you can also access server request context from within any server function using many utilities from `vinxi/http`. Under the hood, Vinxi uses `unjs`'s `h3` package to perform cross-platform HTTP requests.
+In addition to the single parameter that server functions accept, you can also access server request context from within any server function using utilities from `vinxi/http`. Under the hood, Vinxi uses `unjs`'s `h3` package to perform cross-platform HTTP requests.
 
 There are many context functions available to you for things like:
 
@@ -853,5 +851,5 @@ The process looks like this:
 - If the `use server` directive is missing, it is added to the top of the function
 - On the client, the inner function is extracted out of the client bundle and into a separate server bundle
 - The client-side server function is replaced with a proxy function that sends a request to the server to execute the function that was extracted
-- On the server, the server function is no extracted and is executed as-is
+- On the server, the server function is not extracted, and is executed as-is
 - After extraction occurs, each bundle applies a dead-code elimination process to remove any unused code from each bundle.
diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts
index c05cdf295..b06691bd4 100644
--- a/packages/react-router/src/router.ts
+++ b/packages/react-router/src/router.ts
@@ -1743,7 +1743,6 @@ export class Router<
           }
           maskedNext = build(maskedDest)
         }
-        // console.log('buildWithMatches', {foundMask, dest, maskedDest, maskedNext})
       }
 
       const nextMatches = this.getMatchedRoutes(next, dest)
@@ -2118,19 +2117,24 @@ export class Router<
     let updated!: AnyRouteMatch
     const isPending = this.state.pendingMatches?.find((d) => d.id === id)
     const isMatched = this.state.matches.find((d) => d.id === id)
+    const isCached = this.state.cachedMatches.find((d) => d.id === id)
 
     const matchesKey = isPending
       ? 'pendingMatches'
       : isMatched
         ? 'matches'
-        : 'cachedMatches'
+        : isCached
+          ? 'cachedMatches'
+          : ''
 
-    this.__store.setState((s) => ({
-      ...s,
-      [matchesKey]: s[matchesKey]?.map((d) =>
-        d.id === id ? (updated = updater(d)) : d,
-      ),
-    }))
+    if (matchesKey) {
+      this.__store.setState((s) => ({
+        ...s,
+        [matchesKey]: s[matchesKey]?.map((d) =>
+          d.id === id ? (updated = updater(d)) : d,
+        ),
+      }))
+    }
 
     return updated
   }
@@ -2146,7 +2150,7 @@ export class Router<
   loadMatches = async ({
     location,
     matches,
-    preload,
+    preload: allPreload,
     onReady,
     updateMatch = this.updateMatch,
   }: {
@@ -2170,6 +2174,10 @@ export class Router<
       }
     }
 
+    const resolvePreload = (matchId: string) => {
+      return !!(allPreload && !this.state.matches.find((d) => d.id === matchId))
+    }
+
     if (!this.isServer && !this.state.matches.length) {
       triggerOnReady()
     }
@@ -2270,7 +2278,7 @@ export class Router<
               const shouldPending = !!(
                 onReady &&
                 !this.isServer &&
-                !preload &&
+                !resolvePreload(matchId) &&
                 (route.options.loader || route.options.beforeLoad) &&
                 typeof pendingMs === 'number' &&
                 pendingMs !== Infinity &&
@@ -2354,6 +2362,8 @@ export class Router<
                   const { search, params, context, cause } =
                     this.getMatch(matchId)!
 
+                  const preload = resolvePreload(matchId)
+
                   const beforeLoadFnContext: BeforeLoadContextOptions<
                     any,
                     any,
@@ -2364,7 +2374,7 @@ export class Router<
                     search,
                     abortController,
                     params,
-                    preload: !!preload,
+                    preload,
                     context,
                     location,
                     navigate: (opts: any) =>
@@ -2432,7 +2442,9 @@ export class Router<
                 (async () => {
                   const { loaderPromise: prevLoaderPromise } =
                     this.getMatch(matchId)!
+
                   let loaderRunningAsync = false
+
                   if (prevLoaderPromise) {
                     await prevLoaderPromise
                   } else {
@@ -2448,6 +2460,8 @@ export class Router<
                         cause,
                       } = this.getMatch(matchId)!
 
+                      const preload = resolvePreload(matchId)
+
                       return {
                         params,
                         deps: loaderDeps,
@@ -2466,6 +2480,8 @@ export class Router<
                     // This is where all of the stale-while-revalidate magic happens
                     const age = Date.now() - this.getMatch(matchId)!.updatedAt
 
+                    const preload = resolvePreload(matchId)
+
                     const staleAge = preload
                       ? (route.options.preloadStaleTime ??
                         this.options.defaultPreloadStaleTime ??
@@ -2686,7 +2702,7 @@ export class Router<
       await triggerOnReady()
     } catch (err) {
       if (isRedirect(err) || isNotFound(err)) {
-        if (isNotFound(err) && !preload) {
+        if (isNotFound(err) && !allPreload) {
           await triggerOnReady()
         }
         throw err
@@ -2806,17 +2822,21 @@ export class Router<
       dest: opts,
     })
 
-    const loadedMatchIds = Object.fromEntries(
-      [
-        ...this.state.matches,
-        ...(this.state.pendingMatches ?? []),
-        ...this.state.cachedMatches,
-      ].map((d) => [d.id, true]),
+    const activeMatchIds = new Set(
+      [...this.state.matches, ...(this.state.pendingMatches ?? [])].map(
+        (d) => d.id,
+      ),
     )
 
+    const loadedMatchIds = new Set([
+      ...activeMatchIds,
+      ...this.state.cachedMatches.map((d) => d.id),
+    ])
+
+    // If the matches are already loaded, we need to add them to the cachedMatches
     this.__store.batch(() => {
       matches.forEach((match) => {
-        if (!loadedMatchIds[match.id]) {
+        if (!loadedMatchIds.has(match.id)) {
           this.__store.setState((s) => ({
             ...s,
             cachedMatches: [...(s.cachedMatches as any), match],
@@ -2830,6 +2850,14 @@ export class Router<
         matches,
         location: next,
         preload: true,
+        updateMatch: (id, updater) => {
+          // Don't update the match if it's currently loaded
+          if (activeMatchIds.has(id)) {
+            matches = matches.map((d) => (d.id === id ? updater(d) : d))
+          } else {
+            this.updateMatch(id, updater)
+          }
+        },
       })
 
       return matches


Test patch:
diff --git a/packages/react-router/tests/link.test.tsx b/packages/react-router/tests/link.test.tsx
index c36d8b5cc..e76cf82c7 100644
--- a/packages/react-router/tests/link.test.tsx
+++ b/packages/react-router/tests/link.test.tsx
@@ -3794,6 +3794,130 @@ describe('Link', () => {
 
     expect(window.location.pathname).toBe('/posts')
   })
+
+  describe('when preloading a link, `preload` should be', () => {
+    async function runTest({
+      expectedPreload,
+      testIdToHover,
+    }: {
+      expectedPreload: boolean
+      testIdToHover: string
+    }) {
+      const rootRoute = createRootRoute({
+        component: () => {
+          return (
+            <>
+              <Link
+                data-testid="link-1"
+                to="/posts/$postId"
+                params={{ postId: 'id1' }}
+              >
+                To first post
+              </Link>
+              <Link
+                data-testid="link-2"
+                to="/posts/$postId"
+                params={{ postId: 'id2' }}
+              >
+                To second post
+              </Link>
+              <Outlet />
+            </>
+          )
+        },
+      })
+
+      const indexRoute = createRoute({
+        getParentRoute: () => rootRoute,
+        path: '/',
+        component: () => {
+          return (
+            <>
+              <h1>Index</h1>
+            </>
+          )
+        },
+      })
+
+      const PostsComponent = () => {
+        return (
+          <>
+            <h1>Posts</h1>
+            <Outlet />
+          </>
+        )
+      }
+
+      const postsBeforeLoadFn = vi.fn()
+      const postsLoaderFn = vi.fn()
+
+      const postsRoute = createRoute({
+        getParentRoute: () => rootRoute,
+        path: 'posts',
+        component: PostsComponent,
+        beforeLoad: postsBeforeLoadFn,
+        loader: postsLoaderFn,
+      })
+
+      const PostComponent = () => {
+        const params = useParams({ strict: false })
+        return (
+          <>
+            <span>Params: {params.postId}</span>
+          </>
+        )
+      }
+
+      const postBeforeLoadFn = vi.fn()
+      const postLoaderFn = vi.fn()
+
+      const postRoute = createRoute({
+        getParentRoute: () => postsRoute,
+        path: '$postId',
+        component: PostComponent,
+        beforeLoad: postBeforeLoadFn,
+        loader: postLoaderFn,
+      })
+
+      const routeTree = rootRoute.addChildren([
+        indexRoute,
+        postsRoute.addChildren([postRoute]),
+      ])
+
+      const router = createRouter({
+        routeTree,
+        defaultPreload: 'intent',
+      })
+
+      render(<RouterProvider router={router} />)
+      const link = await screen.findByTestId(testIdToHover)
+      fireEvent.mouseOver(link)
+
+      const expected = expect.objectContaining({ preload: expectedPreload })
+      await waitFor(() =>
+        expect(postsBeforeLoadFn).toHaveBeenCalledWith(expected),
+      )
+      await waitFor(() => expect(postsLoaderFn).toHaveBeenCalledWith(expected))
+
+      await waitFor(() =>
+        expect(postBeforeLoadFn).toHaveBeenCalledWith(expected),
+      )
+      await waitFor(() => expect(postLoaderFn).toHaveBeenCalledWith(expected))
+    }
+    test('`true` when on / and hovering `/posts/id1` ', async () => {
+      await runTest({ expectedPreload: true, testIdToHover: 'link-1' })
+    })
+
+    test('`false` when on `/posts/id1` and hovering `/posts/id1`', async () => {
+      window.history.replaceState(null, 'root', '/posts/id1')
+      await runTest({ expectedPreload: false, testIdToHover: 'link-1' })
+    })
+
+    test('`true` when on `/posts/id1` and hovering `/posts/id2`', async () => {
+      window.history.replaceState(null, 'root', '/posts/id1')
+      await runTest({ expectedPreload: false, testIdToHover: 'link-2' })
+    })
+  })
 })
 
 describe('createLink', () => {