Repository: TanStack/router
Pull request #1649 (2024-05-21 21:13:37)

feat(react-router): improve the blocker functionality for use with custom UI
We were talking about this on discord, trying to find a handy way to display custom UI when blocking navigation.

I adapted a bit @freshgiammi's first draft and expanded it to the `Blocker` component to also have `allow`/`deny` in its children.
I also updated the example to validate it works as expected.

Basically we get these two functions to allow the user to show some custom UI (a modal?) when blocking the navigation and giving full control if resuming or blocking it.

If the API makes sense I'm happy to also update the docs.


cc @schiller-manuel 

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

CI is running for commit 29e5aff758e1328deb4c99cf0d0a97659c42f7ca.

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


---

Sent with 💌 from [NxCloud.](https://nx.app?utm_source=pull-request&utm_medium=comment)
vercel[bot]: [vc]: #y1DHamXSlayvQQnb2l5RnFD2TiTYbdtxgu1+B7fvBbE=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJzdGFydC1iYXNpYyIsInJvb3REaXJlY3RvcnkiOiJleGFtcGxlcy9yZWFjdC9zdGFydC1iYXNpYyIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS90YW5zdGFjay9zdGFydC1iYXNpYy9FNXBpakViNkRSQjUzTmc3dXNVZHN2cUJGU1lxIiwicHJldmlld1VybCI6InN0YXJ0LWJhc2ljLWdpdC1mb3JrLWJhbGFzdHJvbmctZmVhdC1pbXByb3ZlLWN1LTJiNTE4My10YW5zdGFjay52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IlBFTkRJTkciLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoic3RhcnQtYmFzaWMtZ2l0LWZvcmstYmFsYXN0cm9uZy1mZWF0LWltcHJvdmUtY3UtMmI1MTgzLXRhbnN0YWNrLnZlcmNlbC5hcHAifX1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **start-basic** | 🔄 Building ([Inspect](https://vercel.com/tanstack/start-basic/E5pijEb6DRB53Ng7usUdsvqBFSYq)) | [Visit Preview](https://vercel.live/open-feedback/start-basic-git-fork-balastrong-feat-improve-cu-2b5183-tanstack.vercel.app?via=pr-comment-visit-preview-link&passThrough=1) | 💬 [**Add feedback**](https://vercel.live/open-feedback/start-basic-git-fork-balastrong-feat-improve-cu-2b5183-tanstack.vercel.app?via=pr-comment-feedback-link) | May 24, 2024 10:30am |



Patch:
diff --git a/docs/framework/react/api/router.md b/docs/framework/react/api/router.md
index 3983eb093..9aabd5976 100644
--- a/docs/framework/react/api/router.md
+++ b/docs/framework/react/api/router.md
@@ -31,6 +31,7 @@ title: Router API
   - [`<Outlet>`](./outletComponent)
 - Hooks
   - [`useAwaited()`](./useAwaitedHook)
+  - [`useBlocker()`](./useBlockerHook)
   - [`useChildMatches()`](./useChildMatchesHook)
   - [`useLinkProps()`](./useLinkPropsHook)
   - [`useLoaderData()`](./useLoaderDataHook)
diff --git a/docs/framework/react/api/router/useBlockerHook.md b/docs/framework/react/api/router/useBlockerHook.md
new file mode 100644
index 000000000..6f8d2f526
--- /dev/null
+++ b/docs/framework/react/api/router/useBlockerHook.md
@@ -0,0 +1,79 @@
+---
+id: useBlockerHook
+title: useBlocker hook
+---
+
+The `useBlocker` method is a hook that [blocks navigation](../../../guide/navigation-blocking) when a condition is met.
+
+## useBlocker options
+
+The `useBlocker` hook accepts a single _optional_ argument, an option object:
+
+### `options.blockerFn` option
+
+- Optional
+- Type: `BlockerFn`
+- The function that returns a `boolean` or `Promise<boolean>` indicating whether to allow navigation.
+
+### `options.condition` option
+
+- Optional - defaults to `true`
+- Type: `boolean`
+- A navigation attempt is blocked when this condition is `true`.
+
+## useBlocker returns
+
+An object with the controls to allow manual blocking and unblocking of navigation.
+
+- `status` - A string literal that can be either `'blocked'` or `'idle'`
+- `proceed` - A function that allows navigation to continue
+- `reset` - A function that cancels navigation (`status` will be be reset to `'idle'`)
+
+## Examples
+
+Two common use cases for the `useBlocker` hook are:
+
+### Basic usage
+
+```tsx
+import { useBlocker } from '@tanstack/react-router'
+
+function MyComponent() {
+  const [formIsDirty, setFormIsDirty] = useState(false)
+
+  useBlocker({
+    blockerfn: () => window.confirm('Are you sure you want to leave?'),
+    condition: formIsDirty,
+  })
+
+  // ...
+}
+```
+
+### Custom UI
+
+```tsx
+import { useBlocker } from '@tanstack/react-router'
+
+function MyComponent() {
+  const [formIsDirty, setFormIsDirty] = useState(false)
+
+  const { proceed, reset, status } = useBlocker({
+    condition: formIsDirty,
+  })
+
+  // ...
+
+  return (
+    <>
+      {/* ... */}
+      {status === 'blocked' && (
+        <div>
+          <p>Are you sure you want to leave?</p>
+          <button onClick={proceed}>Yes</button>
+          <button onClick={reset}>No</button>
+        </div>
+      )}
+    </>
+}
+```
diff --git a/docs/framework/react/guide/navigation-blocking.md b/docs/framework/react/guide/navigation-blocking.md
index e281c21b5..3e4dc0053 100644
--- a/docs/framework/react/guide/navigation-blocking.md
+++ b/docs/framework/react/guide/navigation-blocking.md
@@ -43,19 +43,16 @@ import { useBlocker } from '@tanstack/react-router'
 function MyComponent() {
   const [formIsDirty, setFormIsDirty] = useState(false)
 
-  useBlocker(
-    () => window.confirm('Are you sure you want to leave?'),
-    formIsDirty,
-  )
+  useBlocker({
+    blockerfn: () => window.confirm('Are you sure you want to leave?'),
+    condition: formIsDirty,
+  })
 
   // ...
 }
 ```
 
-The `useBlocker` hook takes 2 arguments:
-
-- `blockerFn: BlockerFn` **Required** - A function that returns a `boolean` or `Promise<boolean>` indicating whether to allow navigation.
-- `condition?: boolean` Optional, defaults to `true` - Any expression or variable to be tested for truthiness to determine if navigation should be blocked
+You can find more information about the `useBlocker` hook in the [API reference](../../api/router/useBlockerHook).
 
 ## Component-based blocking
 
@@ -86,3 +83,68 @@ function MyComponent() {
   )
 }
 ```
+
+## How can I show a custom UI?
+
+In most cases, passing `window.confirm` to the `blockerFn` field of the hook input is enough since it will clearly show the user that the navigation is being blocked.
+
+However, in some situations, you might want to show a custom UI that is intentionally less disruptive and more integrated with your app's design.
+
+**Note:** The return value of `blockerFn` takes precedence, do not pass it if you want to use the manual `proceed` and `reset` functions.
+
+### Hook/logical-based custom UI
+
+```tsx
+import { useBlocker } from '@tanstack/react-router'
+
+function MyComponent() {
+  const [formIsDirty, setFormIsDirty] = useState(false)
+
+  const { proceed, reset, status } = useBlocker({
+    condition: formIsDirty,
+  })
+
+  // ...
+
+  return (
+    <>
+      {/* ... */}
+      {status === 'blocked' && (
+        <div>
+          <p>Are you sure you want to leave?</p>
+          <button onClick={proceed}>Yes</button>
+          <button onClick={reset}>No</button>
+        </div>
+      )}
+    </>
+}
+```
+
+### Component-based custom UI
+
+Similarly to the hook, the `Block` component returns the same state and functions as render props:
+
+```tsx
+import { Block } from '@tanstack/react-router'
+
+function MyComponent() {
+  const [formIsDirty, setFormIsDirty] = useState(false)
+
+  return (
+    <Block condition={formIsDirty}>
+      {({ status, proceed, reset }) => (
+        <>
+          {/* ... */}
+          {status === 'blocked' && (
+            <div>
+              <p>Are you sure you want to leave?</p>
+              <button onClick={proceed}>Yes</button>
+              <button onClick={reset}>No</button>
+            </div>
+          )}
+        </>
+      )}
+    </Block>
+  )
+}
+```
diff --git a/examples/react/kitchen-sink-file-based/src/routes/__root.tsx b/examples/react/kitchen-sink-file-based/src/routes/__root.tsx
index 154e1f54a..25fa17648 100644
--- a/examples/react/kitchen-sink-file-based/src/routes/__root.tsx
+++ b/examples/react/kitchen-sink-file-based/src/routes/__root.tsx
@@ -6,8 +6,8 @@ import {
   useRouterState,
 } from '@tanstack/react-router'
 import { TanStackRouterDevtools } from '@tanstack/router-devtools'
-import { Auth } from '../utils/auth'
 import { Spinner } from '../components/Spinner'
+import { Auth } from '../utils/auth'
 
 function RouterSpinner() {
   const isLoading = useRouterState({ select: (s) => s.status === 'pending' })
diff --git a/examples/react/kitchen-sink-file-based/src/routes/_auth.tsx b/examples/react/kitchen-sink-file-based/src/routes/_auth.tsx
index 6efd66548..30fdcf9f2 100644
--- a/examples/react/kitchen-sink-file-based/src/routes/_auth.tsx
+++ b/examples/react/kitchen-sink-file-based/src/routes/_auth.tsx
@@ -1,5 +1,5 @@
 import * as React from 'react'
-import { createFileRoute, Link, redirect } from '@tanstack/react-router'
+import { createFileRoute, redirect } from '@tanstack/react-router'
 import { auth } from '../utils/auth'
 
 export const Route = createFileRoute('/_auth')({
diff --git a/examples/react/kitchen-sink-file-based/src/routes/_layout.tsx b/examples/react/kitchen-sink-file-based/src/routes/_layout.tsx
index 4205b62d4..e75347317 100644
--- a/examples/react/kitchen-sink-file-based/src/routes/_layout.tsx
+++ b/examples/react/kitchen-sink-file-based/src/routes/_layout.tsx
@@ -1,5 +1,5 @@
 import * as React from 'react'
-import { Link, Outlet, createFileRoute } from '@tanstack/react-router'
+import { createFileRoute, Outlet } from '@tanstack/react-router'
 
 export const Route = createFileRoute('/_layout')({
   component: LayoutComponent,
diff --git a/examples/react/kitchen-sink-file-based/src/routes/dashboard.index.tsx b/examples/react/kitchen-sink-file-based/src/routes/dashboard.index.tsx
index 138e4ceab..22fb1c83c 100644
--- a/examples/react/kitchen-sink-file-based/src/routes/dashboard.index.tsx
+++ b/examples/react/kitchen-sink-file-based/src/routes/dashboard.index.tsx
@@ -1,5 +1,5 @@
 import * as React from 'react'
-import { createFileRoute, Link, Outlet } from '@tanstack/react-router'
+import { createFileRoute } from '@tanstack/react-router'
 import { fetchInvoices } from '../utils/mockTodos'
 
 export const Route = createFileRoute('/dashboard/')({
diff --git a/examples/react/kitchen-sink-file-based/src/routes/dashboard.invoices.$invoiceId.tsx b/examples/react/kitchen-sink-file-based/src/routes/dashboard.invoices.$invoiceId.tsx
index 9600b0873..3acc4fd55 100644
--- a/examples/react/kitchen-sink-file-based/src/routes/dashboard.invoices.$invoiceId.tsx
+++ b/examples/react/kitchen-sink-file-based/src/routes/dashboard.invoices.$invoiceId.tsx
@@ -6,9 +6,9 @@ import {
   useRouter,
 } from '@tanstack/react-router'
 import { z } from 'zod'
-import { fetchInvoiceById, patchInvoice } from '../utils/mockTodos'
-import { useMutation } from '../hooks/useMutation'
 import { InvoiceFields } from '../components/InvoiceFields'
+import { useMutation } from '../hooks/useMutation'
+import { fetchInvoiceById, patchInvoice } from '../utils/mockTodos'
 
 export const Route = createFileRoute('/dashboard/invoices/$invoiceId')({
   parseParams: (params) => ({
diff --git a/examples/react/kitchen-sink-file-based/src/routes/dashboard.invoices.index.tsx b/examples/react/kitchen-sink-file-based/src/routes/dashboard.invoices.index.tsx
index d0dadde07..b0280c3fc 100644
--- a/examples/react/kitchen-sink-file-based/src/routes/dashboard.invoices.index.tsx
+++ b/examples/react/kitchen-sink-file-based/src/routes/dashboard.invoices.index.tsx
@@ -1,11 +1,10 @@
 import * as React from 'react'
 import { createFileRoute, useRouter } from '@tanstack/react-router'
-import { useMutation } from '../hooks/useMutation'
-import { Invoice, postInvoice } from '../utils/mockTodos'
 import { InvoiceFields } from '../components/InvoiceFields'
 import { Spinner } from '../components/Spinner'
+import { useMutation } from '../hooks/useMutation'
+import { Invoice, postInvoice } from '../utils/mockTodos'
 
-// @ts-ignore
 export const Route = createFileRoute('/dashboard/invoices/')({
   component: InvoicesIndexComponent,
 })
diff --git a/examples/react/kitchen-sink-file-based/src/routes/dashboard.invoices.tsx b/examples/react/kitchen-sink-file-based/src/routes/dashboard.invoices.tsx
index 13d9955b1..377da2da2 100644
--- a/examples/react/kitchen-sink-file-based/src/routes/dashboard.invoices.tsx
+++ b/examples/react/kitchen-sink-file-based/src/routes/dashboard.invoices.tsx
@@ -5,8 +5,8 @@ import {
   MatchRoute,
   Outlet,
 } from '@tanstack/react-router'
-import { fetchInvoices } from '../utils/mockTodos'
 import { Spinner } from '../components/Spinner'
+import { fetchInvoices } from '../utils/mockTodos'
 
 export const Route = createFileRoute('/dashboard/invoices')({
   loader: () => fetchInvoices(),
diff --git a/examples/react/kitchen-sink-file-based/src/routes/dashboard.users.index.tsx b/examples/react/kitchen-sink-file-based/src/routes/dashboard.users.index.tsx
index 38481e316..7ad977115 100644
--- a/examples/react/kitchen-sink-file-based/src/routes/dashboard.users.index.tsx
+++ b/examples/react/kitchen-sink-file-based/src/routes/dashboard.users.index.tsx
@@ -1,7 +1,6 @@
 import * as React from 'react'
 import { createFileRoute } from '@tanstack/react-router'
 
-// @ts-ignore
 export const Route = createFileRoute('/dashboard/users/')({
   component: UsersIndexComponent,
 })
diff --git a/examples/react/kitchen-sink-file-based/src/routes/dashboard.users.tsx b/examples/react/kitchen-sink-file-based/src/routes/dashboard.users.tsx
index 73756ffb4..2e0342641 100644
--- a/examples/react/kitchen-sink-file-based/src/routes/dashboard.users.tsx
+++ b/examples/react/kitchen-sink-file-based/src/routes/dashboard.users.tsx
@@ -6,9 +6,9 @@ import {
   Outlet,
   useNavigate,
 } from '@tanstack/react-router'
-import { fetchUsers } from '../utils/mockTodos'
-import { Spinner } from '../components/Spinner'
 import { z } from 'zod'
+import { Spinner } from '../components/Spinner'
+import { fetchUsers } from '../utils/mockTodos'
 
 type UsersViewSortBy = 'name' | 'id' | 'email'
 
diff --git a/examples/react/kitchen-sink-file-based/src/routes/index.tsx b/examples/react/kitchen-sink-file-based/src/routes/index.tsx
index 252494421..9a7e33824 100644
--- a/examples/react/kitchen-sink-file-based/src/routes/index.tsx
+++ b/examples/react/kitchen-sink-file-based/src/routes/index.tsx
@@ -11,7 +11,7 @@ function IndexComponent() {
       <div className={`text-lg`}>Welcome Home!</div>
       <hr className={`my-2`} />
       <Link
-        to="/dashboard/invoices/"
+        to="/dashboard/invoices/$invoiceId"
         params={{
           invoiceId: 3,
         }}
diff --git a/examples/react/kitchen-sink-file-based/src/utils/mockTodos.ts b/examples/react/kitchen-sink-file-based/src/utils/mockTodos.ts
index f560db479..71dc13193 100644
--- a/examples/react/kitchen-sink-file-based/src/utils/mockTodos.ts
+++ b/examples/react/kitchen-sink-file-based/src/utils/mockTodos.ts
@@ -1,8 +1,10 @@
-import { PickAsPartial, PickAsRequired } from '@tanstack/react-router'
-import { match } from 'assert'
 import axios from 'redaxios'
 import { produce } from 'immer'
 import { actionDelayFn, loaderDelayFn, shuffle } from './utils'
+
+type PickAsRequired<TValue, TKey extends keyof TValue> = Omit<TValue, TKey> &
+  Required<Pick<TValue, TKey>>
+
 export type Invoice = {
   id: number
   title: string
diff --git a/examples/react/kitchen-sink-react-query-file-based/src/routes/__root.tsx b/examples/react/kitchen-sink-react-query-file-based/src/routes/__root.tsx
index a2750e2fb..131e160c6 100644
--- a/examples/react/kitchen-sink-react-query-file-based/src/routes/__root.tsx
+++ b/examples/react/kitchen-sink-react-query-file-based/src/routes/__root.tsx
@@ -6,10 +6,10 @@ import {
   useRouterState,
 } from '@tanstack/react-router'
 import { TanStackRouterDevtools } from '@tanstack/router-devtools'
-import { Auth } from '../utils/auth'
-import { Spinner } from '../components/Spinner'
 import { QueryClient } from '@tanstack/react-query'
 import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
+import { Auth } from '../utils/auth'
+import { Spinner } from '../components/Spinner'
 
 function RouterSpinner() {
   const isLoading = useRouterState({ select: (s) => s.status === 'pending' })
diff --git a/examples/react/kitchen-sink-react-query-file-based/src/routes/_auth.tsx b/examples/react/kitchen-sink-react-query-file-based/src/routes/_auth.tsx
index 6efd66548..30fdcf9f2 100644
--- a/examples/react/kitchen-sink-react-query-file-based/src/routes/_auth.tsx
+++ b/examples/react/kitchen-sink-react-query-file-based/src/routes/_auth.tsx
@@ -1,5 +1,5 @@
 import * as React from 'react'
-import { createFileRoute, Link, redirect } from '@tanstack/react-router'
+import { createFileRoute, redirect } from '@tanstack/react-router'
 import { auth } from '../utils/auth'
 
 export const Route = createFileRoute('/_auth')({
diff --git a/examples/react/kitchen-sink-react-query-file-based/src/routes/_layout.tsx b/examples/react/kitchen-sink-react-query-file-based/src/routes/_layout.tsx
index 4205b62d4..e75347317 100644
--- a/examples/react/kitchen-sink-react-query-file-based/src/routes/_layout.tsx
+++ b/examples/react/kitchen-sink-react-query-file-based/src/routes/_layout.tsx
@@ -1,5 +1,5 @@
 import * as React from 'react'
-import { Link, Outlet, createFileRoute } from '@tanstack/react-router'
+import { createFileRoute, Outlet } from '@tanstack/react-router'
 
 export const Route = createFileRoute('/_layout')({
   component: LayoutComponent,
diff --git a/examples/react/kitchen-sink-react-query-file-based/src/routes/dashboard.index.tsx b/examples/react/kitchen-sink-react-query-file-based/src/routes/dashboard.index.tsx
index 7aabda90d..86fdaf117 100644
--- a/examples/react/kitchen-sink-react-query-file-based/src/routes/dashboard.index.tsx
+++ b/examples/react/kitchen-sink-react-query-file-based/src/routes/dashboard.index.tsx
@@ -1,7 +1,7 @@
 import * as React from 'react'
 import { createFileRoute } from '@tanstack/react-router'
-import { invoicesQueryOptions } from '../utils/queryOptions'
 import { useSuspenseQuery } from '@tanstack/react-query'
+import { invoicesQueryOptions } from '../utils/queryOptions'
 
 export const Route = createFileRoute('/dashboard/')({
   loader: (opts) =>
diff --git a/examples/react/kitchen-sink-react-query-file-based/src/routes/dashboard.invoices.$invoiceId.tsx b/examples/react/kitchen-sink-react-query-file-based/src/routes/dashboard.invoices.$invoiceId.tsx
index f484df07f..8c5492efd 100644
--- a/examples/react/kitchen-sink-react-query-file-based/src/routes/dashboard.invoices.$invoiceId.tsx
+++ b/examples/react/kitchen-sink-react-query-file-based/src/routes/dashboard.invoices.$invoiceId.tsx
@@ -1,12 +1,12 @@
 import * as React from 'react'
 import { createFileRoute, Link, useNavigate } from '@tanstack/react-router'
+import { useSuspenseQuery } from '@tanstack/react-query'
 import { z } from 'zod'
 import { InvoiceFields } from '../components/InvoiceFields'
 import {
   invoiceQueryOptions,
   useUpdateInvoiceMutation,
 } from '../utils/queryOptions'
-import { useSuspenseQuery } from '@tanstack/react-query'
 
 export const Route = createFileRoute('/dashboard/invoices/$invoiceId')({
   parseParams: (params) => ({
diff --git a/examples/react/kitchen-sink-react-query-file-based/src/routes/dashboard.invoices.index.tsx b/examples/react/kitchen-sink-react-query-file-based/src/routes/dashboard.invoices.index.tsx
index 3f95dbbb2..13405c33a 100644
--- a/examples/react/kitchen-sink-react-query-file-based/src/routes/dashboard.invoices.index.tsx
+++ b/examples/react/kitchen-sink-react-query-file-based/src/routes/dashboard.invoices.index.tsx
@@ -1,8 +1,8 @@
 import * as React from 'react'
-import { createFileRoute, useRouter } from '@tanstack/react-router'
-import { Invoice, postInvoice } from '../utils/mockTodos'
+import { createFileRoute } from '@tanstack/react-router'
 import { InvoiceFields } from '../components/InvoiceFields'
 import { Spinner } from '../components/Spinner'
+import { Invoice } from '../utils/mockTodos'
 import { useCreateInvoiceMutation } from '../utils/queryOptions'
 
 // @ts-ignore
diff --git a/examples/react/kitchen-sink-react-query-file-based/src/routes/dashboard.invoices.tsx b/examples/react/kitchen-sink-react-query-file-based/src/routes/dashboard.invoices.tsx
index 230c154e5..2a2d544d7 100644
--- a/examples/react/kitchen-sink-react-query-file-based/src/routes/dashboard.invoices.tsx
+++ b/examples/react/kitchen-sink-react-query-file-based/src/routes/dashboard.invoices.tsx
@@ -5,9 +5,9 @@ import {
   MatchRoute,
   Outlet,
 } from '@tanstack/react-router'
+import { useSuspenseQuery } from '@tanstack/react-query'
 import { Spinner } from '../components/Spinner'
 import { invoicesQueryOptions } from '../utils/queryOptions'
-import { useSuspenseQuery } from '@tanstack/react-query'
 
 export const Route = createFileRoute('/dashboard/invoices')({
   loader: (opts) =>
diff --git a/examples/react/kitchen-sink-react-query-file-based/src/routes/dashboard.users.index.tsx b/examples/react/kitchen-sink-react-query-file-based/src/routes/dashboard.users.index.tsx
index 38481e316..7ad977115 100644
--- a/examples/react/kitchen-sink-react-query-file-based/src/routes/dashboard.users.index.tsx
+++ b/examples/react/kitchen-sink-react-query-file-based/src/routes/dashboard.users.index.tsx
@@ -1,7 +1,6 @@
 import * as React from 'react'
 import { createFileRoute } from '@tanstack/react-router'
 
-// @ts-ignore
 export const Route = createFileRoute('/dashboard/users/')({
   component: UsersIndexComponent,
 })
diff --git a/examples/react/kitchen-sink-react-query-file-based/src/routes/dashboard.users.tsx b/examples/react/kitchen-sink-react-query-file-based/src/routes/dashboard.users.tsx
index 2b570c7a1..b13a1ac2d 100644
--- a/examples/react/kitchen-sink-react-query-file-based/src/routes/dashboard.users.tsx
+++ b/examples/react/kitchen-sink-react-query-file-based/src/routes/dashboard.users.tsx
@@ -6,9 +6,9 @@ import {
   createFileRoute,
   useNavigate,
 } from '@tanstack/react-router'
-import { Spinner } from '../components/Spinner'
-import { z } from 'zod'
 import { useSuspenseQuery } from '@tanstack/react-query'
+import { z } from 'zod'
+import { Spinner } from '../components/Spinner'
 import { usersQueryOptions } from '../utils/queryOptions'
 
 type UsersViewSortBy = 'name' | 'id' | 'email'
diff --git a/examples/react/kitchen-sink-react-query-file-based/src/routes/dashboard.users.user.tsx b/examples/react/kitchen-sink-react-query-file-based/src/routes/dashboard.users.user.tsx
index 053fc4599..0dece3c32 100644
--- a/examples/react/kitchen-sink-react-query-file-based/src/routes/dashboard.users.user.tsx
+++ b/examples/react/kitchen-sink-react-query-file-based/src/routes/dashboard.users.user.tsx
@@ -1,8 +1,8 @@
 import * as React from 'react'
 import { createFileRoute } from '@tanstack/react-router'
+import { useSuspenseQuery } from '@tanstack/react-query'
 import { z } from 'zod'
 import { userQueryOptions } from '../utils/queryOptions'
-import { useSuspenseQuery } from '@tanstack/react-query'
 
 export const Route = createFileRoute('/dashboard/users/user')({
   validateSearch: z.object({
diff --git a/examples/react/kitchen-sink-react-query-file-based/src/routes/index.tsx b/examples/react/kitchen-sink-react-query-file-based/src/routes/index.tsx
index 252494421..9a7e33824 100644
--- a/examples/react/kitchen-sink-react-query-file-based/src/routes/index.tsx
+++ b/examples/react/kitchen-sink-react-query-file-based/src/routes/index.tsx
@@ -11,7 +11,7 @@ function IndexComponent() {
       <div className={`text-lg`}>Welcome Home!</div>
       <hr className={`my-2`} />
       <Link
-        to="/dashboard/invoices/"
+        to="/dashboard/invoices/$invoiceId"
         params={{
           invoiceId: 3,
         }}
diff --git a/examples/react/kitchen-sink-react-query-file-based/src/routes/login.tsx b/examples/react/kitchen-sink-react-query-file-based/src/routes/login.tsx
index 42c01d793..bcc606ac3 100644
--- a/examples/react/kitchen-sink-react-query-file-based/src/routes/login.tsx
+++ b/examples/react/kitchen-sink-react-query-file-based/src/routes/login.tsx
@@ -6,7 +6,6 @@ export const Route = createFileRoute('/login')({
   validateSearch: z.object({
     redirect: z.string().optional(),
   }),
-}).update({
   component: LoginComponent,
 })
 
diff --git a/examples/react/kitchen-sink-react-query-file-based/src/utils/mockTodos.ts b/examples/react/kitchen-sink-react-query-file-based/src/utils/mockTodos.ts
index f560db479..71dc13193 100644
--- a/examples/react/kitchen-sink-react-query-file-based/src/utils/mockTodos.ts
+++ b/examples/react/kitchen-sink-react-query-file-based/src/utils/mockTodos.ts
@@ -1,8 +1,10 @@
-import { PickAsPartial, PickAsRequired } from '@tanstack/react-router'
-import { match } from 'assert'
 import axios from 'redaxios'
 import { produce } from 'immer'
 import { actionDelayFn, loaderDelayFn, shuffle } from './utils'
+
+type PickAsRequired<TValue, TKey extends keyof TValue> = Omit<TValue, TKey> &
+  Required<Pick<TValue, TKey>>
+
 export type Invoice = {
   id: number
   title: string
diff --git a/examples/react/kitchen-sink-react-query/src/mockTodos.ts b/examples/react/kitchen-sink-react-query/src/mockTodos.ts
index 6f421efa2..71dc13193 100644
--- a/examples/react/kitchen-sink-react-query/src/mockTodos.ts
+++ b/examples/react/kitchen-sink-react-query/src/mockTodos.ts
@@ -1,4 +1,3 @@
-import { match } from 'assert'
 import axios from 'redaxios'
 import { produce } from 'immer'
 import { actionDelayFn, loaderDelayFn, shuffle } from './utils'
diff --git a/examples/react/kitchen-sink/src/main.tsx b/examples/react/kitchen-sink/src/main.tsx
index 7ab8ce1ab..473987ebc 100644
--- a/examples/react/kitchen-sink/src/main.tsx
+++ b/examples/react/kitchen-sink/src/main.tsx
@@ -46,20 +46,17 @@ function RouterSpinner() {
 
 function RootComponent() {
   return (
-    <div className="bg-gradient-to-r from-lime-700 to-green-600 text-white">
+    <>
       <div className={`min-h-screen flex flex-col`}>
-        <div className={`flex items-center gap-2 bg-gray-800/70`}>
+        <div className={`flex items-center border-b gap-2`}>
           <h1 className={`text-3xl p-2`}>Kitchen Sink</h1>
           {/* Show a global spinner when the router is transitioning */}
           <div className={`text-3xl`}>
             <RouterSpinner />
           </div>
         </div>
-
-        <div className={`flex-1 flex p-2 gap-2`}>
-          <div
-            className={`bg-gray-800/70 rounded-lg divide-y divide-gray-500/30 w-56`}
-          >
+        <div className={`flex-1 flex`}>
+          <div className={`divide-y w-56`}>
             {(
               [
                 ['/', 'Home'],
@@ -83,8 +80,9 @@ function RootComponent() {
                       }
                     }
                     preload="intent"
-                    className="block py-1 px-2 text-lime-300 hover:text-lime-200"
-                    activeProps={{ className: '!text-white font-bold' }}
+                    className={`block py-2 px-3 text-blue-700`}
+                    // Make "active" links bold
+                    activeProps={{ className: `font-bold` }}
                   >
                     {label}
                   </Link>
@@ -92,14 +90,14 @@ function RootComponent() {
               )
             })}
           </div>
-          <div className={`flex-1`}>
+          <div className={`flex-1 border-l border-gray-200`}>
             {/* Render our first route match */}
             <Outlet />
           </div>
         </div>
       </div>
       <TanStackRouterDevtools position="bottom-right" />
-    </div>
+    </>
   )
 }
 
@@ -111,12 +109,32 @@ const indexRoute = createRoute({
 
 function IndexComponent() {
   return (
-    <Link
-      to="/dashboard/invoices/$invoiceId"
-      params={{
-        invoiceId: 2,
-      }}
-    />
+    <div className={`p-2`}>
+      <div className={`text-lg`}>Welcome Home!</div>
+      <hr className={`my-2`} />
+      <Link
+        to={invoiceRoute.to}
+        params={{
+          invoiceId: 3,
+        }}
+        className={`py-1 px-2 text-xs bg-blue-500 text-white rounded-full`}
+      >
+        1 New Invoice
+      </Link>
+      <hr className={`my-2`} />
+      <div className={`max-w-xl`}>
+        As you navigate around take note of the UX. It should feel
+        suspense-like, where routes are only rendered once all of their data and
+        elements are ready.
+        <hr className={`my-2`} />
+        To exaggerate async effects, play with the artificial request delay
+        slider in the bottom-left corner.
+        <hr className={`my-2`} />
+        The last 2 sliders determine if link-hover preloading is enabled (and
+        how long those preloads stick around) and also whether to cache rendered
+        route data (and for how long). Both of these default to 0 (or off).
+      </div>
+    </div>
   )
 }
 
@@ -128,10 +146,11 @@ const dashboardRoute = createRoute({
 
 function DashboardComponent() {
   return (
-    <div className="space-y-2">
-      <div
-        className={`flex flex-wrap bg-gray-800/70 rounded-lg divide-x divide-gray-300/20`}
-      >
+    <>
+      <div className="flex items-center border-b">
+        <h2 className="text-xl p-2">Dashboard</h2>
+      </div>
+      <div className="flex flex-wrap divide-x">
         {(
           [
             ['/dashboard', 'Summary', true],
@@ -144,7 +163,7 @@ function DashboardComponent() {
               key={to}
               to={to}
               activeOptions={{ exact }}
-              activeProps={{ className: `font-bold ` }}
+              activeProps={{ className: `font-bold` }}
               className="p-2"
             >
               {label}
@@ -152,8 +171,9 @@ function DashboardComponent() {
           )
         })}
       </div>
+      <hr />
       <Outlet />
-    </div>
+    </>
   )
 }
 
@@ -168,8 +188,11 @@ function DashboardIndexComponent() {
   const invoices = dashboardIndexRoute.useLoaderData()
 
   return (
-    <div className="p-2 bg-gray-800/70 rounded-lg min-h-[200px]">
-      Welcome! You have <strong>{invoices.length} total invoices</strong>.
+    <div className="p-2">
+      <div className="p-2">
+        Welcome to the dashboard! You have{' '}
+        <strong>{invoices.length} total invoices</strong>.
+      </div>
     </div>
   )
 }
@@ -186,7 +209,7 @@ function InvoicesComponent() {
 
   return (
     <div className="flex-1 flex">
-      <div className="divide-y w-48 bg-gray-800/70 rounded-lg divide-y divide-gray-300/20">
+      <div className="divide-y w-48">
         {invoices?.map((invoice) => {
           return (
             <div key={invoice.id}>
@@ -196,7 +219,7 @@ function InvoicesComponent() {
                   invoiceId: invoice.id,
                 }}
                 preload="intent"
-                className="block py-2 px-3 text-lime-300"
+                className="block py-2 px-3 text-blue-700"
                 activeProps={{ className: `font-bold ` }}
               >
                 <pre className="text-sm">
@@ -220,7 +243,7 @@ function InvoicesComponent() {
           )
         })}
       </div>
-      <div className="flex-1">
+      <div className="flex-1 border-l border-gray-200">
         <Outlet />
       </div>
     </div>
@@ -258,7 +281,7 @@ function InvoicesIndexComponent() {
           <InvoiceFields invoice={{} as Invoice} />
           <div>
             <button
-              className="bg-lime-500 rounded p-2 uppercase text-white font-black disabled:opacity-50"
+              className="bg-blue-500 rounded p-2 uppercase text-white font-black disabled:opacity-50"
               disabled={createInvoiceMutation?.status === 'pending'}
             >
               {createInvoiceMutation?.status === 'pending' ? (
@@ -345,13 +368,12 @@ function InvoiceComponent() {
       />
       <div>
         <Link
-          from={invoiceRoute.fullPath}
-          to={invoiceRoute.to}
           search={(old) => ({
             ...old,
             showNotes: old?.showNotes ? undefined : true,
           })}
-          className="text-lime-700"
+          className="text-blue-700"
+          from={invoiceRoute.fullPath}
           params={true}
         >
           {search.showNotes ? 'Close Notes' : 'Show Notes'}{' '}
@@ -378,7 +400,7 @@ function InvoiceComponent() {
       </div>
       <div>
         <button
-          className="bg-lime-500 rounded p-2 uppercase text-white font-black disabled:opacity-50"
+          className="bg-blue-500 rounded p-2 uppercase text-white font-black disabled:opacity-50"
           disabled={updateInvoiceMutation?.status === 'pending'}
         >
           Save
@@ -443,6 +465,24 @@ function UsersComponent() {
     setFilterDraft(filterBy ?? '')
   }, [filterBy])
 
+  const sortedUsers = React.useMemo(() => {
+    if (!users) return []
+
+    return !sortBy
+      ? users
+      : [...users].sort((a, b) => {
+          return a[sortBy] > b[sortBy] ? 1 : -1
+        })
+  }, [users, sortBy])
+
+  const filteredUsers = React.useMemo(() => {
+    if (!filterBy) return sortedUsers
+
+    return sortedUsers.filter((user) =>
+      user.name.toLowerCase().includes(filterBy.toLowerCase()),
+    )
+  }, [sortedUsers, filterBy])
+
   const setSortBy = (sortBy: UsersViewSortBy) =>
     navigate({
       search: (old) => {
@@ -454,7 +494,6 @@ function UsersComponent() {
           },
         }
       },
-      params: true,
       replace: true,
     })
 
@@ -469,36 +508,35 @@ function UsersComponent() {
           },
         }
       },
-      params: true,
       replace: true,
     })
   }, [filterDraft])
 
   return (
     <div className="flex-1 flex">
-      <div className="divide-y divide-gray-500/30 bg-gray-800/70 rounded-lg">
-        <div className="py-2 px-3 flex gap-2 items-center">
+      <div className="divide-y">
+        <div className="py-2 px-3 flex gap-2 items-center bg-gray-100">
           <div>Sort By:</div>
           <select
             value={sortBy}
             onChange={(e) => setSortBy(e.target.value as UsersViewSortBy)}
-            className="flex-1 p-1 px-2 rounded bg-gray-800/70"
+            className="flex-1 border p-1 px-2 rounded"
           >
             {['name', 'id', 'email'].map((d) => {
               return <option key={d} value={d} children={d} />
             })}
           </select>
         </div>
-        <div className="py-2 px-3 flex gap-2 items-center">
+        <div className="py-2 px-3 flex gap-2 items-center bg-gray-100">
           <div>Filter By:</div>
           <input
             value={filterDraft}
             onChange={(e) => setFilterDraft(e.target.value)}
             placeholder="Search Names..."
-            className="min-w-0 flex-1 p-1 px-2 rounded bg-gray-800/70"
+            className="min-w-0 flex-1 border p-1 px-2 rounded"
           />
         </div>
-        {users?.map((user) => {
+        {filteredUsers?.map((user) => {
           return (
             <div key={user.id}>
               <Link
@@ -507,10 +545,10 @@ function UsersComponent() {
                   ...d,
                   userId: user.id,
                 })}
-                className="block py-2 px-3 text-lime-300"
-                activeProps={{ className: `font-bold ` }}
+                className="block py-2 px-3 text-blue-700"
+                activeProps={{ className: `font-bold` }}
               >
-                <pre className="text-sm px-2">
+                <pre className="text-sm">
                   {user.name}{' '}
                   <MatchRoute
                     to={userRoute.to}
@@ -528,7 +566,7 @@ function UsersComponent() {
           )
         })}
       </div>
-      <div className="flex-initial">
+      <div className="flex-initial border-l border-gray-200">
         <Outlet />
       </div>
     </div>
@@ -678,11 +716,8 @@ function LoginComponent() {
       Logged in as <strong>{auth.username}</strong>
       <div className="h-2" />
       <button
-        onClick={() => {
-          auth.logout()
-          router.invalidate()
-        }}
-        className="text-sm bg-lime-500 text-white border inline-block py-1 px-2 rounded"
+        onClick={() => auth.logout()}
+        className="text-sm bg-blue-500 text-white border inline-block py-1 px-2 rounded"
       >
         Log out
       </button>
@@ -697,11 +732,11 @@ function LoginComponent() {
           value={username}
           onChange={(e) => setUsername(e.target.value)}
           placeholder="Username"
-          className="border p-1 px-2 rounded text-black"
+          className="border p-1 px-2 rounded"
         />
         <button
-          type="submit"
-          className="text-sm bg-lime-500 text-white border inline-block py-1 px-2 rounded"
+          onClick={() => auth.logout()}
+          className="text-sm bg-blue-500 text-white border inline-block py-1 px-2 rounded"
         >
           Login
         </button>
@@ -812,7 +847,7 @@ function App() {
         <div className="p-2 space-y-2">
           <div className="flex gap-2">
             <button
-              className="bg-lime-500 text-white rounded p-1 px-2"
+              className="bg-blue-500 text-white rounded p-1 px-2"
               onClick={() => {
                 setLoaderDelay(150)
               }}
@@ -820,7 +855,7 @@ function App() {
               Fast
             </button>
             <button
-              className="bg-lime-500 text-white rounded p-1 px-2"
+              className="bg-blue-500 text-white rounded p-1 px-2"
               onClick={() => {
                 setLoaderDelay(500)
               }}
@@ -828,7 +863,7 @@ function App() {
               Fast 3G
             </button>
             <button
-              className="bg-lime-500 text-white rounded p-1 px-2"
+              className="bg-blue-500 text-white rounded p-1 px-2"
               onClick={() => {
                 setLoaderDelay(2000)
               }}
@@ -852,7 +887,7 @@ function App() {
         <div className="p-2 space-y-2">
           <div className="flex gap-2">
             <button
-              className="bg-lime-500 text-white rounded p-1 px-2"
+              className="bg-blue-500 text-white rounded p-1 px-2"
               onClick={() => {
                 setPendingMs(1000)
                 setPendingMinMs(500)
@@ -914,7 +949,7 @@ function InvoiceFields({
           name="title"
           defaultValue={invoice?.title}
           placeholder="Invoice Title"
-          className="bg-gray-800/70 rounded p-2 w-full"
+          className="border border-opacity-50 rounded p-2 w-full"
           disabled={disabled}
         />
       </h2>
@@ -924,7 +959,7 @@ function InvoiceFields({
           defaultValue={invoice?.body}
           rows={6}
           placeholder="Invoice Body..."
-          className="bg-gray-800/70 p-2 rounded w-full"
+          className="border border-opacity-50 p-2 rounded w-full"
           disabled={disabled}
         />
       </div>
diff --git a/examples/react/kitchen-sink/src/mockTodos.ts b/examples/react/kitchen-sink/src/mockTodos.ts
index f560db479..71dc13193 100644
--- a/examples/react/kitchen-sink/src/mockTodos.ts
+++ b/examples/react/kitchen-sink/src/mockTodos.ts
@@ -1,8 +1,10 @@
-import { PickAsPartial, PickAsRequired } from '@tanstack/react-router'
-import { match } from 'assert'
 import axios from 'redaxios'
 import { produce } from 'immer'
 import { actionDelayFn, loaderDelayFn, shuffle } from './utils'
+
+type PickAsRequired<TValue, TKey extends keyof TValue> = Omit<TValue, TKey> &
+  Required<Pick<TValue, TKey>>
+
 export type Invoice = {
   id: number
   title: string
diff --git a/examples/react/navigation-blocking/src/main.tsx b/examples/react/navigation-blocking/src/main.tsx
index c4dbad6fc..00026b0fe 100644
--- a/examples/react/navigation-blocking/src/main.tsx
+++ b/examples/react/navigation-blocking/src/main.tsx
@@ -1,15 +1,15 @@
-import React from 'react'
-import ReactDOM from 'react-dom/client'
 import {
+  Link,
   Outlet,
   RouterProvider,
-  Link,
+  createRootRoute,
+  createRoute,
   createRouter,
   useBlocker,
-  createRoute,
-  createRootRoute,
 } from '@tanstack/react-router'
 import { TanStackRouterDevtools } from '@tanstack/router-devtools'
+import React from 'react'
+import ReactDOM from 'react-dom/client'
 
 const rootRoute = createRootRoute({
   component: RootComponent,
@@ -66,20 +66,50 @@ const editor1Route = createRoute({
 
 function Editor1Component() {
   const [value, setValue] = React.useState('')
+  const [useCustomBlocker, setUseCustomBlocker] = React.useState(false)
 
-  useBlocker(
-    () => window.confirm('Are you sure you want to leave editor 1?'),
-    value,
-  )
+  const { proceed, reset, status } = useBlocker({
+    blockerFn: useCustomBlocker
+      ? undefined
+      : () => window.confirm('Are you sure you want to leave editor 1?'),
+    condition: value,
+  })
 
   return (
-    <div className="p-2">
+    <div className="flex flex-col p-2">
       <h3>Editor 1</h3>
-      <input
-        value={value}
-        onChange={(e) => setValue(e.target.value)}
-        className="border"
-      />
+      <label>
+        <input
+          type="checkbox"
+          checked={useCustomBlocker}
+          onChange={(e) => setUseCustomBlocker(e.target.checked)}
+        />{' '}
+        Use custom blocker
+      </label>
+      <div>
+        <input
+          value={value}
+          onChange={(e) => setValue(e.target.value)}
+          className="border"
+        />
+      </div>
+      {status === 'blocked' && (
+        <div className="mt-2">
+          <div>Are you sure you want to leave editor 1?</div>
+          <button
+            className="bg-lime-500 text-white rounded p-1 px-2 mr-2"
+            onClick={proceed}
+          >
+            YES
+          </button>
+          <button
+            className="bg-red-500 text-white rounded p-1 px-2"
+            onClick={reset}
+          >
+            NO
+          </button>
+        </div>
+      )}
       <hr className="m-2" />
       <Link to="/editor-1/editor-2">Go to Editor 2</Link>
       <Outlet />
@@ -96,10 +126,10 @@ const editor2Route = createRoute({
 function Editor2Component() {
   const [value, setValue] = React.useState('')
 
-  useBlocker(
-    () => window.confirm('Are you sure you want to leave editor 2?'),
-    value,
-  )
+  useBlocker({
+    blockerFn: () => window.confirm('Are you sure you want to leave editor 2?'),
+    condition: value,
+  })
 
   return (
     <div className="p-2">
diff --git a/packages/react-router/src/useBlocker.tsx b/packages/react-router/src/useBlocker.tsx
index f0da68765..66a736170 100644
--- a/packages/react-router/src/useBlocker.tsx
+++ b/packages/react-router/src/useBlocker.tsx
@@ -3,25 +3,93 @@ import { useRouter } from './useRouter'
 import type { BlockerFn } from '@tanstack/history'
 import type { ReactNode } from './route'
 
+type BlockerResolver = {
+  status: 'idle' | 'blocked'
+  proceed: () => void
+  reset: () => void
+}
+
+type BlockerOpts = {
+  blockerFn?: BlockerFn
+  condition?: boolean | any
+}
+
+export function useBlocker(blockerFnOrOpts?: BlockerOpts): BlockerResolver
+
+/**
+ * @deprecated Use the BlockerOpts object syntax instead
+ */
+export function useBlocker(
+  blockerFn?: BlockerFn,
+  condition?: boolean | any,
+): BlockerResolver
+
 export function useBlocker(
-  blockerFn: BlockerFn,
-  condition: boolean | any = true,
-): void {
+  blockerFnOrOpts?: BlockerFn | BlockerOpts,
+  condition?: boolean | any,
+): BlockerResolver {
+  const { blockerFn, blockerCondition } = blockerFnOrOpts
+    ? typeof blockerFnOrOpts === 'function'
+      ? { blockerFn: blockerFnOrOpts, blockerCondition: condition ?? true }
+      : {
+          blockerFn: blockerFnOrOpts.blockerFn,
+          blockerCondition: blockerFnOrOpts.condition ?? true,
+        }
+    : { blockerFn: undefined, blockerCondition: condition ?? true }
   const { history } = useRouter()
 
-  React.useEffect(() => {
-    if (!condition) return
-    return history.block(blockerFn)
+  const [resolver, setResolver] = React.useState<BlockerResolver>({
+    status: 'idle',
+    proceed: () => {},
+    reset: () => {},
   })
+
+  const createPromise = () =>
+    new Promise<boolean>((resolve) => {
+      setResolver({
+        status: 'idle',
+        proceed: () => resolve(true),
+        reset: () => resolve(false),
+      })
+    })
+
+  const [promise, setPromise] = React.useState(createPromise)
+
+  React.useEffect(() => {
+    const blockerFnComposed = async () => {
+      // If a function is provided, it takes precedence over the promise blocker
+      if (blockerFn) {
+        return await blockerFn()
+      }
+
+      setResolver((prev) => ({
+        ...prev,
+        status: 'blocked',
+      }))
+      const canNavigateAsync = await promise
+
+      setPromise(createPromise)
+
+      return canNavigateAsync
+    }
+
+    return !blockerCondition ? undefined : history.block(blockerFnComposed)
+  }, [blockerFn, blockerCondition, history, promise])
+
+  return resolver
 }
 
-export function Block({ blocker, condition, children }: PromptProps) {
-  useBlocker(blocker, condition)
-  return children ?? null
+export function Block({ blockerFn, condition, children }: PromptProps) {
+  const resolver = useBlocker({ blockerFn, condition })
+  return children
+    ? typeof children === 'function'
+      ? children(resolver)
+      : children
+    : null
 }
 
 export type PromptProps = {
-  blocker: BlockerFn
+  blockerFn?: BlockerFn
   condition?: boolean | any
-  children?: ReactNode
+  children?: ReactNode | (({ proceed, reset }: BlockerResolver) => ReactNode)
 }


Test patch:
diff --git a/packages/react-router/tests/router.test.tsx b/packages/react-router/tests/router.test.tsx
index 6b1a44f5c..b22da6d96 100644
--- a/packages/react-router/tests/router.test.tsx
+++ b/packages/react-router/tests/router.test.tsx
@@ -1,4 +1,4 @@
-import React from 'react'
+import React, { act } from 'react'
 import { afterEach, describe, expect, it, vi } from 'vitest'
 import { cleanup, render, waitFor } from '@testing-library/react'
 import {
@@ -439,7 +439,7 @@ describe('router emits events during rendering', () => {
     await router.load()
     render(<RouterProvider router={router} />)
 
-    router.navigate({ to: '/$', params: { _splat: 'tanner' } })
+    await act(() => router.navigate({ to: '/$', params: { _splat: 'tanner' } }))
 
     await waitFor(() => expect(mockFn1).toBeCalledTimes(2))
   })
diff --git a/packages/react-router/tests/useBlocker.test.tsx b/packages/react-router/tests/useBlocker.test.tsx
new file mode 100644
index 000000000..a5c1fffad
--- /dev/null
+++ b/packages/react-router/tests/useBlocker.test.tsx
@@ -0,0 +1,42 @@
+import '@testing-library/jest-dom/vitest'
+import { cleanup, renderHook } from '@testing-library/react'
+import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
+import { useBlocker } from '../src'
+
+const block = vi.fn()
+vi.mock('../src/useRouter', () => ({
+  useRouter: () => ({ history: { block } }),
+}))
+
+describe('useBlocker', () => {
+  beforeEach(() => {
+    block.mockClear()
+  })
+
+  describe('condition', () => {
+    test('should not add the blocker if condition is false', () => {
+      renderHook(() => useBlocker({ condition: false }))
+      expect(block).not.toHaveBeenCalled()
+    })
+
+    test('should not add the blocker if condition is false (deprecated API)', () => {
+      renderHook(() => useBlocker(undefined, false))
+      expect(block).not.toHaveBeenCalled()
+    })
+
+    test('should add the blocker if condition is true', () => {
+      renderHook(() => useBlocker({ condition: true }))
+      expect(block).toHaveBeenCalledOnce()
+    })
+
+    test('should add the blocker if condition is true (deprecated API)', () => {
+      renderHook(() => useBlocker(undefined, true))
+      expect(block).toHaveBeenCalledOnce()
+    })
+
+    test('should add the blocker if condition is not provided', () => {
+      renderHook(() => useBlocker())
+      expect(block).toHaveBeenCalledOnce()
+    })
+  })
+})