ghcopilot-hub 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +176 -0
- package/hub/agents/README.md +243 -0
- package/hub/agents/archiver.agent.md +231 -0
- package/hub/agents/explore.agent.md +49 -0
- package/hub/agents/implementador.agent.md +176 -0
- package/hub/agents/librarian.agent.md +34 -0
- package/hub/agents/momus.agent.md +130 -0
- package/hub/agents/oracle.agent.md +52 -0
- package/hub/agents/plan-guardian.agent.md +109 -0
- package/hub/agents/planificador.agent.md +295 -0
- package/hub/agents/test-sentinel.agent.md +106 -0
- package/hub/base/.github/copilot-instructions.md +10 -0
- package/hub/base/.github/instructions/ghcopilot-hub.instructions.md +6 -0
- package/hub/base/.github/prompts/ghcopilot-hub-maintenance.prompt.md +8 -0
- package/hub/base/.vscode/settings.json +1 -0
- package/hub/packs/base-web.json +4 -0
- package/hub/packs/nextjs-ssr.json +4 -0
- package/hub/packs/node-api.json +4 -0
- package/hub/packs/spa-tanstack.json +4 -0
- package/hub/skills/architecture-testing/SKILL.md +108 -0
- package/hub/skills/architecture-testing/references/archunitts.md +46 -0
- package/hub/skills/ghcopilot-hub-consumer/SKILL.md +115 -0
- package/hub/skills/ghcopilot-hub-consumer/references/workflow.md +39 -0
- package/hub/skills/mermaid-expert/SKILL.md +152 -0
- package/hub/skills/mermaid-expert/assets/examples/c4_model.md +121 -0
- package/hub/skills/mermaid-expert/assets/examples/flowchart.md +123 -0
- package/hub/skills/mermaid-expert/assets/examples/img/base_minimal.png +0 -0
- package/hub/skills/mermaid-expert/assets/examples/img/corporate.png +0 -0
- package/hub/skills/mermaid-expert/assets/examples/img/dark.png +0 -0
- package/hub/skills/mermaid-expert/assets/examples/img/dark_neo.png +0 -0
- package/hub/skills/mermaid-expert/assets/examples/img/default_neo.png +0 -0
- package/hub/skills/mermaid-expert/assets/examples/img/forest_corp.png +0 -0
- package/hub/skills/mermaid-expert/assets/examples/img/handdrawn.png +0 -0
- package/hub/skills/mermaid-expert/assets/examples/img/neo.png +0 -0
- package/hub/skills/mermaid-expert/assets/examples/img/neutral_sketch.png +0 -0
- package/hub/skills/mermaid-expert/assets/examples/img/retro.png +0 -0
- package/hub/skills/mermaid-expert/assets/examples/sequence.md +116 -0
- package/hub/skills/mermaid-expert/assets/examples/styles_and_looks.md +102 -0
- package/hub/skills/mermaid-expert/assets/examples/technical.md +130 -0
- package/hub/skills/mermaid-expert/assets/examples.md +57 -0
- package/hub/skills/mermaid-expert/references/cheatsheet.md +88 -0
- package/hub/skills/mermaid-expert/references/validation.md +66 -0
- package/hub/skills/react/SKILL.md +235 -0
- package/hub/skills/react/references/common-mistakes.md +518 -0
- package/hub/skills/react/references/composition-patterns.md +526 -0
- package/hub/skills/react/references/effects-patterns.md +396 -0
- package/hub/skills/react/references/react-compiler.md +268 -0
- package/hub/skills/react-hook-form/SKILL.md +291 -0
- package/hub/skills/react-hook-form/references/field-arrays.md +98 -0
- package/hub/skills/react-hook-form/references/integration.md +102 -0
- package/hub/skills/react-hook-form/references/performance.md +96 -0
- package/hub/skills/skill-creator/SKILL.md +152 -0
- package/hub/skills/skill-creator/assets/SKILL-TEMPLATE.md +84 -0
- package/hub/skills/skill-judge/README.md +261 -0
- package/hub/skills/skill-judge/SKILL.md +806 -0
- package/hub/skills/tailwind/SKILL.md +200 -0
- package/hub/skills/tanstack/SKILL.md +284 -0
- package/hub/skills/tanstack/references/loader-adapter-examples.md +79 -0
- package/hub/skills/tanstack/references/query-options-examples.md +115 -0
- package/hub/skills/tanstack/references/resilience-patterns.md +110 -0
- package/hub/skills/tanstack/references/suspense-consumption-examples.md +82 -0
- package/hub/skills/tanstack-query/SKILL.md +241 -0
- package/hub/skills/tanstack-query/references/advanced-hooks.md +126 -0
- package/hub/skills/tanstack-query/references/best-practices.md +241 -0
- package/hub/skills/tanstack-query/references/cache-strategies.md +474 -0
- package/hub/skills/tanstack-query/references/common-patterns.md +239 -0
- package/hub/skills/tanstack-query/references/migration-v5.md +93 -0
- package/hub/skills/tanstack-query/references/resilience-and-mutations.md +63 -0
- package/hub/skills/tanstack-query/references/testing.md +116 -0
- package/hub/skills/tanstack-query/references/top-errors.md +148 -0
- package/hub/skills/tanstack-query/references/typescript.md +176 -0
- package/hub/skills/tanstack-router/SKILL.md +145 -0
- package/hub/skills/tanstack-router/references/code-splitting.md +31 -0
- package/hub/skills/tanstack-router/references/errors-and-boundaries.md +44 -0
- package/hub/skills/tanstack-router/references/loaders-and-preload.md +51 -0
- package/hub/skills/tanstack-router/references/navigation.md +24 -0
- package/hub/skills/tanstack-router/references/private-routes.md +169 -0
- package/hub/skills/tanstack-router/references/router-context.md +35 -0
- package/hub/skills/tanstack-router/references/search-params.md +29 -0
- package/hub/skills/tanstack-router/references/typescript.md +24 -0
- package/hub/skills/testing/SKILL.md +187 -0
- package/hub/skills/testing/references/assertions.md +64 -0
- package/hub/skills/testing/references/async-testing.md +66 -0
- package/hub/skills/testing/references/e2e-strategy.md +69 -0
- package/hub/skills/testing/references/layer-matrix.md +67 -0
- package/hub/skills/testing/references/performance.md +49 -0
- package/hub/skills/testing/references/tooling-map.md +81 -0
- package/hub/skills/testing/references/zustand-mocking.md +84 -0
- package/hub/skills/typescript/SKILL.md +232 -0
- package/hub/skills/typescript/references/perf-additional-concerns.md +248 -0
- package/hub/skills/typescript/references/perf-execution-cache-locality.md +178 -0
- package/hub/skills/typescript/references/reduce-branching.md +147 -0
- package/hub/skills/typescript/references/reduce-looping.md +203 -0
- package/hub/skills/typescript/references/style-and-types.md +171 -0
- package/hub/skills/typescript/references/type-vs-interface.md +27 -0
- package/hub/skills/zod/SKILL.md +219 -0
- package/hub/skills/zustand/SKILL.md +273 -0
- package/package.json +59 -0
- package/tooling/cli/src/bin.js +11 -0
- package/tooling/cli/src/cli.js +409 -0
- package/tooling/cli/src/lib/catalog-loader.js +191 -0
- package/tooling/cli/src/lib/constants.js +39 -0
- package/tooling/cli/src/lib/errors.js +8 -0
- package/tooling/cli/src/lib/frontmatter.js +41 -0
- package/tooling/cli/src/lib/fs-utils.js +77 -0
- package/tooling/cli/src/lib/managed-header.js +74 -0
- package/tooling/cli/src/lib/manifest.js +105 -0
- package/tooling/cli/src/lib/resolver.js +53 -0
- package/tooling/cli/src/lib/sync-engine.js +262 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# TypeScript Patterns for TanStack Query
|
|
2
|
+
|
|
3
|
+
**Type-safe query and mutation patterns**
|
|
4
|
+
|
|
5
|
+
> Alignment note: define `queryOptions` in the Application layer and reuse them in Presentation.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Basic Type Inference
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
type Todo = {
|
|
13
|
+
id: number;
|
|
14
|
+
title: string;
|
|
15
|
+
completed: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const { data } = useQuery({
|
|
19
|
+
queryKey: ["todos"],
|
|
20
|
+
queryFn: async (): Promise<Todo[]> => {
|
|
21
|
+
const response = await fetch("/api/todos");
|
|
22
|
+
return response.json();
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 2. Generic Query Hook
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
function useEntity<T>(endpoint: string, id: number) {
|
|
33
|
+
return useQuery({
|
|
34
|
+
queryKey: [endpoint, id],
|
|
35
|
+
queryFn: async (): Promise<T> => {
|
|
36
|
+
const response = await fetch(`/api/${endpoint}/${id}`);
|
|
37
|
+
return response.json();
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 3. queryOptions with Type Safety
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
export const todosQueryOptions = queryOptions({
|
|
49
|
+
queryKey: ["todos"],
|
|
50
|
+
queryFn: async (): Promise<Todo[]> => {
|
|
51
|
+
const response = await fetch("/api/todos");
|
|
52
|
+
return response.json();
|
|
53
|
+
},
|
|
54
|
+
staleTime: 1000 * 60,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
useQuery(todosQueryOptions);
|
|
58
|
+
useSuspenseQuery(todosQueryOptions);
|
|
59
|
+
queryClient.prefetchQuery(todosQueryOptions);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 4. Mutation with Types
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
type CreateTodoInput = { title: string };
|
|
68
|
+
|
|
69
|
+
type CreateTodoResponse = Todo;
|
|
70
|
+
|
|
71
|
+
const { mutate } = useMutation<CreateTodoResponse, Error, CreateTodoInput, { previous?: Todo[] }>({
|
|
72
|
+
mutationFn: async (input) => {
|
|
73
|
+
const response = await fetch("/api/todos", {
|
|
74
|
+
method: "POST",
|
|
75
|
+
body: JSON.stringify(input),
|
|
76
|
+
});
|
|
77
|
+
return response.json();
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 5. Custom Error Types
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
class ApiError extends Error {
|
|
88
|
+
constructor(
|
|
89
|
+
message: string,
|
|
90
|
+
public status: number,
|
|
91
|
+
public code: string
|
|
92
|
+
) {
|
|
93
|
+
super(message);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const { error } = useQuery<Todo[], ApiError>({
|
|
98
|
+
queryKey: ["todos"],
|
|
99
|
+
queryFn: async () => {
|
|
100
|
+
const response = await fetch("/api/todos");
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
throw new ApiError("Failed to fetch", response.status, "FETCH_ERROR");
|
|
103
|
+
}
|
|
104
|
+
return response.json();
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 6. Zod Schema Validation
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
import { z } from "zod";
|
|
115
|
+
|
|
116
|
+
const TodoSchema = z.object({
|
|
117
|
+
id: z.number(),
|
|
118
|
+
title: z.string(),
|
|
119
|
+
completed: z.boolean(),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
type Todo = z.infer<typeof TodoSchema>;
|
|
123
|
+
|
|
124
|
+
const { data } = useQuery({
|
|
125
|
+
queryKey: ["todos"],
|
|
126
|
+
queryFn: async () => {
|
|
127
|
+
const response = await fetch("/api/todos");
|
|
128
|
+
const json = await response.json();
|
|
129
|
+
return TodoSchema.array().parse(json);
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 7. Type-Safe Query Keys
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
const queryKeys = {
|
|
140
|
+
todos: {
|
|
141
|
+
all: ["todos"] as const,
|
|
142
|
+
lists: () => [...queryKeys.todos.all, "list"] as const,
|
|
143
|
+
list: (filters: TodoFilters) => [...queryKeys.todos.lists(), filters] as const,
|
|
144
|
+
details: () => [...queryKeys.todos.all, "detail"] as const,
|
|
145
|
+
detail: (id: number) => [...queryKeys.todos.details(), id] as const,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
useQuery({
|
|
150
|
+
queryKey: queryKeys.todos.detail(1),
|
|
151
|
+
queryFn: () => fetchTodo(1),
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## 8. Strict Null Checks
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
const { data } = useQuery({
|
|
161
|
+
queryKey: ["todo", id],
|
|
162
|
+
queryFn: () => fetchTodo(id),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const title = data?.title ?? "No title";
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Best Practices
|
|
171
|
+
|
|
172
|
+
✅ Always type `queryFn` return values
|
|
173
|
+
✅ Use `as const` for query keys
|
|
174
|
+
✅ Prefer `queryOptions` for reuse
|
|
175
|
+
✅ Use Zod for runtime + compile-time validation
|
|
176
|
+
✅ Keep strict null checks enabled
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tanstack-router
|
|
3
|
+
description: >
|
|
4
|
+
TanStack Router patterns aligned with the project Clean Architecture and the tanstack skill. Trigger: When
|
|
5
|
+
implementing or refactoring routing, loaders, search params, router defaults, route-level error boundaries, and
|
|
6
|
+
recovery flows.
|
|
7
|
+
license: Apache-2.0
|
|
8
|
+
metadata:
|
|
9
|
+
author: jmgomezdev
|
|
10
|
+
version: "1.0"
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## When to Use
|
|
14
|
+
|
|
15
|
+
- Creating or updating TanStack Router routes, loaders, and router configuration.
|
|
16
|
+
- Adding search param validation, preload strategy, or error/not-found handling.
|
|
17
|
+
- Splitting route components with `.lazy.tsx` and `getRouteApi()`.
|
|
18
|
+
- Using advanced routing behaviors like masks or custom search serialization.
|
|
19
|
+
|
|
20
|
+
## Reference Router (Mandatory Loading)
|
|
21
|
+
|
|
22
|
+
Loading protocol:
|
|
23
|
+
|
|
24
|
+
1. Pick one routing scenario first.
|
|
25
|
+
2. MANDATORY: read only the selected reference before implementation.
|
|
26
|
+
3. Do NOT load all references by default.
|
|
27
|
+
4. If blocked, load one extra reference only.
|
|
28
|
+
|
|
29
|
+
| Scenario | MANDATORY Reference | Do NOT Load (unless needed) |
|
|
30
|
+
| ---------------------------------------- | -------------------------------------------------------------------------- | --------------------------- |
|
|
31
|
+
| Loader flow and preload orchestration | [references/loaders-and-preload.md](references/loaders-and-preload.md) | `navigation.md` |
|
|
32
|
+
| Route-level boundary and recovery UX | [references/errors-and-boundaries.md](references/errors-and-boundaries.md) | `code-splitting.md` |
|
|
33
|
+
| Search params parsing and defaults | [references/search-params.md](references/search-params.md) | `private-routes.md` |
|
|
34
|
+
| Context wiring and typed root route | [references/router-context.md](references/router-context.md) | `navigation.md` |
|
|
35
|
+
| Private/public redirects and auth groups | [references/private-routes.md](references/private-routes.md) | `search-params.md` |
|
|
36
|
+
| Lazy route modules and route API typing | [references/code-splitting.md](references/code-splitting.md) | `private-routes.md` |
|
|
37
|
+
| Navigation, links, masks | [references/navigation.md](references/navigation.md) | `router-context.md` |
|
|
38
|
+
|
|
39
|
+
## Critical Patterns
|
|
40
|
+
|
|
41
|
+
- **Follow the tanstack skill** for render-as-you-fetch: Application owns `queryOptions`, Interface uses loaders,
|
|
42
|
+
Presentation consumes with `useSuspenseQuery`.
|
|
43
|
+
- **Interface-only routing:** route files live under `src/interface/router/**` and never call repositories directly.
|
|
44
|
+
- **Use typed router context:** create root with `createRootRouteWithContext` and inject `queryClient` (and auth if
|
|
45
|
+
needed).
|
|
46
|
+
- **Loaders use `queryClient.ensureQueryData`**, never `prefetchQuery`, and never fetch directly in components.
|
|
47
|
+
- **Always validate search params** with `validateSearch` and `zod` (defaults via `.catch`).
|
|
48
|
+
- **Register the router type** once so `useNavigate`, `Link`, and hooks infer valid routes.
|
|
49
|
+
- **Prefer `<Link>` over `useNavigate`** for normal navigation (accessibility + preloading).
|
|
50
|
+
- **Use `from` / `Route.fullPath` / `getRouteApi()`** for strict type narrowing in components.
|
|
51
|
+
- **Use parallel loaders** (`Promise.all`) to avoid waterfalls.
|
|
52
|
+
- **Use `.lazy.tsx`** for heavy UI components; keep config in the main route file.
|
|
53
|
+
- **Configure not-found handling** with `notFoundComponent` or `defaultNotFoundComponent`.
|
|
54
|
+
- **Route read failures must bubble** to `errorComponent`; never swallow loader failures.
|
|
55
|
+
- **Classify errors at route boundary**: not-found UX separate from generic server/infrastructure errors.
|
|
56
|
+
- **Retry from route boundaries** with `router.invalidate()` (or equivalent route reset), not full reload.
|
|
57
|
+
- **Preload intent by default** and set `defaultPreloadStaleTime: 0` when using TanStack Query.
|
|
58
|
+
- **Private routes:** use pathless route groups with `beforeLoad` redirects and auth context. See
|
|
59
|
+
`references/private-routes.md`.
|
|
60
|
+
|
|
61
|
+
## Code Examples
|
|
62
|
+
|
|
63
|
+
### Loader + Query cache (minimal)
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
67
|
+
|
|
68
|
+
import { productQueries } from "@/application/product/product.queries";
|
|
69
|
+
|
|
70
|
+
export const Route = createFileRoute("/products/$productId")({
|
|
71
|
+
loader: async ({ params, context: { queryClient } }) => {
|
|
72
|
+
await queryClient.ensureQueryData(productQueries.detail(params.productId));
|
|
73
|
+
},
|
|
74
|
+
errorComponent: ProductDetailErrorBoundary,
|
|
75
|
+
component: ProductDetailPage,
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Search validation with Zod (minimal)
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
83
|
+
import { z } from "zod";
|
|
84
|
+
|
|
85
|
+
const searchSchema = z.object({
|
|
86
|
+
page: z.number().min(1).catch(1),
|
|
87
|
+
sort: z.enum(["name", "price"]).catch("name"),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
export const Route = createFileRoute("/products")({
|
|
91
|
+
validateSearch: (search) => searchSchema.parse(search),
|
|
92
|
+
component: ProductsPage,
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Strict types in split components (minimal)
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
import { getRouteApi } from "@tanstack/react-router";
|
|
100
|
+
|
|
101
|
+
const route = getRouteApi("/products/$productId");
|
|
102
|
+
|
|
103
|
+
export function ProductHeader() {
|
|
104
|
+
const { productId } = route.useParams();
|
|
105
|
+
return <h1>Product {productId}</h1>;
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Commands
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npm run lint
|
|
113
|
+
npm run test
|
|
114
|
+
npm run build
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Use VS Code native search (`#tool:search`) for route boundary checks:
|
|
118
|
+
|
|
119
|
+
- `query: loader:|errorComponent|notFoundComponent` (regex), path: `src/interface/router/routes`
|
|
120
|
+
|
|
121
|
+
## Failure Modes and Fallbacks
|
|
122
|
+
|
|
123
|
+
- Loader throws and UI stays blank: Ensure route defines `errorComponent` and retry path.
|
|
124
|
+
- Not-found and server errors render the same UI: Split boundary handling into typed not-found and generic error
|
|
125
|
+
branches.
|
|
126
|
+
- Navigation recovery uses hard refresh: Replace with router/query invalidation strategy.
|
|
127
|
+
|
|
128
|
+
## Never Do This
|
|
129
|
+
|
|
130
|
+
- Never catch loader errors only to return fake placeholder payloads.
|
|
131
|
+
- Never implement route error handling solely inside page components.
|
|
132
|
+
- Never use `prefetchQuery` for required route data in router flow.
|
|
133
|
+
|
|
134
|
+
## Resources
|
|
135
|
+
|
|
136
|
+
- Base skill: [../tanstack/SKILL.md](../tanstack/SKILL.md)
|
|
137
|
+
- Base skill: [../tanstack-query/SKILL.md](../tanstack-query/SKILL.md)
|
|
138
|
+
- Router context: [references/router-context.md](references/router-context.md)
|
|
139
|
+
- Loaders and preload: [references/loaders-and-preload.md](references/loaders-and-preload.md)
|
|
140
|
+
- Errors and boundaries: [references/errors-and-boundaries.md](references/errors-and-boundaries.md)
|
|
141
|
+
- Search params: [references/search-params.md](references/search-params.md)
|
|
142
|
+
- Navigation: [references/navigation.md](references/navigation.md)
|
|
143
|
+
- Code splitting: [references/code-splitting.md](references/code-splitting.md)
|
|
144
|
+
- TypeScript: [references/typescript.md](references/typescript.md)
|
|
145
|
+
- Private routes: [references/private-routes.md](references/private-routes.md)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Code splitting
|
|
2
|
+
|
|
3
|
+
## Rules
|
|
4
|
+
|
|
5
|
+
- Keep loader/validation in the base route.
|
|
6
|
+
- Lazy-load UI with `createRoute().lazy`.
|
|
7
|
+
- In lazy modules, use `getRouteApi` for typed hooks.
|
|
8
|
+
|
|
9
|
+
## Minimal example
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
export const detailRoute = createRoute({
|
|
13
|
+
getParentRoute: () => rootRoute,
|
|
14
|
+
path: "/products/$productId",
|
|
15
|
+
loader: ({ context: { queryClient }, params }) =>
|
|
16
|
+
queryClient.ensureQueryData(productQueries.detail(params.productId)),
|
|
17
|
+
}).lazy(() => import("./productDetail.lazy").then((m) => m.Route));
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
import { createLazyRoute, getRouteApi } from "@tanstack/react-router";
|
|
22
|
+
|
|
23
|
+
const routeApi = getRouteApi("/products/$productId");
|
|
24
|
+
|
|
25
|
+
export const Route = createLazyRoute("/products/$productId")({
|
|
26
|
+
component: () => {
|
|
27
|
+
const { productId } = routeApi.useParams();
|
|
28
|
+
return <ProductDetailPage productId={productId} />;
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
```
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Errors and boundaries
|
|
2
|
+
|
|
3
|
+
Use this reference when implementing route-level error behavior for loader-driven pages.
|
|
4
|
+
|
|
5
|
+
## Rules
|
|
6
|
+
|
|
7
|
+
- Loader read failures should propagate to route `errorComponent`.
|
|
8
|
+
- Keep not-found UI separate from generic server/network failures.
|
|
9
|
+
- Retry should invalidate router/query state, not hard reload the page.
|
|
10
|
+
- Do not mask loader failures by returning fake fallback payloads.
|
|
11
|
+
|
|
12
|
+
## Minimal route boundary pattern
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { createFileRoute, useRouter } from "@tanstack/react-router";
|
|
16
|
+
|
|
17
|
+
import { productQueries } from "@/application/product/product.queries";
|
|
18
|
+
|
|
19
|
+
const isNotFoundError = (error: unknown) => error instanceof Error && error.message.includes("404");
|
|
20
|
+
|
|
21
|
+
function ProductDetailErrorBoundary({ error }: { error: unknown }) {
|
|
22
|
+
const router = useRouter();
|
|
23
|
+
|
|
24
|
+
if (isNotFoundError(error)) {
|
|
25
|
+
return <ProductNotFound />;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return <RouteErrorPage title="Unable to load product" onRetry={() => router.invalidate()} />;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const Route = createFileRoute("/products/$productId")({
|
|
32
|
+
loader: async ({ params, context: { queryClient } }) => {
|
|
33
|
+
await queryClient.ensureQueryData(productQueries.detail(params.productId));
|
|
34
|
+
},
|
|
35
|
+
errorComponent: ProductDetailErrorBoundary,
|
|
36
|
+
component: ProductDetailPage,
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Common pitfalls
|
|
41
|
+
|
|
42
|
+
- Catching inside loader and returning synthetic data.
|
|
43
|
+
- Rendering route errors inside page components instead of boundaries.
|
|
44
|
+
- Using full page reload for retry.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Loaders and preload
|
|
2
|
+
|
|
3
|
+
## Rules
|
|
4
|
+
|
|
5
|
+
- Required data must be loaded in loaders with `ensureQueryData`.
|
|
6
|
+
- Use `Promise.all` for parallel loading.
|
|
7
|
+
- Use `defaultPreload: 'intent'` and `defaultPreloadStaleTime: 0` with TanStack Query.
|
|
8
|
+
- Keep loader failures visible to route boundaries; do not return fake fallback data from loader catches.
|
|
9
|
+
|
|
10
|
+
## Boundary hand-off
|
|
11
|
+
|
|
12
|
+
When loader data is required for first render, pair loader prefetch with route `errorComponent`.
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
export const detailRoute = createRoute({
|
|
16
|
+
getParentRoute: () => rootRoute,
|
|
17
|
+
path: "/products/$productId",
|
|
18
|
+
loader: async ({ context: { queryClient }, params }) => {
|
|
19
|
+
await queryClient.ensureQueryData(productQueries.detail(params.productId));
|
|
20
|
+
},
|
|
21
|
+
errorComponent: ProductDetailErrorBoundary,
|
|
22
|
+
component: ProductDetailPage,
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Retry should invalidate router/query state (for example `router.invalidate()`), not hard refresh.
|
|
27
|
+
|
|
28
|
+
## Minimal example
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
export const detailRoute = createRoute({
|
|
32
|
+
getParentRoute: () => rootRoute,
|
|
33
|
+
path: "/products/$productId",
|
|
34
|
+
loader: async ({ context: { queryClient }, params }) => {
|
|
35
|
+
await Promise.all([
|
|
36
|
+
queryClient.ensureQueryData(productQueries.detail(params.productId)),
|
|
37
|
+
queryClient.ensureQueryData(productQueries.related(params.productId)),
|
|
38
|
+
]);
|
|
39
|
+
},
|
|
40
|
+
component: ProductDetailPage,
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
export const router = createRouter({
|
|
46
|
+
routeTree,
|
|
47
|
+
context: { queryClient },
|
|
48
|
+
defaultPreload: "intent",
|
|
49
|
+
defaultPreloadStaleTime: 0,
|
|
50
|
+
});
|
|
51
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Navigation
|
|
2
|
+
|
|
3
|
+
## Rules
|
|
4
|
+
|
|
5
|
+
- Prefer `Link` for standard navigation.
|
|
6
|
+
- Use `useNavigate` only for side effects.
|
|
7
|
+
- Use route masks for modal URLs.
|
|
8
|
+
|
|
9
|
+
## Minimal example
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { Link, useNavigate } from "@tanstack/react-router";
|
|
13
|
+
|
|
14
|
+
<Link to="/products/$productId" params={{ productId: product.id }} preload="intent">
|
|
15
|
+
View
|
|
16
|
+
</Link>;
|
|
17
|
+
|
|
18
|
+
const navigate = useNavigate();
|
|
19
|
+
navigate({ to: "/products" });
|
|
20
|
+
|
|
21
|
+
<Link to="/products/$productId" params={{ productId: product.id }} mask={{ to: "/products" }}>
|
|
22
|
+
Quick view
|
|
23
|
+
</Link>;
|
|
24
|
+
```
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Private Routes
|
|
2
|
+
|
|
3
|
+
This reference describes how to implement private routes with TanStack Router while respecting the project Clean Architecture.
|
|
4
|
+
|
|
5
|
+
## Goals
|
|
6
|
+
|
|
7
|
+
- Share auth state across the route tree via Router context.
|
|
8
|
+
- Redirect early in `beforeLoad` for protected and public-only areas.
|
|
9
|
+
- Route index decides where to land based on auth and role.
|
|
10
|
+
- Layout routes group protected areas (admin, dashboard, etc.).
|
|
11
|
+
- Auth changes trigger redirects by invalidating the router.
|
|
12
|
+
|
|
13
|
+
## Architecture Rules
|
|
14
|
+
|
|
15
|
+
- Route files live in `src/interface/router/**`.
|
|
16
|
+
- Auth is owned by Application (query + hook), not by Interface or Presentation.
|
|
17
|
+
- Presentation provides Router context (no direct repository calls in routes).
|
|
18
|
+
- Redirects happen in `beforeLoad`, not inside page components.
|
|
19
|
+
|
|
20
|
+
## Router Context and Provider
|
|
21
|
+
|
|
22
|
+
Create a typed router context that includes `queryClient` and `auth`. Set `auth: undefined!` in router creation and pass the real value in the provider.
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { QueryClient } from "@tanstack/react-query";
|
|
26
|
+
import { createRootRouteWithContext, createRouter } from "@tanstack/react-router";
|
|
27
|
+
|
|
28
|
+
type RouterContext = {
|
|
29
|
+
queryClient: QueryClient;
|
|
30
|
+
auth: AuthState | undefined;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const rootRoute = createRootRouteWithContext<RouterContext>()({
|
|
34
|
+
component: AppLayout,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const queryClient = new QueryClient();
|
|
38
|
+
|
|
39
|
+
export const router = createRouter({
|
|
40
|
+
routeTree,
|
|
41
|
+
context: { queryClient, auth: undefined! },
|
|
42
|
+
defaultPreload: "intent",
|
|
43
|
+
defaultPreloadStaleTime: 0,
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { RouterProvider } from "@tanstack/react-router";
|
|
49
|
+
|
|
50
|
+
import { useAuth } from "@/application/auth/hooks/useAuth";
|
|
51
|
+
import { router } from "@/interface/router";
|
|
52
|
+
|
|
53
|
+
export function AuthedRouterProvider() {
|
|
54
|
+
const auth = useAuth();
|
|
55
|
+
|
|
56
|
+
React.useEffect(() => {
|
|
57
|
+
router.invalidate();
|
|
58
|
+
}, [auth]);
|
|
59
|
+
|
|
60
|
+
return <RouterProvider router={router} context={{ auth }} />;
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Route Groups and Redirects
|
|
65
|
+
|
|
66
|
+
Use pathless route groups to separate authenticated and unauthenticated sections. Place these in `src/interface/router/routes/`.
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
routes/
|
|
70
|
+
_authenticated/
|
|
71
|
+
route.tsx
|
|
72
|
+
admin/
|
|
73
|
+
route.tsx
|
|
74
|
+
_unauthenticated/
|
|
75
|
+
route.tsx
|
|
76
|
+
index.tsx
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Protected group
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
import { Outlet, createFileRoute, redirect } from "@tanstack/react-router";
|
|
83
|
+
|
|
84
|
+
export const Route = createFileRoute("/_authenticated")({
|
|
85
|
+
beforeLoad: ({ context: { auth } }) => {
|
|
86
|
+
if (!auth?.user) {
|
|
87
|
+
throw redirect({ to: "/login" });
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
component: RouteComponent,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
function RouteComponent() {
|
|
94
|
+
return <Outlet />;
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Public-only group
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
import { Outlet, createFileRoute, redirect } from "@tanstack/react-router";
|
|
102
|
+
|
|
103
|
+
export const Route = createFileRoute("/_unauthenticated")({
|
|
104
|
+
beforeLoad: ({ context: { auth } }) => {
|
|
105
|
+
if (auth?.user) {
|
|
106
|
+
throw redirect({ to: "/admin" });
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
component: RouteComponent,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
function RouteComponent() {
|
|
113
|
+
return <Outlet />;
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Index Redirect
|
|
118
|
+
|
|
119
|
+
Index route decides where to land based on auth and role.
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
import { createFileRoute, redirect } from "@tanstack/react-router";
|
|
123
|
+
|
|
124
|
+
export const Route = createFileRoute("/")({
|
|
125
|
+
beforeLoad: ({ context: { auth } }) => {
|
|
126
|
+
if (!auth?.user) {
|
|
127
|
+
throw redirect({ to: "/login" });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (auth.user.role === "admin") {
|
|
131
|
+
throw redirect({ to: "/admin" });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
throw redirect({ to: "/products" });
|
|
135
|
+
},
|
|
136
|
+
component: PageLoader,
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Layout Route for Admin
|
|
141
|
+
|
|
142
|
+
Use a layout route to wrap all admin pages and redirect bare `/admin` to a default child.
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
import { createFileRoute, redirect } from "@tanstack/react-router";
|
|
146
|
+
|
|
147
|
+
export const Route = createFileRoute("/_authenticated/admin")({
|
|
148
|
+
beforeLoad: ({ location }) => {
|
|
149
|
+
if (location.pathname === "/admin") {
|
|
150
|
+
throw redirect({ to: "/admin/profile" });
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
component: AdminDashboardLayout,
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Auth Change Redirects
|
|
158
|
+
|
|
159
|
+
Invalidate the router when auth changes so `beforeLoad` runs again. Also invalidate the auth query when login/logout happens.
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
queryClient.invalidateQueries({ queryKey: ["auth"] });
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Common Pitfalls
|
|
166
|
+
|
|
167
|
+
- Do not fetch auth directly in routes or pages.
|
|
168
|
+
- Do not handle redirects inside page components.
|
|
169
|
+
- Do not import DTOs or repositories in Presentation or Interface routes.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Router context
|
|
2
|
+
|
|
3
|
+
## Rules
|
|
4
|
+
|
|
5
|
+
- Use `createRootRouteWithContext` to type router context.
|
|
6
|
+
- Inject shared services via `createRouter` context.
|
|
7
|
+
- Access `context.queryClient` inside loaders.
|
|
8
|
+
- Avoid global imports inside route files.
|
|
9
|
+
|
|
10
|
+
## Minimal example
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import type { QueryClient } from "@tanstack/react-query";
|
|
14
|
+
import { createRootRouteWithContext, createRoute, createRouter } from "@tanstack/react-router";
|
|
15
|
+
|
|
16
|
+
type RouterContext = { queryClient: QueryClient };
|
|
17
|
+
|
|
18
|
+
export const rootRoute = createRootRouteWithContext<RouterContext>()({
|
|
19
|
+
component: RootLayout,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const listRoute = createRoute({
|
|
23
|
+
getParentRoute: () => rootRoute,
|
|
24
|
+
path: "/",
|
|
25
|
+
loader: async ({ context: { queryClient } }) => {
|
|
26
|
+
await queryClient.ensureQueryData(productQueries.list());
|
|
27
|
+
},
|
|
28
|
+
component: ProductListPage,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const router = createRouter({
|
|
32
|
+
routeTree: rootRoute.addChildren([listRoute]),
|
|
33
|
+
context: { queryClient },
|
|
34
|
+
});
|
|
35
|
+
```
|