Cheatsheet

Cheatsheet — TanStack Router

Decision rules

  • Starting a new project → scaffold with @tanstack/cli create --router-only, don't hand-roll. It wires TypeScript, Tailwind, and file/code-based routing choice interactively.
  • Choosing routing style → default to file-based; only use code-based if the filesystem genuinely can't express the route tree you need. File-based needs less boilerplate for equivalent functionality and gets automatic code-splitting for free.
  • Naming a route file → use the routing-concepts tokens, not ad hoc names. $param = dynamic segment, $ alone = splat, _prefix = pathless layout, _suffix = non-nested, -prefix = excluded from routing, (group) = organizational only, {-$param} = optional param, [x] = escape a literal routing-significant character.
  • Two routes could match the same URL → trust automatic precedence, don't reorder files to "fix" it. Order is always index > static (most specific first) > dynamic (longest first) > splat, regardless of declaration order.
  • Fetching route data → use loader, not useEffect in the component. Loaders run in parallel with route matching and support the built-in cache, preloading, and SSR out of the box; component-level fetching causes waterfalls and loading flashes.
  • Loader needs a search param → put it in loaderDeps, never read search directly inside loader. Only include params the loader actually consumes; extras cause unneeded reloads.
  • Need mutation state, optimistic UI, or a shared cache across non-route reads → bring in TanStack Query (or similar), don't stretch the built-in loader cache. The built-in cache is deliberately minimal (no mutation API, no persistence).
  • Loader has a genuinely slow, non-critical fetch → defer it (unawaited promise + <Await>), don't block the whole route with a pendingComponent. Requires Streaming SSR to actually stream server-side; without it, deferral is client-only.
  • Reading/writing URL query state → always go through validateSearch + Route.useSearch(), never raw URLSearchParams. Search params are typed application state, not ad hoc strings; use .catch() fallbacks so malformed URLs degrade gracefully instead of erroring.
  • A search param should always survive/always hide by default across links → use retainSearchParams/stripSearchParams middleware on the route, not manual logic at every <Link>.
  • Reusing a { to, params, search } object across Link/navigate/redirect → wrap it in linkOptions(), not a bare object literal. Bare literals only surface type errors where they're spread into Link, not at the point of definition.
  • Wrapping Link for a UI library or custom styling → use createLink(), not a generic prop-spread wrapper. Preserves full type safety and preload="intent" support.
  • Guarding a route (auth) → throw redirect() from the parent route's beforeLoad, not from the component. Parent beforeLoad always runs before child beforeLoad, so one guard protects the whole subtree; use isRedirect() when catching errors around the check.
  • A resource might not exist (e.g. deleted post) → throw notFound() from loader, not from the component. Keeps loader data correctly typed and avoids UI flicker; reserve CatchNotFound for component-thrown cases only.
  • One consistent not-found page vs preserving parent layout → notFoundMode: 'root' for the former, default 'fuzzy' for the latter.
  • Need to attach fixed metadata to a route (breadcrumb title, "hide navbar" flag) → use staticData, not context/beforeLoad. staticData is synchronous and identical per route; use context only when the value is async or depends on params/auth.
  • After a mutation, loader data is stale → call router.invalidate(); use { sync: true } only when you must wait before proceeding (e.g. before a redirect).
  • Cross-cutting dependency needed in many loaders (data client, auth, feature flags) → inject via router context (createRootRouteWithContext), not module-level imports.
  • A hook needs to run before beforeLoad/loader can see its result → call the hook in a component and pass the value through <RouterProvider context={...}>. Hooks cannot run inside beforeLoad/loader (Rules of Hooks).
  • Optimizing perceived nav speed with minimal effort → set defaultPreload: 'intent'. It's the single highest-value default; use 'viewport' for below-the-fold links, 'render' for near-always-needed routes.
  • Using an external cache alongside preloading → set defaultPreloadStaleTime: 0 so the external library's freshness rules govern reuse instead of the router's own preloadStaleTime.
  • Building a component that forwards to/params/search externally → type it with ValidateLinkOptions/ValidateLinkOptionsArray/etc., not the internal LinkProps type. Using LinkProps directly is a documented TypeScript-performance trap.
  • A shared component can't declare which route it renders under → use strict: false on the hook, don't guess a from.
  • Object literal ordering in createRoute/createFileRoute → always put params/validateSearchcontextbeforeLoadloader in that order. loader before beforeLoad breaks context type inference; enable the create-route-property-order ESLint rule and let --fix handle it.
  • Using typescript-eslint's only-throw-error (type-checked rulesets) → allowlist Redirect and NotFoundError from @tanstack/router-core, don't disable the rule wholesale.
  • A component re-renders too often on unrelated state changes → add select to the router-state hook; if select returns a freshly-computed object, also set structuralSharing: true.
  • Building a modal/overlay that should show a clean parent URL → use route masking (mask on Link/navigate, or routeMasks + createRouteMask()), not a separate non-nested route with manual URL rewriting. Remember masked state resets on reload unless unmaskOnReload: true.
  • Locale-prefixed routes without duplicating the tree → /{-$locale}/about (optional param), not separate route trees per locale.
  • Confirming before an in-app navigation discards unsaved changes → useBlocker()/<Block>; for tab close/refresh → enableBeforeUnload. These are two separate mechanisms, both are needed for full protection.
  • Non-browser/testing/SSR-bootstrap environment → createMemoryHistory({ initialEntries }), not the default browser history.
  • Server can't rewrite all paths to index.htmlcreateHashHistory, not the default browser history.
  • SSR with an external cache (e.g. TanStack Query) → create the client per-request inside createRouter/getRouter(), never at module scope. Module-level singletons leak state across requests.
  • Need deferred/streamed data to actually stream server-side → configure Streaming SSR explicitly. Without it, deferred promises still resolve, but only client-side.
  • Migrating off React Router → uninstall react-router-dom early to let TypeScript surface every remaining call site as a compile error, faster than manual grepping.

Quick defaults reference

SettingDefaultNotes
staleTime0Immediately stale; revalidates in background on reuse
gcTime5 minRetention before cached loader data is GC-eligible
preloadStaleTime30 secSeparate freshness window for preloaded data
preloadGcTime5 minSeparate retention window for preloaded data
defaultPreloadDelay50 msDelay before intent/viewport preload starts
notFoundMode'fuzzy'Nearest route with notFoundComponent, not always root
routeFileIgnorePrefix-Files/folders excluded from route generation
routeToken / indexTokenroute / indexLayout-route / index-route filename markers
quoteStylesingleGenerated route tree formatting
autoCodeSplittingfalse (bundler plugin)Will default to true in v2
unmaskOnReloadfalseMasked URLs persist across reload unless set