Capítulo 14 de 24

Chapter 14: Navigation Blocking

Core Idea

Navigation blocking intercepts an in-progress route change (or a browser beforeunload) and asks the user to confirm before it completes, typically to protect unsaved form state. TanStack Router exposes this through the useBlocker hook, which can block based on component state or on the specific route/params/search being navigated to.

Setup

  • Routing style: code-based (createRoute / createRootRoute / createRouter)
  • Key dependencies: @tanstack/react-router (useBlocker), @tanstack/react-router-devtools
  • Structure: single src/main.tsx with an editor-1 route (nested editor-2 child) whose text input triggers a block, plus a root-level blocker that targets one specific destination (/foo/$id with id: '123' and search.hello === 'world') regardless of component state.

Code Example

function Editor1Component() {
  const [value, setValue] = React.useState('')

  // Block leaving editor-1 if there is text in the input
  const { proceed, reset, next, current, status } = useBlocker({
    shouldBlockFn: () => value !== '',
    enableBeforeUnload: () => value !== '',
    withResolver: true,
  })

  return (
    <div className="flex flex-col p-2">
      <input value={value} onChange={(e) => setValue(e.target.value)} className="border" />
      {status === 'blocked' && (
        <div className="mt-2">
          <div>Are you sure you want to leave editor 1?</div>
          <div>You are going from {current.pathname} to {next.pathname}</div>
          <button onClick={proceed}>YES</button>
          <button onClick={reset}>NO</button>
        </div>
      )}
    </div>
  )
}

A second, route-targeted blocker on the root component only fires for one exact destination:

const { proceed, reset, status } = useBlocker({
  shouldBlockFn: ({ current, next }) => {
    if (
      current.routeId === '/editor-1' &&
      next.fullPath === '/foo/$id' &&
      next.params.id === '123' &&
      next.search.hello === 'world'
    ) {
      return true
    }
    return false
  },
  enableBeforeUnload: false,
  withResolver: true,
})
  • What it demonstrates: useBlocker's shouldBlockFn receives { current, next } match info to decide per-navigation whether to block, withResolver: true returns proceed/reset/status for building a custom confirmation UI, and enableBeforeUnload (boolean or function) separately controls whether the native browser tab-close/refresh prompt is also armed.

Key Takeaways

  1. Use a plain boolean/state-derived shouldBlockFn (() => value !== '') for "block while this component has unsaved changes"; use the { current, next } form for targeting specific route transitions regardless of local state.
  2. enableBeforeUnload is independent of shouldBlockFn and should usually mirror the same "dirty" condition so tab close/refresh is guarded consistently with in-app navigation.
  3. withResolver: true gives you status === 'blocked', proceed(), and reset() to render your own confirmation dialog inline, rather than relying on a native confirm().
  4. Multiple useBlocker calls can coexist (component-level and root-level in this example); each evaluates independently against the attempted navigation.

Connects To

  • Authenticated Routes (ch017): both patterns use beforeLoad/hook-level interception to redirect or halt navigation before a route commits, though blocking is user-initiated cancellation versus auth's automatic redirect.