@smicolon/ai-kit 0.1.0 → 0.1.1
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/.claude-plugin/CLAUDE.md +7 -0
- package/.claude-plugin/marketplace.json +373 -0
- package/package.json +4 -3
- package/packs/architect/CHANGELOG.md +17 -0
- package/packs/architect/README.md +58 -0
- package/packs/architect/agents/system-architect.md +768 -0
- package/packs/architect/commands/diagram-create.md +300 -0
- package/packs/better-auth/.claude-plugin/plugin.json +14 -0
- package/packs/better-auth/.mcp.json +14 -0
- package/packs/better-auth/CHANGELOG.md +26 -0
- package/packs/better-auth/README.md +125 -0
- package/packs/better-auth/agents/auth-architect.md +278 -0
- package/packs/better-auth/commands/auth-provider-add.md +265 -0
- package/packs/better-auth/commands/auth-setup.md +298 -0
- package/packs/better-auth/skills/auth-security/SKILL.md +425 -0
- package/packs/better-auth/skills/better-auth-patterns/SKILL.md +455 -0
- package/packs/dev-loop/.claude-plugin/plugin.json +10 -0
- package/packs/dev-loop/CHANGELOG.md +69 -0
- package/packs/dev-loop/README.md +155 -0
- package/packs/dev-loop/commands/cancel-dev.md +21 -0
- package/packs/dev-loop/commands/dev-loop.md +72 -0
- package/packs/dev-loop/commands/dev-plan.md +351 -0
- package/packs/dev-loop/hooks/hooks.json +15 -0
- package/packs/dev-loop/hooks/stop-hook.sh +178 -0
- package/packs/dev-loop/scripts/setup-dev-loop.sh +194 -0
- package/packs/dev-loop/skills/tdd-planner/SKILL.md +249 -0
- package/packs/dev-loop/skills/tdd-planner/references/framework-patterns.md +874 -0
- package/packs/dev-loop/skills/tdd-planner/references/good-example.md +260 -0
- package/packs/dev-loop/skills/tdd-planner/references/plan-template.md +275 -0
- package/packs/django/CHANGELOG.md +39 -0
- package/packs/django/README.md +92 -0
- package/packs/django/agents/django-architect.md +182 -0
- package/packs/django/agents/django-builder.md +250 -0
- package/packs/django/agents/django-feature-based.md +420 -0
- package/packs/django/agents/django-reviewer.md +253 -0
- package/packs/django/agents/django-tester.md +230 -0
- package/packs/django/commands/api-endpoint.md +285 -0
- package/packs/django/commands/model-create.md +178 -0
- package/packs/django/commands/test-generate.md +325 -0
- package/packs/django/rules/migrations.md +138 -0
- package/packs/django/rules/models.md +167 -0
- package/packs/django/rules/serializers.md +126 -0
- package/packs/django/rules/services.md +131 -0
- package/packs/django/rules/tests.md +140 -0
- package/packs/django/rules/views.md +102 -0
- package/packs/django/skills/import-convention-enforcer/SKILL.md +226 -0
- package/packs/django/skills/import-convention-enforcer/patterns/django-imports.md +343 -0
- package/packs/django/skills/migration-safety-checker/SKILL.md +375 -0
- package/packs/django/skills/model-entity-validator/SKILL.md +298 -0
- package/packs/django/skills/performance-optimizer/SKILL.md +447 -0
- package/packs/django/skills/red-phase-verifier/SKILL.md +180 -0
- package/packs/django/skills/security-first-validator/SKILL.md +435 -0
- package/packs/django/skills/test-coverage-advisor/SKILL.md +394 -0
- package/packs/django/skills/test-validity-checker/SKILL.md +194 -0
- package/packs/failure-log/.claude-plugin/plugin.json +14 -0
- package/packs/failure-log/CHANGELOG.md +20 -0
- package/packs/failure-log/README.md +168 -0
- package/packs/failure-log/commands/failure-add.md +106 -0
- package/packs/failure-log/commands/failure-list.md +89 -0
- package/packs/failure-log/hooks/hooks.json +16 -0
- package/packs/failure-log/hooks/scripts/inject-failures.sh +64 -0
- package/packs/failure-log/skills/failure-log-manager/SKILL.md +164 -0
- package/packs/flutter/.claude-plugin/plugin.json +10 -0
- package/packs/flutter/CHANGELOG.md +19 -0
- package/packs/flutter/README.md +170 -0
- package/packs/flutter/agents/flutter-architect.md +166 -0
- package/packs/flutter/agents/flutter-builder.md +303 -0
- package/packs/flutter/agents/release-manager.md +355 -0
- package/packs/flutter/commands/fastlane-setup.md +188 -0
- package/packs/flutter/commands/flutter-build.md +90 -0
- package/packs/flutter/commands/flutter-deploy.md +133 -0
- package/packs/flutter/commands/flutter-test.md +117 -0
- package/packs/flutter/commands/signing-setup.md +209 -0
- package/packs/flutter/hooks/hooks.json +17 -0
- package/packs/flutter/skills/fastlane-knowledge/SKILL.md +193 -0
- package/packs/flutter/skills/flutter-architecture/SKILL.md +127 -0
- package/packs/flutter/skills/store-publishing/SKILL.md +163 -0
- package/packs/hono/.claude-plugin/plugin.json +19 -0
- package/packs/hono/CHANGELOG.md +19 -0
- package/packs/hono/README.md +143 -0
- package/packs/hono/agents/hono-architect.md +240 -0
- package/packs/hono/agents/hono-builder.md +285 -0
- package/packs/hono/agents/hono-reviewer.md +279 -0
- package/packs/hono/agents/hono-tester.md +346 -0
- package/packs/hono/commands/middleware-create.md +223 -0
- package/packs/hono/commands/project-init.md +306 -0
- package/packs/hono/commands/route-create.md +153 -0
- package/packs/hono/commands/rpc-client.md +263 -0
- package/packs/hono/hooks/hooks.json +4 -0
- package/packs/hono/skills/cloudflare-bindings/SKILL.md +408 -0
- package/packs/hono/skills/hono-patterns/SKILL.md +309 -0
- package/packs/hono/skills/rpc-typesafe/SKILL.md +388 -0
- package/packs/hono/skills/zod-validation/SKILL.md +332 -0
- package/packs/nestjs/CHANGELOG.md +29 -0
- package/packs/nestjs/README.md +75 -0
- package/packs/nestjs/agents/nestjs-architect.md +402 -0
- package/packs/nestjs/agents/nestjs-builder.md +301 -0
- package/packs/nestjs/agents/nestjs-tester.md +437 -0
- package/packs/nestjs/commands/module-create.md +369 -0
- package/packs/nestjs/rules/controllers.md +92 -0
- package/packs/nestjs/rules/dto.md +124 -0
- package/packs/nestjs/rules/entities.md +102 -0
- package/packs/nestjs/rules/services.md +106 -0
- package/packs/nestjs/skills/barrel-export-manager/SKILL.md +389 -0
- package/packs/nestjs/skills/import-convention-enforcer/SKILL.md +365 -0
- package/packs/nextjs/CHANGELOG.md +36 -0
- package/packs/nextjs/README.md +76 -0
- package/packs/nextjs/agents/frontend-tester.md +680 -0
- package/packs/nextjs/agents/frontend-visual.md +820 -0
- package/packs/nextjs/agents/nextjs-architect.md +331 -0
- package/packs/nextjs/agents/nextjs-modular.md +433 -0
- package/packs/nextjs/commands/component-create.md +398 -0
- package/packs/nextjs/rules/api-routes.md +129 -0
- package/packs/nextjs/rules/components.md +106 -0
- package/packs/nextjs/rules/hooks.md +132 -0
- package/packs/nextjs/skills/accessibility-validator/SKILL.md +445 -0
- package/packs/nextjs/skills/import-convention-enforcer/SKILL.md +399 -0
- package/packs/nextjs/skills/react-form-validator/SKILL.md +569 -0
- package/packs/nuxtjs/CHANGELOG.md +30 -0
- package/packs/nuxtjs/README.md +56 -0
- package/packs/nuxtjs/agents/frontend-tester.md +680 -0
- package/packs/nuxtjs/agents/frontend-visual.md +820 -0
- package/packs/nuxtjs/agents/nuxtjs-architect.md +537 -0
- package/packs/nuxtjs/commands/component-create.md +223 -0
- package/packs/nuxtjs/rules/components.md +101 -0
- package/packs/nuxtjs/rules/composables.md +118 -0
- package/packs/nuxtjs/rules/server-routes.md +127 -0
- package/packs/nuxtjs/skills/accessibility-validator/SKILL.md +183 -0
- package/packs/nuxtjs/skills/import-convention-enforcer/SKILL.md +196 -0
- package/packs/nuxtjs/skills/veevalidate-form-validator/SKILL.md +190 -0
- package/packs/onboard/CHANGELOG.md +22 -0
- package/packs/onboard/README.md +103 -0
- package/packs/onboard/agents/onboard-guide.md +118 -0
- package/packs/onboard/commands/onboard.md +313 -0
- package/packs/onboard/skills/onboard-context-provider/SKILL.md +98 -0
- package/packs/tanstack-router/.claude-plugin/plugin.json +14 -0
- package/packs/tanstack-router/CHANGELOG.md +30 -0
- package/packs/tanstack-router/README.md +113 -0
- package/packs/tanstack-router/agents/tanstack-architect.md +173 -0
- package/packs/tanstack-router/agents/tanstack-builder.md +360 -0
- package/packs/tanstack-router/agents/tanstack-tester.md +454 -0
- package/packs/tanstack-router/commands/form-create.md +313 -0
- package/packs/tanstack-router/commands/query-create.md +263 -0
- package/packs/tanstack-router/commands/route-create.md +190 -0
- package/packs/tanstack-router/commands/table-create.md +413 -0
- package/packs/tanstack-router/skills/ai-patterns/SKILL.md +370 -0
- package/packs/tanstack-router/skills/db-patterns/SKILL.md +346 -0
- package/packs/tanstack-router/skills/devtools-patterns/SKILL.md +415 -0
- package/packs/tanstack-router/skills/form-patterns/SKILL.md +425 -0
- package/packs/tanstack-router/skills/pacer-patterns/SKILL.md +341 -0
- package/packs/tanstack-router/skills/query-patterns/SKILL.md +359 -0
- package/packs/tanstack-router/skills/router-patterns/SKILL.md +285 -0
- package/packs/tanstack-router/skills/store-patterns/SKILL.md +351 -0
- package/packs/tanstack-router/skills/table-patterns/SKILL.md +531 -0
- package/packs/tanstack-router/skills/tanstack-conventions/SKILL.md +428 -0
- package/packs/tanstack-router/skills/virtual-patterns/SKILL.md +490 -0
- package/packs/worktree/.claude-plugin/plugin.json +19 -0
- package/packs/worktree/CHANGELOG.md +24 -0
- package/packs/worktree/README.md +110 -0
- package/packs/worktree/commands/wt.md +73 -0
- package/packs/worktree/scripts/wt.sh +396 -0
- package/packs/worktree/skills/worktree-manager/SKILL.md +68 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: TanStack Router Patterns
|
|
3
|
+
description: >-
|
|
4
|
+
Auto-enforce TanStack Router file-based routing conventions. Activates when
|
|
5
|
+
creating routes, configuring navigation, handling route params, or working
|
|
6
|
+
with search params in React SPA applications.
|
|
7
|
+
version: 1.0.0
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# TanStack Router Patterns
|
|
11
|
+
|
|
12
|
+
This skill enforces TanStack Router best practices for file-based routing in React SPA applications.
|
|
13
|
+
|
|
14
|
+
## Route File Naming Conventions
|
|
15
|
+
|
|
16
|
+
| Pattern | Example | URL Path | Purpose |
|
|
17
|
+
|---------|---------|----------|---------|
|
|
18
|
+
| `__root.tsx` | `routes/__root.tsx` | - | Root layout, wraps all routes |
|
|
19
|
+
| `index.tsx` | `routes/index.tsx` | `/` | Index route |
|
|
20
|
+
| `about.tsx` | `routes/about.tsx` | `/about` | Static segment |
|
|
21
|
+
| `posts.tsx` | `routes/posts.tsx` | `/posts` | Layout route (has `<Outlet />`) |
|
|
22
|
+
| `posts.index.tsx` | `routes/posts.index.tsx` | `/posts` | Posts index (nested in layout) |
|
|
23
|
+
| `posts.$postId.tsx` | `routes/posts.$postId.tsx` | `/posts/:postId` | Dynamic parameter |
|
|
24
|
+
| `posts_.$postId.edit.tsx` | `routes/posts_.$postId.edit.tsx` | `/posts/:postId/edit` | Pathless parent layout |
|
|
25
|
+
| `_auth.tsx` | `routes/_auth.tsx` | - | Pathless layout (no URL segment) |
|
|
26
|
+
| `_auth.login.tsx` | `routes/_auth.login.tsx` | `/login` | Child of pathless layout |
|
|
27
|
+
| `(marketing)/` | `routes/(marketing)/about.tsx` | `/about` | Route group (organization only) |
|
|
28
|
+
| `$.tsx` | `routes/$.tsx` | `/*` | Catch-all/splat route |
|
|
29
|
+
|
|
30
|
+
## Route File Structure
|
|
31
|
+
|
|
32
|
+
### Basic Route
|
|
33
|
+
```typescript
|
|
34
|
+
// routes/about.tsx
|
|
35
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
36
|
+
|
|
37
|
+
export const Route = createFileRoute('/about')({
|
|
38
|
+
component: AboutPage,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
function AboutPage() {
|
|
42
|
+
return <div>About Page</div>
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Route with Loader
|
|
47
|
+
```typescript
|
|
48
|
+
// routes/posts.$postId.tsx
|
|
49
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
50
|
+
import { postQueryOptions } from '@/features/posts/queries'
|
|
51
|
+
|
|
52
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
53
|
+
loader: ({ context: { queryClient }, params }) =>
|
|
54
|
+
queryClient.ensureQueryData(postQueryOptions(params.postId)),
|
|
55
|
+
component: PostDetailPage,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
function PostDetailPage() {
|
|
59
|
+
const { postId } = Route.useParams()
|
|
60
|
+
const post = Route.useLoaderData()
|
|
61
|
+
|
|
62
|
+
return <PostDetail post={post} />
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Route with Search Params
|
|
67
|
+
```typescript
|
|
68
|
+
// routes/posts.tsx
|
|
69
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
70
|
+
import { z } from 'zod'
|
|
71
|
+
|
|
72
|
+
const postsSearchSchema = z.object({
|
|
73
|
+
page: z.number().default(1),
|
|
74
|
+
sort: z.enum(['newest', 'oldest', 'popular']).default('newest'),
|
|
75
|
+
search: z.string().optional(),
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
export const Route = createFileRoute('/posts')({
|
|
79
|
+
validateSearch: postsSearchSchema,
|
|
80
|
+
component: PostsPage,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
function PostsPage() {
|
|
84
|
+
const { page, sort, search } = Route.useSearch()
|
|
85
|
+
const navigate = Route.useNavigate()
|
|
86
|
+
|
|
87
|
+
const setPage = (newPage: number) => {
|
|
88
|
+
navigate({ search: (prev) => ({ ...prev, page: newPage }) })
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return <PostList page={page} sort={sort} search={search} onPageChange={setPage} />
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Root Route with Context
|
|
96
|
+
```typescript
|
|
97
|
+
// routes/__root.tsx
|
|
98
|
+
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'
|
|
99
|
+
import type { QueryClient } from '@tanstack/react-query'
|
|
100
|
+
|
|
101
|
+
interface RouterContext {
|
|
102
|
+
queryClient: QueryClient
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const Route = createRootRouteWithContext<RouterContext>()({
|
|
106
|
+
component: RootComponent,
|
|
107
|
+
beforeLoad: async ({ context }) => {
|
|
108
|
+
// Auth check, theme loading, etc.
|
|
109
|
+
return { user: await getUser() }
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
function RootComponent() {
|
|
114
|
+
return (
|
|
115
|
+
<div>
|
|
116
|
+
<Header />
|
|
117
|
+
<Outlet />
|
|
118
|
+
<Footer />
|
|
119
|
+
</div>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Pathless Layout Route
|
|
125
|
+
```typescript
|
|
126
|
+
// routes/_auth.tsx
|
|
127
|
+
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
|
|
128
|
+
|
|
129
|
+
export const Route = createFileRoute('/_auth')({
|
|
130
|
+
beforeLoad: async ({ context }) => {
|
|
131
|
+
if (!context.user) {
|
|
132
|
+
throw redirect({ to: '/login' })
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
component: AuthLayout,
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
function AuthLayout() {
|
|
139
|
+
return (
|
|
140
|
+
<div className="authenticated-layout">
|
|
141
|
+
<Sidebar />
|
|
142
|
+
<main>
|
|
143
|
+
<Outlet />
|
|
144
|
+
</main>
|
|
145
|
+
</div>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Navigation Patterns
|
|
151
|
+
|
|
152
|
+
### Declarative Navigation
|
|
153
|
+
```typescript
|
|
154
|
+
import { Link } from '@tanstack/react-router'
|
|
155
|
+
|
|
156
|
+
// Basic link
|
|
157
|
+
<Link to="/about">About</Link>
|
|
158
|
+
|
|
159
|
+
// With params
|
|
160
|
+
<Link to="/posts/$postId" params={{ postId: '123' }}>
|
|
161
|
+
View Post
|
|
162
|
+
</Link>
|
|
163
|
+
|
|
164
|
+
// With search params
|
|
165
|
+
<Link to="/posts" search={{ page: 2, sort: 'newest' }}>
|
|
166
|
+
Page 2
|
|
167
|
+
</Link>
|
|
168
|
+
|
|
169
|
+
// Active styling
|
|
170
|
+
<Link
|
|
171
|
+
to="/posts"
|
|
172
|
+
activeProps={{ className: 'active' }}
|
|
173
|
+
inactiveProps={{ className: 'inactive' }}
|
|
174
|
+
>
|
|
175
|
+
Posts
|
|
176
|
+
</Link>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Imperative Navigation
|
|
180
|
+
```typescript
|
|
181
|
+
import { useNavigate } from '@tanstack/react-router'
|
|
182
|
+
|
|
183
|
+
function PostCard({ post }) {
|
|
184
|
+
const navigate = useNavigate()
|
|
185
|
+
|
|
186
|
+
const handleClick = () => {
|
|
187
|
+
navigate({
|
|
188
|
+
to: '/posts/$postId',
|
|
189
|
+
params: { postId: post.id },
|
|
190
|
+
search: { tab: 'comments' }
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return <div onClick={handleClick}>{post.title}</div>
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Programmatic Redirect
|
|
199
|
+
```typescript
|
|
200
|
+
import { redirect } from '@tanstack/react-router'
|
|
201
|
+
|
|
202
|
+
export const Route = createFileRoute('/admin')({
|
|
203
|
+
beforeLoad: async ({ context }) => {
|
|
204
|
+
if (!context.user?.isAdmin) {
|
|
205
|
+
throw redirect({ to: '/', search: { error: 'unauthorized' } })
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
})
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Error Handling
|
|
212
|
+
|
|
213
|
+
### Route Error Boundary
|
|
214
|
+
```typescript
|
|
215
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
216
|
+
loader: async ({ params }) => {
|
|
217
|
+
const post = await getPost(params.postId)
|
|
218
|
+
if (!post) {
|
|
219
|
+
throw new Error('Post not found')
|
|
220
|
+
}
|
|
221
|
+
return post
|
|
222
|
+
},
|
|
223
|
+
errorComponent: ({ error }) => (
|
|
224
|
+
<div className="error">
|
|
225
|
+
<h2>Error loading post</h2>
|
|
226
|
+
<p>{error.message}</p>
|
|
227
|
+
</div>
|
|
228
|
+
),
|
|
229
|
+
component: PostPage,
|
|
230
|
+
})
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Pending Component
|
|
234
|
+
```typescript
|
|
235
|
+
export const Route = createFileRoute('/posts')({
|
|
236
|
+
pendingComponent: () => <div>Loading posts...</div>,
|
|
237
|
+
pendingMinMs: 500, // Show pending after 500ms
|
|
238
|
+
pendingMs: 1000, // Minimum pending display time
|
|
239
|
+
component: PostsPage,
|
|
240
|
+
})
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Conventions to Enforce
|
|
244
|
+
|
|
245
|
+
1. **Always use `createFileRoute`** - Never manual route configuration
|
|
246
|
+
2. **Type-safe params** - Use `Route.useParams()` for full type inference
|
|
247
|
+
3. **Validate search params** - Use Zod schemas with `validateSearch`
|
|
248
|
+
4. **Loaders for data** - Prefetch with `ensureQueryData`, not direct fetches
|
|
249
|
+
5. **Context for shared data** - Auth, theme, queryClient in root context
|
|
250
|
+
6. **Error boundaries** - Always provide `errorComponent` for data routes
|
|
251
|
+
7. **Pathless layouts** - Use `_` prefix for auth guards, feature layouts
|
|
252
|
+
|
|
253
|
+
## Anti-Patterns to Block
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
// ❌ WRONG: Manual route definition
|
|
257
|
+
const router = createRouter({
|
|
258
|
+
routes: [{ path: '/posts', component: Posts }]
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
// ✅ CORRECT: File-based routes
|
|
262
|
+
// routes/posts.tsx with createFileRoute
|
|
263
|
+
|
|
264
|
+
// ❌ WRONG: useParams from react-router-dom
|
|
265
|
+
import { useParams } from 'react-router-dom'
|
|
266
|
+
|
|
267
|
+
// ✅ CORRECT: Route-specific useParams
|
|
268
|
+
const { postId } = Route.useParams()
|
|
269
|
+
|
|
270
|
+
// ❌ WRONG: Unvalidated search params
|
|
271
|
+
const search = new URLSearchParams(window.location.search)
|
|
272
|
+
|
|
273
|
+
// ✅ CORRECT: Validated with Zod
|
|
274
|
+
const { page, sort } = Route.useSearch()
|
|
275
|
+
|
|
276
|
+
// ❌ WRONG: Direct fetch in loader
|
|
277
|
+
loader: async () => {
|
|
278
|
+
const response = await fetch('/api/posts')
|
|
279
|
+
return response.json()
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ✅ CORRECT: Use query client
|
|
283
|
+
loader: ({ context: { queryClient } }) =>
|
|
284
|
+
queryClient.ensureQueryData(postsQueryOptions())
|
|
285
|
+
```
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: TanStack Store Patterns (Alpha)
|
|
3
|
+
description: >-
|
|
4
|
+
TanStack Store patterns for framework-agnostic reactive state. Activates when
|
|
5
|
+
implementing client-side state, global stores, or reactive state management
|
|
6
|
+
in React applications. NOTE: Alpha library - API may change.
|
|
7
|
+
version: 1.0.0
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# TanStack Store Patterns (Alpha)
|
|
11
|
+
|
|
12
|
+
> **Alpha Library**: TanStack Store is in alpha. APIs may change between versions.
|
|
13
|
+
|
|
14
|
+
This skill covers TanStack Store for framework-agnostic reactive state management.
|
|
15
|
+
|
|
16
|
+
## Basic Store
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// lib/stores/counter-store.ts
|
|
20
|
+
import { Store } from '@tanstack/store'
|
|
21
|
+
|
|
22
|
+
export const counterStore = new Store({
|
|
23
|
+
count: 0,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// Actions
|
|
27
|
+
export const increment = () => {
|
|
28
|
+
counterStore.setState((state) => ({
|
|
29
|
+
...state,
|
|
30
|
+
count: state.count + 1,
|
|
31
|
+
}))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const decrement = () => {
|
|
35
|
+
counterStore.setState((state) => ({
|
|
36
|
+
...state,
|
|
37
|
+
count: state.count - 1,
|
|
38
|
+
}))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const reset = () => {
|
|
42
|
+
counterStore.setState((state) => ({
|
|
43
|
+
...state,
|
|
44
|
+
count: 0,
|
|
45
|
+
}))
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Using Store in React
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { useStore } from '@tanstack/react-store'
|
|
53
|
+
import { counterStore, increment, decrement, reset } from '@/lib/stores/counter-store'
|
|
54
|
+
|
|
55
|
+
function Counter() {
|
|
56
|
+
const count = useStore(counterStore, (state) => state.count)
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div>
|
|
60
|
+
<span>Count: {count}</span>
|
|
61
|
+
<button onClick={decrement}>-</button>
|
|
62
|
+
<button onClick={increment}>+</button>
|
|
63
|
+
<button onClick={reset}>Reset</button>
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Complex Store with Derived State
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// lib/stores/cart-store.ts
|
|
73
|
+
import { Store } from '@tanstack/store'
|
|
74
|
+
|
|
75
|
+
interface CartItem {
|
|
76
|
+
id: string
|
|
77
|
+
name: string
|
|
78
|
+
price: number
|
|
79
|
+
quantity: number
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface CartState {
|
|
83
|
+
items: CartItem[]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const cartStore = new Store<CartState>({
|
|
87
|
+
items: [],
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// Derived selectors
|
|
91
|
+
export const selectCartItems = (state: CartState) => state.items
|
|
92
|
+
|
|
93
|
+
export const selectCartTotal = (state: CartState) =>
|
|
94
|
+
state.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
|
|
95
|
+
|
|
96
|
+
export const selectCartCount = (state: CartState) =>
|
|
97
|
+
state.items.reduce((sum, item) => sum + item.quantity, 0)
|
|
98
|
+
|
|
99
|
+
// Actions
|
|
100
|
+
export const addToCart = (item: Omit<CartItem, 'quantity'>) => {
|
|
101
|
+
cartStore.setState((state) => {
|
|
102
|
+
const existing = state.items.find((i) => i.id === item.id)
|
|
103
|
+
if (existing) {
|
|
104
|
+
return {
|
|
105
|
+
...state,
|
|
106
|
+
items: state.items.map((i) =>
|
|
107
|
+
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
|
|
108
|
+
),
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
...state,
|
|
113
|
+
items: [...state.items, { ...item, quantity: 1 }],
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const removeFromCart = (itemId: string) => {
|
|
119
|
+
cartStore.setState((state) => ({
|
|
120
|
+
...state,
|
|
121
|
+
items: state.items.filter((i) => i.id !== itemId),
|
|
122
|
+
}))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const updateQuantity = (itemId: string, quantity: number) => {
|
|
126
|
+
if (quantity <= 0) {
|
|
127
|
+
removeFromCart(itemId)
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
cartStore.setState((state) => ({
|
|
131
|
+
...state,
|
|
132
|
+
items: state.items.map((i) =>
|
|
133
|
+
i.id === itemId ? { ...i, quantity } : i
|
|
134
|
+
),
|
|
135
|
+
}))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const clearCart = () => {
|
|
139
|
+
cartStore.setState(() => ({ items: [] }))
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Using Complex Store
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { useStore } from '@tanstack/react-store'
|
|
147
|
+
import {
|
|
148
|
+
cartStore,
|
|
149
|
+
selectCartItems,
|
|
150
|
+
selectCartTotal,
|
|
151
|
+
selectCartCount,
|
|
152
|
+
updateQuantity,
|
|
153
|
+
removeFromCart,
|
|
154
|
+
} from '@/lib/stores/cart-store'
|
|
155
|
+
|
|
156
|
+
function CartSummary() {
|
|
157
|
+
const count = useStore(cartStore, selectCartCount)
|
|
158
|
+
const total = useStore(cartStore, selectCartTotal)
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<div>
|
|
162
|
+
<span>{count} items</span>
|
|
163
|
+
<span>${total.toFixed(2)}</span>
|
|
164
|
+
</div>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function CartItems() {
|
|
169
|
+
const items = useStore(cartStore, selectCartItems)
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<ul>
|
|
173
|
+
{items.map((item) => (
|
|
174
|
+
<li key={item.id}>
|
|
175
|
+
<span>{item.name}</span>
|
|
176
|
+
<input
|
|
177
|
+
type="number"
|
|
178
|
+
value={item.quantity}
|
|
179
|
+
onChange={(e) => updateQuantity(item.id, parseInt(e.target.value))}
|
|
180
|
+
min={0}
|
|
181
|
+
/>
|
|
182
|
+
<button onClick={() => removeFromCart(item.id)}>Remove</button>
|
|
183
|
+
</li>
|
|
184
|
+
))}
|
|
185
|
+
</ul>
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Store with Persistence
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
// lib/stores/theme-store.ts
|
|
194
|
+
import { Store } from '@tanstack/store'
|
|
195
|
+
|
|
196
|
+
type Theme = 'light' | 'dark' | 'system'
|
|
197
|
+
|
|
198
|
+
interface ThemeState {
|
|
199
|
+
theme: Theme
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const getInitialTheme = (): Theme => {
|
|
203
|
+
if (typeof window === 'undefined') return 'system'
|
|
204
|
+
const stored = localStorage.getItem('theme')
|
|
205
|
+
if (stored === 'light' || stored === 'dark' || stored === 'system') {
|
|
206
|
+
return stored
|
|
207
|
+
}
|
|
208
|
+
return 'system'
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export const themeStore = new Store<ThemeState>({
|
|
212
|
+
theme: getInitialTheme(),
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
// Subscribe to persist changes
|
|
216
|
+
themeStore.subscribe(() => {
|
|
217
|
+
const { theme } = themeStore.state
|
|
218
|
+
localStorage.setItem('theme', theme)
|
|
219
|
+
|
|
220
|
+
// Apply theme to document
|
|
221
|
+
const root = document.documentElement
|
|
222
|
+
if (theme === 'system') {
|
|
223
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
224
|
+
root.classList.toggle('dark', prefersDark)
|
|
225
|
+
} else {
|
|
226
|
+
root.classList.toggle('dark', theme === 'dark')
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
export const setTheme = (theme: Theme) => {
|
|
231
|
+
themeStore.setState(() => ({ theme }))
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Store Factory Pattern
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
// lib/stores/create-entity-store.ts
|
|
239
|
+
import { Store } from '@tanstack/store'
|
|
240
|
+
|
|
241
|
+
interface EntityState<T> {
|
|
242
|
+
entities: Record<string, T>
|
|
243
|
+
ids: string[]
|
|
244
|
+
loading: boolean
|
|
245
|
+
error: string | null
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function createEntityStore<T extends { id: string }>() {
|
|
249
|
+
const store = new Store<EntityState<T>>({
|
|
250
|
+
entities: {},
|
|
251
|
+
ids: [],
|
|
252
|
+
loading: false,
|
|
253
|
+
error: null,
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
store,
|
|
258
|
+
|
|
259
|
+
selectAll: (state: EntityState<T>) =>
|
|
260
|
+
state.ids.map((id) => state.entities[id]),
|
|
261
|
+
|
|
262
|
+
selectById: (id: string) => (state: EntityState<T>) =>
|
|
263
|
+
state.entities[id],
|
|
264
|
+
|
|
265
|
+
setMany: (items: T[]) => {
|
|
266
|
+
store.setState((state) => ({
|
|
267
|
+
...state,
|
|
268
|
+
entities: items.reduce((acc, item) => ({ ...acc, [item.id]: item }), state.entities),
|
|
269
|
+
ids: [...new Set([...state.ids, ...items.map((i) => i.id)])],
|
|
270
|
+
}))
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
setOne: (item: T) => {
|
|
274
|
+
store.setState((state) => ({
|
|
275
|
+
...state,
|
|
276
|
+
entities: { ...state.entities, [item.id]: item },
|
|
277
|
+
ids: state.ids.includes(item.id) ? state.ids : [...state.ids, item.id],
|
|
278
|
+
}))
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
removeOne: (id: string) => {
|
|
282
|
+
store.setState((state) => {
|
|
283
|
+
const { [id]: removed, ...entities } = state.entities
|
|
284
|
+
return {
|
|
285
|
+
...state,
|
|
286
|
+
entities,
|
|
287
|
+
ids: state.ids.filter((i) => i !== id),
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
setLoading: (loading: boolean) => {
|
|
293
|
+
store.setState((state) => ({ ...state, loading }))
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
setError: (error: string | null) => {
|
|
297
|
+
store.setState((state) => ({ ...state, error }))
|
|
298
|
+
},
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Usage
|
|
303
|
+
const usersStore = createEntityStore<User>()
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## When to Use TanStack Store vs Query
|
|
307
|
+
|
|
308
|
+
| Use Case | Solution |
|
|
309
|
+
|----------|----------|
|
|
310
|
+
| Server data | TanStack Query |
|
|
311
|
+
| URL state | TanStack Router |
|
|
312
|
+
| Form state | TanStack Form |
|
|
313
|
+
| Local UI state | React useState |
|
|
314
|
+
| Shared client state | TanStack Store |
|
|
315
|
+
| Theme/preferences | TanStack Store + localStorage |
|
|
316
|
+
| Cart/wishlist | TanStack Store |
|
|
317
|
+
|
|
318
|
+
## Conventions
|
|
319
|
+
|
|
320
|
+
1. **Separate stores by domain** - One store per feature/concern
|
|
321
|
+
2. **Selector functions** - Define selectors for derived state
|
|
322
|
+
3. **Action functions** - Export actions, don't expose setState directly
|
|
323
|
+
4. **Type safety** - Always type your store state
|
|
324
|
+
5. **Persistence** - Use subscribe for localStorage sync
|
|
325
|
+
6. **Prefer Query** - Use Query for server state, Store for client-only
|
|
326
|
+
|
|
327
|
+
## Anti-Patterns
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
// ❌ WRONG: Mutating state directly
|
|
331
|
+
cartStore.state.items.push(newItem)
|
|
332
|
+
|
|
333
|
+
// ✅ CORRECT: Using setState
|
|
334
|
+
cartStore.setState((state) => ({
|
|
335
|
+
...state,
|
|
336
|
+
items: [...state.items, newItem],
|
|
337
|
+
}))
|
|
338
|
+
|
|
339
|
+
// ❌ WRONG: Storing server data
|
|
340
|
+
const postsStore = new Store({ posts: [] })
|
|
341
|
+
|
|
342
|
+
// ✅ CORRECT: Use Query for server data
|
|
343
|
+
const { data: posts } = useQuery(postsQueryOptions())
|
|
344
|
+
|
|
345
|
+
// ❌ WRONG: Exposing setState directly
|
|
346
|
+
export { counterStore }
|
|
347
|
+
// Component: counterStore.setState(...)
|
|
348
|
+
|
|
349
|
+
// ✅ CORRECT: Export action functions
|
|
350
|
+
export const increment = () => counterStore.setState(...)
|
|
351
|
+
```
|