smithery/redpanda-data

tanstack-router-migration

Migrate React applications from React Router to TanStack Router with file-based routing.

Installation

$ npx skills add smithery/redpanda-data --skill tanstack-router-migration

Summary

  • Migrate React applications from React Router to TanStack Router with file-based routing.
  • Use when user requests: (1) Router migration, (2) TanStack Router setup, (3) File-based routing implementation, (4) React Router replacement, (5) Type-safe routing, or mentions 'migrate router', 'tanstack router', 'file-based routes'.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery/redpanda-data.

npx skills add smithery/redpanda-data

Browse all from smithery/redpanda-data

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Skill metadata

Parsed from SKILL.md frontmatter.

Allowed toolsRead, Write, Edit, Bash, Glob, Grep, Task

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 16,765 B
  • docs SUMMARY.md 356 B

History

  1. First recorded snapshot · 0 installs

SKILL.md

React Router to TanStack Router Migration

Migrate React applications from React Router to TanStack Router with file-based routing. This skill provides a structured approach for both incremental and clean migrations.

Critical Rules

ALWAYS:

  • Use file-based routing with routes in src/routes/ directory
  • Use from parameter in all hooks for type safety (useParams({ from: '/path' }))
  • Validate search params with Zod schemas using @tanstack/zod-adapter
  • Configure build tool plugin before creating routes
  • Register router type for full TypeScript inference
  • Use fallback() wrapper for optional search params

NEVER:

  • Edit routeTree.gen.ts (auto-generated file)
  • Use React Router hooks in new code during migration
  • Forget the from parameter (loses type safety)
  • Use string-only validation for search params
  • Skip the build plugin configuration

Dependencies

# Core dependencies
bun add @tanstack/react-router @tanstack/zod-adapter

# Build plugin (choose one based on your bundler)
bun add -d @tanstack/router-plugin

# Optional integrations
bun add nuqs                    # URL state management
bun add @sentry/react           # Error tracking with router integration

Migration Phases

Phase 1: Assessment

Audit existing React Router usage:

# Find all React Router imports
grep -r "from 'react-router" src/ --include="*.tsx" --include="*.ts"
grep -r 'from "react-router' src/ --include="*.tsx" --include="*.ts"

# Find hook usages
grep -r "useParams\|useSearchParams\|useNavigate\|useLocation\|useMatch" src/

Document:

  • React Router version (v5 or v6)
  • Number of routes
  • useParams usage count
  • useSearchParams usage count
  • useNavigate usage count
  • Custom Link components
  • Route guards/protected routes
  • Existing route structure

Phase 2: Setup

1. Configure Build Tool

See [references/build-configuration.md](references/build-configuration.md) for full configs.

Rspack/Rsbuild:

// rsbuild.config.ts
import { TanStackRouterRspack } from '@tanstack/router-plugin/rspack';

export default {
  tools: {
    rspack: (config) => {
      config.plugins?.push(
        TanStackRouterRspack({
          target: 'react',
          autoCodeSplitting: true,
          routesDirectory: './src/routes',
          generatedRouteTree: './src/routeTree.gen.ts',
          quoteStyle: 'single',
          semicolons: true,
        })
      );
      // Prevent rebuild loop
      config.watchOptions = { ignored: ['**/routeTree.gen.ts'] };
      return config;
    },
  },
};

Vite:

// vite.config.ts
import { TanStackRouterVite } from '@tanstack/router-plugin/vite';

export default defineConfig({
  plugins: [
    TanStackRouterVite({
      target: 'react',
      autoCodeSplitting: true,
      routesDirectory: './src/routes',
      generatedRouteTree: './src/routeTree.gen.ts',
    }),
    react(),
  ],
});

2. Configure Linter

// biome.jsonc or eslint config
{
  "files": {
    "ignore": ["**/routeTree.gen.ts"]
  },
  "overrides": [
    {
      "include": ["**/routes/**/*"],
      "linter": {
        "rules": {
          "style": {
            "useFilenamingConvention": "off"  // Allow $param.tsx naming
          }
        }
      }
    }
  ]
}

3. Create Routes Directory

mkdir -p src/routes

Phase 3: Router Creation

Create Router Instance:

// src/app.tsx
import { createRouter, RouterProvider } from '@tanstack/react-router';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { routeTree } from './routeTree.gen';
import { NotFoundPage } from './components/misc/not-found-page';

const queryClient = new QueryClient();

const router = createRouter({
  routeTree,
  context: {
    basePath: getBasePath(),
    queryClient,
  },
  basepath: getBasePath(),
  trailingSlash: 'never',
  defaultNotFoundComponent: NotFoundPage,
});

// Register router type for full TypeScript inference
declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router;
  }

  // Extend HistoryState for typed navigation state
  interface HistoryState {
    // Add your custom state properties here
    returnUrl?: string;
    documentId?: string;
    documentName?: string;
  }
}

export function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <RouterProvider router={router} />
    </QueryClientProvider>
  );
}

Define Router Context Type:

// src/routes/__root.tsx
import type { QueryClient } from '@tanstack/react-query';

export type RouterContext = {
  basePath: string;
  queryClient: QueryClient;
};

Phase 4: Route Migration

Create Root Layout:

// src/routes/__root.tsx
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router';
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools';
import type { QueryClient } from '@tanstack/react-query';
import { NuqsAdapter } from 'nuqs/adapters/tanstack-router';

export type RouterContext = {
  basePath: string;
  queryClient: QueryClient;
};

export const Route = createRootRouteWithContext<RouterContext>()({
  component: RootLayout,
});

function RootLayout() {
  return (
    <>
      <NuqsAdapter>
        <ErrorBoundary>
          <AppLayout>
            <Outlet />
          </AppLayout>
        </ErrorBoundary>
      </NuqsAdapter>
      {process.env.NODE_ENV === 'development' && (
        <TanStackRouterDevtools position="bottom-right" />
      )}
    </>
  );
}

File-Based Route Structure:

src/routes/
├── __root.tsx                    # Root layout
├── index.tsx                     # / (root redirect)
├── overview/
│   └── index.tsx                 # /overview
├── topics/
│   ├── index.tsx                 # /topics
│   └── $topicName/
│       ├── index.tsx             # /topics/:topicName
│       └── edit.tsx              # /topics/:topicName/edit
├── security/
│   ├── index.tsx                 # /security (redirect)
│   ├── acls/
│   │   ├── index.tsx             # /security/acls
│   │   ├── create.tsx            # /security/acls/create
│   │   └── $aclName/
│   │       └── details.tsx       # /security/acls/:aclName/details

See [references/route-templates.md](references/route-templates.md) for complete templates.

Phase 5: Hook Migration

React Router TanStack Router
useParams() useParams({ from: '/path/$param' })
useSearchParams() routeApi.useSearch() with Zod validation
useNavigate() useNavigate({ from: '/path' })
useLocation() useLocation() (same API)
<Link to="/path"> <Link to="/path"> (type-safe)
<Navigate to="/path" /> <Navigate to="/path" />

See [references/migration-patterns.md](references/migration-patterns.md) for detailed before/after examples.

Navigation State:

Pass typed state between routes using HistoryState:

// Navigating with state
const navigate = useNavigate();
navigate({
  to: '/documents/$documentId',
  params: { documentId },
  state: {
    returnUrl: location.pathname,
    documentName: 'My Document',
  },
});

// Reading state in destination component
import { useLocation } from '@tanstack/react-router';

function DocumentPage() {
  const location = useLocation();
  const { returnUrl, documentName } = location.state;
  // Use state values...
}

useParams Migration:

// Before (React Router)
import { useParams } from 'react-router-dom';
const { id } = useParams<{ id: string }>();

// After (TanStack Router)
import { useParams } from '@tanstack/react-router';
const { id } = useParams({ from: '/items/$id' });

useSearch with Zod Validation:

// In route file
import { fallback, zodValidator } from '@tanstack/zod-adapter';
import { z } from 'zod';

const searchSchema = z.object({
  tab: fallback(z.string().optional(), undefined),
  page: fallback(z.number().optional(), 1),
  q: fallback(z.string().optional(), undefined),
});

export const Route = createFileRoute('/items/')({
  validateSearch: zodValidator(searchSchema),
  component: ItemsPage,
});

// In component
import { getRouteApi, useNavigate } from '@tanstack/react-router';

const routeApi = getRouteApi('/items/');

function ItemsPage() {
  const { tab, page, q } = routeApi.useSearch();
  const navigate = useNavigate({ from: '/items/' });

  const handleTabChange = (newTab: string) => {
    navigate({ search: (prev) => ({ ...prev, tab: newTab }) });
  };
}

Phase 6: Testing

Create Test Utilities:

// src/test-utils.tsx
import { createMemoryHistory, createRouter, RouterProvider } from '@tanstack/react-router';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render, type RenderOptions } from '@testing-library/react';
import { routeTree } from './routeTree.gen';
import type { RouterContext } from './routes/__root';

interface RenderWithFileRoutesOptions extends Omit<RenderOptions, 'wrapper'> {
  initialLocation?: string;
  routerContext?: Partial<RouterContext>;
}

export function renderWithFileRoutes(
  ui: React.ReactElement | null = null,
  { initialLocation = '/', routerContext = {}, ...renderOptions }: RenderWithFileRoutesOptions = {}
) {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  });

  const router = createRouter({
    routeTree,
    history: createMemoryHistory({ initialEntries: [initialLocation] }),
    context: { basePath: '', queryClient, ...routerContext },
  });

  function Wrapper({ children }: { children: React.ReactNode }) {
    return (
      <QueryClientProvider client={queryClient}>
        <RouterProvider router={router}>{children}</RouterProvider>
      </QueryClientProvider>
    );
  }

  return {
    ...render(ui ?? <div />, { wrapper: Wrapper, ...renderOptions }),
    router,
  };
}

export async function renderRoute(location: string, options?: RenderWithFileRoutesOptions) {
  const result = renderWithFileRoutes(null, { initialLocation: location, ...options });
  await result.router.load();
  return result;
}

Configure Rstest:

// rstest.config.integration.ts
import { pluginReact } from '@rsbuild/plugin-react';
import { defineProject } from '@rstest/core';

export default defineProject({
  plugins: [pluginReact()],
  include: ['src/**/*.test.tsx'],
  testEnvironment: {
    name: 'happy-dom',
    prebundle: 'auto',
  },
  setupFiles: './rstest.setup.ts',
});

Rstest consumes the generated route tree. Keep route generation in rsbuild.config.ts.

Phase 7: Integrations

Sentry Integration:

// src/app.tsx
import * as Sentry from '@sentry/react';

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  integrations: [
    Sentry.tanstackRouterBrowserTracingIntegration(router),
  ],
  tracesSampleRate: 1.0,
});

nuqs Integration:

// src/routes/__root.tsx
import { NuqsAdapter } from 'nuqs/adapters/tanstack-router';

function RootLayout() {
  return (
    <NuqsAdapter>
      <Outlet />
    </NuqsAdapter>
  );
}

Incremental Migration (Legacy Compatibility):

See [references/incremental-migration.md](references/incremental-migration.md) for patterns to run both routers together during migration.

Quick Reference

Route File Naming

Pattern File URL
Index route topics/index.tsx /topics
Dynamic param topics/$topicName.tsx /topics/:topicName
Nested dynamic topics/$topicName/edit.tsx /topics/:topicName/edit
Pathless layout _layout.tsx (no URL segment)
Catch-all $.tsx /*

Common Zod Patterns

import { fallback, zodValidator } from '@tanstack/zod-adapter';
import { z } from 'zod';

const searchSchema = z.object({
  // Optional string with undefined default
  tab: fallback(z.string().optional(), undefined),

  // Optional number with default value
  page: fallback(z.number().optional(), 1),

  // Required string
  id: z.string(),

  // Enum with default
  sort: fallback(z.enum(['asc', 'desc']).optional(), 'asc'),

  // Boolean
  expanded: fallback(z.boolean().optional(), false),
});

Trailing Slash in from Parameter

The from parameter must exactly match the route path as defined:

// Index routes (files named index.tsx) include trailing slash:
useParams({ from: '/topics/$topicName/' })  // Route: topics/$topicName/index.tsx

// Non-index routes do NOT include trailing slash:
useParams({ from: '/topics/$topicName/edit' })  // Route: topics/$topicName/edit.tsx

Type-Safe Navigation

// With params
<Link to="/topics/$topicName" params={{ topicName: 'my-topic' }}>
  View Topic
</Link>

// With search params
<Link to="/topics" search={{ page: 2, sort: 'desc' }}>
  Page 2
</Link>

// Programmatic navigation
const navigate = useNavigate({ from: '/topics/$topicName' });
navigate({
  to: '/topics/$topicName/edit',
  params: { topicName },
  search: { tab: 'settings' },
});

Checklist

Pre-Migration

  • Dependencies installed (@tanstack/react-router, @tanstack/router-plugin, @tanstack/zod-adapter)
  • Build tool plugin configured
  • Linter configured to allow $param.tsx naming
  • src/routes/ directory created

Route Migration

  • __root.tsx created with providers and layout
  • index.tsx created for root redirect
  • All routes migrated to file-based structure
  • Search params validated with Zod schemas
  • staticData added for titles/icons

Hook Migration

  • All useParams calls updated with from parameter
  • All useSearchParams replaced with routeApi.useSearch()
  • All useNavigate calls updated with from parameter
  • All Link components verified working

Testing

  • renderWithFileRoutes utility created
  • Rstest integration project configured for route tests
  • Existing tests updated to use new utilities

Integrations

  • Sentry integration configured (if used)
  • nuqs adapter wrapped in root layout (if used)

Cleanup (after full migration)

  • React Router dependencies removed
  • Legacy route definitions deleted
  • BrowserRouter wrapper removed
  • RouterSync component removed

Common Pitfalls

  1. Missing from parameter - Always specify from in hooks for type safety
  2. Forgetting fallback() wrapper - Optional search params need fallback(z.string().optional(), undefined)
  3. Trailing slash inconsistency - Configure trailingSlash: 'never' and be consistent
  4. Editing routeTree.gen.ts - Never edit; it's auto-generated on file changes
  5. Missing build plugin - Routes won't generate without the bundler plugin
  6. Async navigation warnings - navigate() returns Promise; use void navigate() or await it
  7. Using <Navigate> for section redirects - Use beforeLoad with throw redirect() instead to prevent navigation loops in embedded mode:

``typescript beforeLoad: () => { throw redirect({ to: '/section/$tab', params: { tab: 'default' }, replace: true }); } ``

  1. Trailing slash in from parameter for index routes - Index routes (files named index.tsx) require trailing slash in from:

``typescript // Index route: /topics/$topicName/index.tsx useParams({ from: '/topics/$topicName/' }) // ✅ Correct (trailing slash) useParams({ from: '/topics/$topicName' }) // ❌ Wrong ``

  1. Missing HistoryState extension - Extend HistoryState interface for typed navigation state (see Phase 3)

Documentation