smithery/blencorp

tanstack-router

TanStack Router file-based routing patterns including route creation, navigation, loaders, type-safe routing, and lazy loading. Use when creating routes, implementing navigation, or working with TanStack Router.

Installation

$ npx skills add smithery/blencorp --skill tanstack-router

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/blencorp.

npx skills add smithery/blencorp

Browse all from smithery/blencorp

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

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 9,640 B
  • docs SUMMARY.md 234 B

History

  1. First recorded snapshot · 0 installs

SKILL.md

TanStack Router Patterns

Purpose

File-based routing with TanStack Router, emphasizing type-safe navigation, route loaders, and lazy loading.

When to Use This Skill

  • Creating new routes
  • Implementing navigation
  • Using route loaders for data
  • Type-safe routing with parameters
  • Lazy loading routes

Quick Start

Basic Route

// routes/posts/index.tsx
import { createFileRoute } from '@tanstack/react-router';
import { postsApi } from '~/features/posts/api/postsApi';

export const Route = createFileRoute('/posts')({
  loader: async () => {
    const posts = await postsApi.getAll();
    return { posts };
  },
  component: PostsPage,
});

function PostsPage() {
  const { posts } = Route.useLoaderData();

  return (
    <div>
      <h1>Posts</h1>
      {posts.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
    </div>
  );
}

File-Based Routing

Directory Structure

routes/
├── __root.tsx          # Root route
├── index.tsx           # /
├── about.tsx           # /about
├── posts/
│   ├── index.tsx       # /posts
│   └── $postId.tsx     # /posts/:postId
└── users/
    ├── index.tsx       # /users
    └── $userId/
        ├── index.tsx   # /users/:userId
        └── posts.tsx   # /users/:userId/posts

Route Mapping

File Path                        → URL Path
routes/index.tsx                 → /
routes/about.tsx                 → /about
routes/posts/index.tsx           → /posts
routes/posts/$postId.tsx         → /posts/:postId
routes/users/$userId/index.tsx   → /users/:userId
routes/users/$userId/posts.tsx   → /users/:userId/posts

Route Parameters

Dynamic Routes

// routes/posts/$postId.tsx
import { createFileRoute } from '@tanstack/react-router';
import { postsApi } from '~/features/posts/api/postsApi';

export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => {
    const post = await postsApi.get(params.postId);
    return { post };
  },
  component: PostDetails,
});

function PostDetails() {
  const { post } = Route.useLoaderData();
  const { postId } = Route.useParams();

  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </div>
  );
}

Multiple Parameters

// routes/users/$userId/posts/$postId.tsx
export const Route = createFileRoute('/users/$userId/posts/$postId')({
  loader: async ({ params }) => {
    const { userId, postId } = params;
    const post = await postsApi.getByUserAndId(userId, postId);
    return { post };
  },
  component: UserPostDetails,
});

Route Loaders

Basic Loader

export const Route = createFileRoute('/posts')({
  loader: async () => {
    const posts = await postsApi.getAll();
    return { posts };
  },
  component: PostsPage,
});

Loader with Dependencies

export const Route = createFileRoute('/users/$userId/posts')({
  loader: async ({ params, context }) => {
    const [user, posts] = await Promise.all([
      userApi.get(params.userId),
      postsApi.getByUser(params.userId),
    ]);
    return { user, posts };
  },
  component: UserPosts,
});

Loader Error Handling

export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => {
    try {
      const post = await postsApi.get(params.postId);
      return { post, error: null };
    } catch (error) {
      return { post: null, error: 'Post not found' };
    }
  },
  component: PostDetails,
});

function PostDetails() {
  const { post, error } = Route.useLoaderData();

  if (error) return <Error message={error} />;
  return <div>{post.title}</div>;
}

Navigation

import { Link, useNavigate } from '@tanstack/react-router';

// Link component
<Link to="/posts/$postId" params={{ postId: '123' }}>View Post</Link>

// Programmatic navigation
const navigate = useNavigate();
navigate({ to: '/posts', search: { filter: 'published' } });

Lazy Loading

Lazy Route Component

// routes/posts/index.tsx
import { createFileRoute } from '@tanstack/react-router';
import { lazy } from 'react';

const PostsPage = lazy(() => import('~/features/posts/PostsPage'));

export const Route = createFileRoute('/posts')({
  component: PostsPage,
});

Lazy Loader

export const Route = createFileRoute('/posts')({
  loader: async () => {
    // Dynamically import heavy module only when route loads
    const { processData } = await import('~/lib/heavyModule');
    const posts = await postsApi.getAll();
    const processed = processData(posts);
    return { posts: processed };
  },
  component: PostsPage,
});

Search Params

Type-Safe Search Params

import { z } from 'zod';

const postsSearchSchema = z.object({
  filter: z.enum(['all', 'published', 'draft']).default('all'),
  sort: z.enum(['date', 'title']).default('date'),
  page: z.number().default(1),
});

export const Route = createFileRoute('/posts')({
  validateSearch: postsSearchSchema,
  loader: async ({ search }) => {
    const posts = await postsApi.getAll(search);
    return { posts };
  },
  component: PostsPage,
});

function PostsPage() {
  const { posts } = Route.useLoaderData();
  const search = Route.useSearch();

  return (
    <div>
      <p>Filter: {search.filter}</p>
      <p>Sort: {search.sort}</p>
      <p>Page: {search.page}</p>
    </div>
  );
}

Updating Search Params

import { useNavigate } from '@tanstack/react-router';

function FilterButtons() {
  const navigate = useNavigate();
  const search = Route.useSearch();

  const setFilter = (filter: string) => {
    navigate({
      to: '.',
      search: (prev) => ({ ...prev, filter }),
    });
  };

  return (
    <div>
      <button onClick={() => setFilter('all')}>All</button>
      <button onClick={() => setFilter('published')}>Published</button>
      <button onClick={() => setFilter('draft')}>Draft</button>
    </div>
  );
}

Layouts

Root Layout

// routes/__root.tsx
import { createRootRoute, Outlet } from '@tanstack/react-router';

export const Route = createRootRoute({
  component: RootLayout,
});

function RootLayout() {
  return (
    <div>
      <Header />
      <main>
        <Outlet />  {/* Child routes render here */}
      </main>
      <Footer />
    </div>
  );
}

Nested Layouts

// routes/dashboard.tsx
export const Route = createFileRoute('/dashboard')({
  component: DashboardLayout,
});

function DashboardLayout() {
  return (
    <div className="dashboard">
      <Sidebar />
      <div className="content">
        <Outlet />  {/* Dashboard child routes */}
      </div>
    </div>
  );
}

// routes/dashboard/index.tsx
export const Route = createFileRoute('/dashboard')({
  component: DashboardHome,
});

// routes/dashboard/analytics.tsx
export const Route = createFileRoute('/dashboard/analytics')({
  component: Analytics,
});

Route Guards

Authentication Guard

export const Route = createFileRoute('/admin')({
  beforeLoad: async ({ context }) => {
    if (!context.auth.isAuthenticated) {
      throw redirect({
        to: '/login',
        search: { redirect: '/admin' },
      });
    }
  },
  component: AdminPage,
});

Permission Guard

export const Route = createFileRoute('/admin/users')({
  beforeLoad: async ({ context }) => {
    if (!context.auth.hasPermission('users:manage')) {
      throw redirect({ to: '/unauthorized' });
    }
  },
  component: UsersPage,
});

Breadcrumbs

Route Breadcrumbs

export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => {
    const post = await postsApi.get(params.postId);
    return { post };
  },
  meta: ({ loaderData }) => [
    { title: 'Home', path: '/' },
    { title: 'Posts', path: '/posts' },
    { title: loaderData.post.title, path: `/posts/${loaderData.post.id}` },
  ],
  component: PostDetails,
});

Best Practices

1. Use Loaders for Data

// ✅ Good: Loader fetches data
export const Route = createFileRoute('/posts')({
  loader: async () => {
    const posts = await postsApi.getAll();
    return { posts };
  },
  component: PostsPage,
});

// ❌ Avoid: Fetching in component
function PostsPage() {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    postsApi.getAll().then(setPosts);
  }, []);

  return <div>...</div>;
}

2. Lazy Load Heavy Routes

// ✅ Good: Lazy load admin panel
const AdminPanel = lazy(() => import('~/features/admin/AdminPanel'));

export const Route = createFileRoute('/admin')({
  component: AdminPanel,
});

3. Type-Safe Navigation

// ✅ Good: Type-safe Link
<Link to="/posts/$postId" params={{ postId: post.id }}>
  View Post
</Link>

// ❌ Avoid: String concatenation
<a href={`/posts/${post.id}`}>View Post</a>

Additional Resources

For more patterns, see:

  • [routing-guide.md](resources/routing-guide.md) - Advanced routing
  • [navigation-patterns.md](resources/navigation-patterns.md) - Navigation strategies
  • [route-loaders.md](resources/route-loaders.md) - Complex loaders