[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
---
|
||||
id: react-tanstack-router-patterns
|
||||
title: TanStack Router — Type-safe / loader / search params
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [react, router, tanstack-router, vibe-coding]
|
||||
tech_stack: { language: "TS / React", applicable_to: ["Frontend"] }
|
||||
applied_in: []
|
||||
aliases: [TanStack Router, file-based routing, type-safe params, loader, search validation]
|
||||
---
|
||||
|
||||
# TanStack Router
|
||||
|
||||
> Type-safe React router. **Search params validation, file-based routing, loader, suspense** 일급. React Router 의 type-safe 버전 + Next 의 file-based.
|
||||
|
||||
## 📖 핵심 개념
|
||||
- File-based: `routes/*.tsx` → 자동 tree.
|
||||
- Type-safe link: `<Link to="/users/$id" params={{ id: '1' }} />` 컴파일 검증.
|
||||
- Search params: zod 같은 schema.
|
||||
- Loader: 라우트 진입 전 데이터 fetch.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### Setup
|
||||
```bash
|
||||
yarn add @tanstack/react-router @tanstack/router-plugin
|
||||
```
|
||||
|
||||
```ts
|
||||
// vite.config.ts
|
||||
import { TanStackRouterVite } from '@tanstack/router-plugin/vite';
|
||||
plugins: [TanStackRouterVite()];
|
||||
```
|
||||
|
||||
### Route 정의 (file-based)
|
||||
```tsx
|
||||
// src/routes/__root.tsx
|
||||
import { Outlet, createRootRoute } from '@tanstack/react-router';
|
||||
export const Route = createRootRoute({ component: () => <Outlet /> });
|
||||
|
||||
// src/routes/users.$id.tsx
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const Route = createFileRoute('/users/$id')({
|
||||
params: { parse: (p) => ({ id: z.string().uuid().parse(p.id) }) },
|
||||
validateSearch: z.object({ tab: z.enum(['profile', 'orders']).default('profile') }),
|
||||
loader: async ({ params }) => api.user.get(params.id),
|
||||
component: UserPage,
|
||||
});
|
||||
|
||||
function UserPage() {
|
||||
const { id } = Route.useParams(); // typed
|
||||
const { tab } = Route.useSearch(); // typed
|
||||
const user = Route.useLoaderData(); // typed
|
||||
return <h1>{user.name}</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
### Link (type-safe)
|
||||
```tsx
|
||||
import { Link } from '@tanstack/react-router';
|
||||
|
||||
<Link to="/users/$id" params={{ id: '42' }} search={{ tab: 'orders' }}>
|
||||
Profile
|
||||
</Link>
|
||||
|
||||
// 잘못된 path / params 컴파일 에러
|
||||
```
|
||||
|
||||
### Navigate (programmatic)
|
||||
```tsx
|
||||
const navigate = Route.useNavigate();
|
||||
navigate({ to: '/users/$id', params: { id: '42' } });
|
||||
|
||||
// 또는 search 만 update
|
||||
navigate({ search: (prev) => ({ ...prev, tab: 'orders' }) });
|
||||
```
|
||||
|
||||
### Loader + Suspense
|
||||
```tsx
|
||||
export const Route = createFileRoute('/posts')({
|
||||
loader: async () => api.posts.list(),
|
||||
pendingComponent: () => <Spinner />,
|
||||
errorComponent: ({ error }) => <Error error={error} />,
|
||||
component: PostsPage,
|
||||
});
|
||||
|
||||
function PostsPage() {
|
||||
const posts = Route.useLoaderData();
|
||||
return ...
|
||||
}
|
||||
```
|
||||
|
||||
### TanStack Query 통합 (loader 안)
|
||||
```ts
|
||||
import { queryOptions } from '@tanstack/react-query';
|
||||
|
||||
const userQuery = (id: string) => queryOptions({
|
||||
queryKey: ['user', id],
|
||||
queryFn: () => api.user.get(id),
|
||||
});
|
||||
|
||||
export const Route = createFileRoute('/users/$id')({
|
||||
loader: ({ context, params }) => context.queryClient.ensureQueryData(userQuery(params.id)),
|
||||
component: UserPage,
|
||||
});
|
||||
|
||||
function UserPage() {
|
||||
const { id } = Route.useParams();
|
||||
const { data } = useSuspenseQuery(userQuery(id));
|
||||
return <h1>{data.name}</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
### Search params (typed link)
|
||||
```tsx
|
||||
<Link to="/posts" search={{ q: 'react', page: 2 }}>Posts</Link>
|
||||
|
||||
const { q, page } = Route.useSearch();
|
||||
```
|
||||
|
||||
URL: `/posts?q=react&page=2`.
|
||||
|
||||
### Nested layout
|
||||
```tsx
|
||||
// routes/_app.tsx — layout
|
||||
export const Route = createFileRoute('/_app')({
|
||||
component: () => (
|
||||
<div className="app">
|
||||
<Sidebar />
|
||||
<Outlet />
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
// routes/_app.users.tsx — _app layout 안
|
||||
export const Route = createFileRoute('/_app/users')({...});
|
||||
```
|
||||
|
||||
### Auth (beforeLoad)
|
||||
```ts
|
||||
export const Route = createFileRoute('/admin')({
|
||||
beforeLoad: async ({ context }) => {
|
||||
if (!context.auth.user) throw redirect({ to: '/login' });
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Devtools
|
||||
```tsx
|
||||
import { TanStackRouterDevtools } from '@tanstack/router-devtools';
|
||||
|
||||
<RouterProvider router={router} />
|
||||
<TanStackRouterDevtools router={router} />
|
||||
```
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 상황 | 추천 |
|
||||
|---|---|
|
||||
| Type-safe critical | TanStack Router |
|
||||
| Next.js | Next App Router (자체) |
|
||||
| 단순 SPA | React Router 6 |
|
||||
| 거대 monorepo + 강 types | TanStack Router |
|
||||
| File-based + SSR | Next / Remix |
|
||||
| 빠른 prototype | React Router |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **String path concat**: type 안전 X. params object.
|
||||
- **Search 검증 없음**: 잘못된 URL 이 crash.
|
||||
- **Loader 안에 mutation**: 의도와 다름. action 으로.
|
||||
- **Route 별 `useParams<RouteId>()` 매번 type 인자**: factory 활용.
|
||||
- **beforeLoad 무거운 작업**: 라우트 진입 느림.
|
||||
- **404 / pendingComponent 없음**: 빈 화면.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- File-based + zod search/params + loader + TQ 통합 4종.
|
||||
- Vite plugin 자동 generate.
|
||||
- Devtools 필수 (개발).
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[React_TanStack_Query_Advanced]]
|
||||
- [[React_Router_Patterns]]
|
||||
- [[React_Suspense_for_Data]]
|
||||
Reference in New Issue
Block a user