TanStack Router Enforcement
What This Catches
- Ban
react-router-domimports - Ban
window.locationnavigation (block) + reads (warn) - Warn
window.location.reload()-- suggestrouter.invalidate() - Ban
strict: falsein router hooks - Ban untyped
useParams()/useSearch()/useLoaderData()/useRouteContext()missing{ from } - Ban
URLSearchParams-- suggest nuqs - Warn exported components from route files (break code splitting)
- Require
validateSearchwhenuseSearchin route files - Warn when Query-primed loaders are consumed via
useLoaderDatainstead ofuseQuery/useSuspenseQuery - Warn when router uses
queryClientcontext withoutdefaultPreloadStaleTime: 0 - Warn when router uses
queryClientcontext withoutcreateRootRouteWithContext
Auto-regen route tree on route file change.
TanStack Router + Query
When a route needs server data, prefer this ownership split:
- Router loader: start fetching early after navigation intent.
- TanStack Query: own cache, refetch, invalidation, and garbage collection.
- Component: read via
useQuery()oruseSuspenseQuery()so Query has an active observer.
Do not enforce suspense globally. Choose per field/page:
useSuspenseQuery()for blocking, page-critical data that should use route pending/error boundaries.useQuery()for deferred or secondary data with inline loading/empty/error states.
export const Route = createFileRoute('/dashboards/$dashboardId')({
loader: ({ context, params }) => {
context.queryClient.prefetchQuery(dashboardQueryOptions(params.dashboardId))
},
component: Dashboard,
})
function Dashboard() {
const params = Route.useParams()
const dashboard = useSuspenseQuery(dashboardQueryOptions(params.dashboardId))
const widgetCount = useQuery(widgetCountQueryOptions(params.dashboardId))
return <DashboardView dashboard={dashboard.data} widgetCount={widgetCount.data} />
}
Router setup when Query owns cache:
const rootRoute = createRootRouteWithContext<{ queryClient: QueryClient }>()({
component: RootLayout,
})
const router = createRouter({
routeTree,
context: { queryClient },
defaultPreloadStaleTime: 0,
defaultPendingComponent: DefaultLoader,
defaultErrorComponent: DefaultError,
})
Avoid Route.useLoaderData() for Query-loaded data. It bypasses Query observers, so focus refetch, invalidation refetch, and cache retention can behave surprisingly.
Customization
Routes dir pattern default /routes/. Update grep pattern in hook scripts if project use different convention:
if ! echo "$file_path" | grep -qE '/routes/'; then # default
if ! echo "$file_path" | grep -qE '/pages/'; then # pages-based
if ! echo "$file_path" | grep -qE '/app/routes/'; then # nested
Type-Safe Search Params with nuqs
import { useQueryState, parseAsInteger, parseAsString } from 'nuqs'
function UsersPage() {
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1))
const [filter, setFilter] = useQueryState('filter', parseAsString)
}
Setup (install, config, verify): see SETUP.md.