@folpe/loom 0.1.0 → 0.3.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.
Files changed (45) hide show
  1. package/README.md +82 -16
  2. package/data/agents/backend/AGENT.md +12 -0
  3. package/data/agents/database/AGENT.md +3 -0
  4. package/data/agents/frontend/AGENT.md +10 -0
  5. package/data/agents/marketing/AGENT.md +3 -0
  6. package/data/agents/orchestrator/AGENT.md +1 -15
  7. package/data/agents/security/AGENT.md +3 -0
  8. package/data/agents/tests/AGENT.md +2 -0
  9. package/data/agents/ux-ui/AGENT.md +5 -0
  10. package/data/presets/api-backend.yaml +37 -0
  11. package/data/presets/chrome-extension.yaml +36 -0
  12. package/data/presets/cli-tool.yaml +31 -0
  13. package/data/presets/e-commerce.yaml +49 -0
  14. package/data/presets/expo-mobile.yaml +41 -0
  15. package/data/presets/fullstack-auth.yaml +45 -0
  16. package/data/presets/landing-page.yaml +38 -0
  17. package/data/presets/mvp-lean.yaml +35 -0
  18. package/data/presets/saas-default.yaml +7 -11
  19. package/data/presets/saas-full.yaml +71 -0
  20. package/data/skills/api-design/SKILL.md +149 -0
  21. package/data/skills/auth-rbac/SKILL.md +179 -0
  22. package/data/skills/better-auth-patterns/SKILL.md +212 -0
  23. package/data/skills/chrome-extension-patterns/SKILL.md +105 -0
  24. package/data/skills/cli-development/SKILL.md +147 -0
  25. package/data/skills/drizzle-patterns/SKILL.md +166 -0
  26. package/data/skills/env-validation/SKILL.md +142 -0
  27. package/data/skills/form-validation/SKILL.md +169 -0
  28. package/data/skills/hero-copywriting/SKILL.md +12 -4
  29. package/data/skills/i18n-patterns/SKILL.md +176 -0
  30. package/data/skills/layered-architecture/SKILL.md +131 -0
  31. package/data/skills/nextjs-conventions/SKILL.md +46 -7
  32. package/data/skills/react-native-patterns/SKILL.md +87 -0
  33. package/data/skills/react-query-patterns/SKILL.md +193 -0
  34. package/data/skills/resend-email/SKILL.md +181 -0
  35. package/data/skills/seo-optimization/SKILL.md +106 -0
  36. package/data/skills/server-actions-patterns/SKILL.md +156 -0
  37. package/data/skills/shadcn-ui/SKILL.md +126 -0
  38. package/data/skills/stripe-integration/SKILL.md +96 -0
  39. package/data/skills/supabase-patterns/SKILL.md +110 -0
  40. package/data/skills/table-pagination/SKILL.md +224 -0
  41. package/data/skills/tailwind-patterns/SKILL.md +12 -2
  42. package/data/skills/testing-patterns/SKILL.md +203 -0
  43. package/data/skills/ui-ux-guidelines/SKILL.md +179 -0
  44. package/dist/index.js +254 -100
  45. package/package.json +2 -1
@@ -0,0 +1,176 @@
1
+ ---
2
+ name: i18n-patterns
3
+ description: "Internationalization with next-intl for RSC, Server Actions, and Zod messages. Use when adding multi-language support, translating user-facing strings, or implementing locale-aware validation."
4
+ ---
5
+
6
+ # I18n Patterns
7
+
8
+ ## Critical Rules
9
+
10
+ - **All user-facing strings must be translated** — no hardcoded text in components.
11
+ - **Use namespaces** — group translations by feature/page.
12
+ - **Server-side `getTranslations`** — use in RSC and Server Actions.
13
+ - **Client-side `useTranslations`** — use in Client Components only.
14
+ - **Validation messages are translated** — use i18n error map with Zod.
15
+ - **ICU message syntax** for plurals and interpolation: `"{count, plural, one {# item} other {# items}}"`.
16
+
17
+ ## Setup with next-intl
18
+
19
+ ### Configuration
20
+
21
+ ```ts
22
+ // src/i18n/config.ts
23
+ export const locales = ["en", "fr", "de"] as const;
24
+ export type Locale = (typeof locales)[number];
25
+ export const defaultLocale: Locale = "en";
26
+ ```
27
+
28
+ ### Message Files
29
+
30
+ ```
31
+ messages/
32
+ en.json
33
+ fr.json
34
+ de.json
35
+ ```
36
+
37
+ Structure messages by namespace:
38
+
39
+ ```json
40
+ {
41
+ "common": {
42
+ "save": "Save",
43
+ "cancel": "Cancel",
44
+ "delete": "Delete",
45
+ "loading": "Loading..."
46
+ },
47
+ "auth": {
48
+ "login": "Sign in",
49
+ "logout": "Sign out",
50
+ "register": "Create account"
51
+ },
52
+ "users": {
53
+ "title": "Users",
54
+ "create": "Create user",
55
+ "name": "Name",
56
+ "email": "Email"
57
+ },
58
+ "validation": {
59
+ "required": "This field is required",
60
+ "email.invalid": "Please enter a valid email",
61
+ "too_small": "Must be at least {minimum} characters",
62
+ "too_big": "Must be at most {maximum} characters"
63
+ }
64
+ }
65
+ ```
66
+
67
+ ## Server Components (RSC)
68
+
69
+ ```tsx
70
+ import { getTranslations } from "next-intl/server";
71
+
72
+ export default async function UsersPage() {
73
+ const t = await getTranslations("users");
74
+
75
+ return (
76
+ <div>
77
+ <h1>{t("title")}</h1>
78
+ <Button>{t("create")}</Button>
79
+ </div>
80
+ );
81
+ }
82
+ ```
83
+
84
+ ## Client Components
85
+
86
+ ```tsx
87
+ "use client";
88
+ import { useTranslations } from "next-intl";
89
+
90
+ export function UserForm() {
91
+ const t = useTranslations("users");
92
+
93
+ return <label>{t("name")}</label>;
94
+ }
95
+ ```
96
+
97
+ ## Server Actions with I18n
98
+
99
+ Pass locale context to server actions for localized error messages:
100
+
101
+ ```ts
102
+ // src/actions/user.actions.ts
103
+ "use server";
104
+ import { getLocale, getTranslations } from "next-intl/server";
105
+
106
+ export async function createUser(input: unknown) {
107
+ const t = await getTranslations("validation");
108
+ const locale = await getLocale();
109
+
110
+ const schema = z.object({
111
+ name: z.string().min(2, t("too_small", { minimum: 2 })),
112
+ email: z.string().email(t("email.invalid")),
113
+ });
114
+
115
+ const parsed = schema.safeParse(input);
116
+ if (!parsed.success) {
117
+ return { success: false, error: parsed.error.errors[0].message };
118
+ }
119
+ // ...
120
+ }
121
+ ```
122
+
123
+ ## Zod Validation with I18n
124
+
125
+ ### Custom Error Map
126
+
127
+ ```ts
128
+ // src/lib/zod-i18n.ts
129
+ import { z } from "zod";
130
+
131
+ export function createI18nErrorMap(
132
+ t: (key: string, params?: Record<string, unknown>) => string
133
+ ): z.ZodErrorMap {
134
+ return (issue, ctx) => {
135
+ switch (issue.code) {
136
+ case z.ZodIssueCode.too_small:
137
+ if (issue.type === "string") {
138
+ return { message: t("too_small", { minimum: issue.minimum }) };
139
+ }
140
+ break;
141
+ case z.ZodIssueCode.too_big:
142
+ if (issue.type === "string") {
143
+ return { message: t("too_big", { maximum: issue.maximum }) };
144
+ }
145
+ break;
146
+ case z.ZodIssueCode.invalid_string:
147
+ if (issue.validation === "email") {
148
+ return { message: t("email.invalid") };
149
+ }
150
+ break;
151
+ case z.ZodIssueCode.invalid_type:
152
+ if (issue.received === "undefined") {
153
+ return { message: t("required") };
154
+ }
155
+ break;
156
+ }
157
+ return { message: ctx.defaultError };
158
+ };
159
+ }
160
+ ```
161
+
162
+ ## Date and Number Formatting
163
+
164
+ ```tsx
165
+ import { useFormatter } from "next-intl";
166
+
167
+ function PriceDisplay({ amount }: { amount: number }) {
168
+ const format = useFormatter();
169
+ return <span>{format.number(amount, { style: "currency", currency: "EUR" })}</span>;
170
+ }
171
+
172
+ function DateDisplay({ date }: { date: Date }) {
173
+ const format = useFormatter();
174
+ return <span>{format.dateTime(date, { dateStyle: "medium" })}</span>;
175
+ }
176
+ ```
@@ -0,0 +1,131 @@
1
+ ---
2
+ name: layered-architecture
3
+ description: "Enforces strict layered architecture: Presentation → Facade → Service → DAL → Persistence. Use when structuring a Next.js application, creating new features with separation of concerns, or refactoring code into layers."
4
+ ---
5
+
6
+ # Layered Architecture
7
+
8
+ Strict separation of concerns across five layers. Each layer only calls the layer directly below it.
9
+
10
+ ## Critical Rules
11
+
12
+ - **Never skip layers** — presentation must not call services directly.
13
+ - **No business logic in facades** — they coordinate, not decide.
14
+ - **No auth checks in DAL** — DAL is purely data access.
15
+ - **No database calls in services** — services call DAL.
16
+ - **Functional style** — pure functions, no classes, prefer composition over inheritance.
17
+ - **Top-down design** — keep functions short, extract when >100 lines.
18
+
19
+ ## Layer Overview
20
+
21
+ ```
22
+ Presentation (RSC / Client Components)
23
+
24
+ Facade (entry point for business logic)
25
+
26
+ Service (business rules, authorization)
27
+
28
+ DAL — Data Access Layer (queries, data shaping)
29
+
30
+ Persistence (Drizzle ORM / database)
31
+ ```
32
+
33
+ ## Presentation Layer
34
+
35
+ - React Server Components and Client Components live here.
36
+ - Components call **facades only** — never services or DAL directly.
37
+ - RSC by default. Only add `"use client"` when strictly needed (hooks, events, browser APIs).
38
+ - Top-down design: if a component exceeds ~100 lines, extract sub-functions/sub-components.
39
+ - Functional over OOP — pure functions, composition, immutability.
40
+
41
+ ```tsx
42
+ // app/(app)/users/page.tsx — Presentation
43
+ import { getUserList } from "@/facades/user.facade";
44
+
45
+ export default async function UsersPage() {
46
+ const users = await getUserList();
47
+ return <UserTable users={users} />;
48
+ }
49
+ ```
50
+
51
+ ## Facade Layer
52
+
53
+ - Entry points for business logic. One facade per domain entity.
54
+ - Located in `src/facades/` — files named `{entity}.facade.ts`.
55
+ - Facades orchestrate service calls and transform data for the presentation.
56
+ - Facades handle auth context extraction and pass it down.
57
+ - Never contain business logic — only coordination.
58
+
59
+ ```ts
60
+ // src/facades/user.facade.ts
61
+ import { getCurrentUser } from "@/lib/auth";
62
+ import { listUsers, createUser } from "@/services/user.service";
63
+
64
+ export async function getUserList() {
65
+ const currentUser = await getCurrentUser();
66
+ return listUsers(currentUser);
67
+ }
68
+ ```
69
+
70
+ ## Service Layer
71
+
72
+ - Contains all business rules and authorization checks.
73
+ - Located in `src/services/` — files named `{entity}.service.ts`.
74
+ - Services receive the auth context as parameter — never fetch it themselves.
75
+ - CASL authorization checks happen here.
76
+ - Services call DAL functions for data access.
77
+
78
+ ```ts
79
+ // src/services/user.service.ts
80
+ import { ForbiddenError } from "@casl/ability";
81
+ import { defineAbilityFor } from "@/lib/casl";
82
+ import { findAllUsers } from "@/dal/user.dal";
83
+
84
+ export async function listUsers(currentUser: AuthUser) {
85
+ const ability = defineAbilityFor(currentUser);
86
+ ForbiddenError.from(ability).throwUnlessCan("read", "User");
87
+ return findAllUsers();
88
+ }
89
+ ```
90
+
91
+ ## Data Access Layer (DAL)
92
+
93
+ - Pure data access — no business logic, no authorization.
94
+ - Located in `src/dal/` — files named `{entity}.dal.ts`.
95
+ - DAL functions shape data: select specific columns, join relations, paginate.
96
+ - Always return typed results — never raw query results.
97
+
98
+ ```ts
99
+ // src/dal/user.dal.ts
100
+ import { db } from "@/lib/db";
101
+ import { users } from "@/schema";
102
+
103
+ export async function findAllUsers() {
104
+ return db.select({
105
+ id: users.id,
106
+ name: users.name,
107
+ email: users.email,
108
+ role: users.role,
109
+ }).from(users);
110
+ }
111
+ ```
112
+
113
+ ## Persistence Layer
114
+
115
+ - Drizzle ORM schema definitions and database client.
116
+ - Located in `src/schema/` for table definitions, `src/lib/db.ts` for the client.
117
+ - Schema-only — no query logic here.
118
+
119
+ ## File Naming Convention
120
+
121
+ ```
122
+ src/
123
+ facades/ # {entity}.facade.ts
124
+ services/ # {entity}.service.ts
125
+ dal/ # {entity}.dal.ts
126
+ schema/ # {entity}.ts (Drizzle table defs)
127
+ lib/db.ts # Database client
128
+ ```
129
+
130
+ - All new files use **kebab-case**.
131
+ - One file per entity per layer — avoid catch-all files.
@@ -1,11 +1,18 @@
1
1
  ---
2
2
  name: nextjs-conventions
3
- description: "Enforces Next.js 15+ / React 19 / TypeScript conventions and best practices. Use when working on any Next.js project to ensure consistent patterns."
4
- allowed-tools: "Read, Write, Edit, Glob, Grep"
3
+ description: "Next.js 15+ / React 19 / TypeScript conventions for App Router, RSC, route groups, and file naming. Use when creating pages, components, layouts, or structuring a Next.js application."
5
4
  ---
6
5
 
7
6
  # Next.js Conventions
8
7
 
8
+ ## Critical Rules
9
+
10
+ - **RSC by default** — only add `"use client"` when strictly needed.
11
+ - **Functional over OOP** — pure functions, composition, immutability. Never class components.
12
+ - **kebab-case for all new files** — `user-profile.tsx`, `format-date.ts`.
13
+ - **Top-down design** — extract sub-functions/sub-components when >100 lines.
14
+ - **Never use `any`** — use `unknown` if the type is truly unknown.
15
+
9
16
  ## App Router
10
17
 
11
18
  - Use the App Router (`src/app/`) exclusively. Never use the Pages Router.
@@ -13,14 +20,39 @@ allowed-tools: "Read, Write, Edit, Glob, Grep"
13
20
  - Use `loading.tsx` for Suspense boundaries and `error.tsx` for error boundaries.
14
21
  - Use `not-found.tsx` for 404 pages at the appropriate route level.
15
22
 
23
+ ## Route Groups
24
+
25
+ Organize routes by access level using route groups:
26
+
27
+ ```
28
+ src/app/
29
+ (public)/ # No auth required — landing, login, register
30
+ login/
31
+ register/
32
+ (auth)/ # Auth required, any role — onboarding
33
+ onboarding/
34
+ (app)/ # Auth required, active user — main app
35
+ dashboard/
36
+ settings/
37
+ admin/ # Admin only — user management, app settings
38
+ users/
39
+ settings/
40
+ ```
41
+
42
+ - `(public)` routes are accessible without authentication.
43
+ - `(auth)` routes require a session but no specific role.
44
+ - `(app)` routes require an active, verified user.
45
+ - `admin` routes require admin role — not a route group so the URL reflects `/admin/`.
46
+
16
47
  ## Server vs Client Components
17
48
 
18
- - **Default to Server Components**. Only add `"use client"` when you need:
49
+ - **RSC by default**. Only add `"use client"` when strictly needed:
19
50
  - Event handlers (`onClick`, `onChange`, etc.)
20
51
  - React hooks (`useState`, `useEffect`, `useRef`, etc.)
21
52
  - Browser-only APIs (`window`, `localStorage`, etc.)
22
53
  - Never import server-only modules in client components.
23
54
  - Pass server data to client components via props, not by importing server functions.
55
+ - Wrap async data in `<Suspense>` boundaries for streaming and progressive rendering.
24
56
 
25
57
  ## Data Fetching
26
58
 
@@ -31,10 +63,10 @@ allowed-tools: "Read, Write, Edit, Glob, Grep"
31
63
 
32
64
  ## File Naming
33
65
 
34
- - Components: `PascalCase.tsx` (e.g., `UserProfile.tsx`)
35
- - Utilities: `kebab-case.ts` (e.g., `format-date.ts`)
36
- - Types: define in `src/types/` with `.ts` extension
37
- - Server Actions: `src/actions/{entity}.actions.ts`
66
+ - **All new files: `kebab-case`** (e.g., `user-profile.tsx`, `format-date.ts`).
67
+ - Types: define in `src/types/` with `.ts` extension.
68
+ - Server Actions: `src/actions/{entity}.actions.ts`.
69
+ - Schemas: `src/schemas/{entity}.schema.ts`.
38
70
 
39
71
  ## TypeScript
40
72
 
@@ -62,3 +94,10 @@ allowed-tools: "Read, Write, Edit, Glob, Grep"
62
94
  - Use `next/link` for internal navigation. Never use `<a>` for internal links.
63
95
  - Use `next/font` for font loading.
64
96
  - Lazy load heavy client components with `dynamic()` from `next/dynamic`.
97
+
98
+ ## Code Design
99
+
100
+ - **Functional over OOP** — pure functions, composition, immutability. Never use class components.
101
+ - **Top-down design** — if a function or component exceeds ~100 lines, extract sub-functions or sub-components.
102
+ - Prefer named exports over default exports (except for page/layout components).
103
+ - Group imports: React/Next.js first, then external libs, then internal modules.
@@ -0,0 +1,87 @@
1
+ ---
2
+ name: react-native-patterns
3
+ description: "React Native and Expo best practices for navigation, styling, performance, and native features. Use when building mobile apps with Expo, implementing React Native navigation, styling with NativeWind, optimizing mobile performance, or adding push notifications."
4
+ ---
5
+
6
+ # React Native & Expo Patterns
7
+
8
+ ## Critical Rules
9
+
10
+ - **Always try Expo Go first** — only use `npx expo run:ios/android` when custom native modules are required.
11
+ - **Functional components only** — never class components.
12
+ - **Use `expo-image` instead of React Native's `Image`** — better caching and performance.
13
+ - **Use `react-native-safe-area-context`** — never React Native's built-in `SafeAreaView`.
14
+ - **Use `FlashList` instead of `FlatList`** for large lists — significantly better performance.
15
+ - **Never block the JS thread** — target 60fps, offload heavy computation to native.
16
+
17
+ ## Expo Router
18
+
19
+ - Use file-based routing in the `app/` directory exclusively.
20
+ - Never co-locate components, types, or utilities in the `app/` directory — keep them in `src/`.
21
+ - Always ensure a route matches `/` (may be inside a group route).
22
+ - Use layout routes (`_layout.tsx`) for shared navigation UI (tabs, stacks, drawers).
23
+ - Remove old route files immediately when restructuring navigation.
24
+ - Use kebab-case for file names: `comment-card.tsx`.
25
+
26
+ ## Styling with NativeWind
27
+
28
+ - Use NativeWind (Tailwind for React Native) as the primary styling method.
29
+ - Use `className` prop like web Tailwind — NativeWind compiles to StyleSheet.
30
+ - Use `cn()` helper for conditional classes.
31
+ - Prefer `useWindowDimensions` over `Dimensions.get()` for responsive layouts.
32
+ - Use flexbox exclusively for layout — avoid absolute positioning except for overlays.
33
+
34
+ ## Components
35
+
36
+ - Use functional components only — never class components.
37
+ - Use `expo-image` instead of React Native's `Image` — better caching and performance.
38
+ - Use `expo-image` with `source="sf:name"` for SF Symbols icons — not `@expo/vector-icons`.
39
+ - Use `react-native-safe-area-context` — never React Native's built-in `SafeAreaView`.
40
+ - Use `<ScrollView contentInsetAdjustmentBehavior="automatic" />` instead of `<SafeAreaView>` for root scrolling.
41
+ - Use `process.env.EXPO_OS` instead of `Platform.OS`.
42
+ - Use `React.use` instead of `React.useContext`.
43
+
44
+ ## Navigation
45
+
46
+ - Use Expo Router's typed routes for type-safe navigation.
47
+ - Handle deep linking and universal links from project start.
48
+ - Use native tabs (`NativeTabs`) for iOS tab navigation when possible.
49
+ - Preload frequently accessed screens for faster navigation.
50
+
51
+ ## Performance
52
+
53
+ - **Profile before optimizing** — measure with React DevTools and Xcode Instruments.
54
+ - Replace `ScrollView` with `FlashList` (not `FlatList`) for large lists.
55
+ - Use React Compiler for automatic memoization when available.
56
+ - Avoid barrel exports — import directly from source files to reduce bundle size.
57
+ - Keep JS thread work minimal — offload to native with Turbo Modules when needed.
58
+ - Target 60fps — never block the JS thread with heavy computation.
59
+ - Use `useDeferredValue` for expensive computations during rendering.
60
+
61
+ ## Animations
62
+
63
+ - Use `react-native-reanimated` for performant animations on the UI thread.
64
+ - Animate only `transform` and `opacity` — avoid animating layout properties.
65
+ - Use shared values and worklets for gesture-driven animations.
66
+ - Respect `prefers-reduced-motion` accessibility setting.
67
+
68
+ ## Offline & Storage
69
+
70
+ - Use `@react-native-async-storage/async-storage` or `react-native-mmkv` for local key-value storage.
71
+ - Use `expo-sqlite` for structured offline data.
72
+ - Implement optimistic updates for network operations.
73
+ - Cache critical data locally for offline-first experience.
74
+
75
+ ## Push Notifications
76
+
77
+ - Use `expo-notifications` for push notification setup.
78
+ - Request permission at a contextual moment — never on first launch.
79
+ - Handle notification responses (tap, dismiss) in the app root.
80
+ - Store push tokens in your backend database, linked to user profiles.
81
+
82
+ ## App Distribution
83
+
84
+ - Use EAS Build for creating production builds.
85
+ - Use EAS Submit for app store submissions.
86
+ - Configure `app.json` / `app.config.ts` thoroughly: icons, splash, permissions, version.
87
+ - Test on both iOS and Android — never assume platform parity.
@@ -0,0 +1,193 @@
1
+ ---
2
+ name: react-query-patterns
3
+ description: "TanStack React Query for data fetching, caching, mutations, and optimistic updates. Use when managing server state in client components, implementing data fetching hooks, or caching API responses."
4
+ ---
5
+
6
+ # React Query Patterns
7
+
8
+ ## Critical Rules
9
+
10
+ - **Query keys are structured** — use the factory pattern from `query-keys.ts`.
11
+ - **Custom hooks for reuse** — wrap `useQuery`/`useMutation` in domain hooks.
12
+ - **Invalidate on mutation** — always invalidate related queries after writes.
13
+ - **Optimistic updates for UX** — use for edits and deletes where latency matters.
14
+ - **Prefetch in RSC** — hydrate queries in server components for instant loads.
15
+ - **Never fetch in useEffect** — use React Query instead.
16
+
17
+ ## Setup
18
+
19
+ ```tsx
20
+ // src/providers/query-provider.tsx
21
+ "use client";
22
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
23
+ import { useState } from "react";
24
+
25
+ export function QueryProvider({ children }: { children: React.ReactNode }) {
26
+ const [queryClient] = useState(
27
+ () =>
28
+ new QueryClient({
29
+ defaultOptions: {
30
+ queries: {
31
+ staleTime: 60 * 1000, // 1 minute
32
+ refetchOnWindowFocus: false,
33
+ },
34
+ },
35
+ })
36
+ );
37
+
38
+ return (
39
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
40
+ );
41
+ }
42
+ ```
43
+
44
+ ## Query Keys Convention
45
+
46
+ ```ts
47
+ // src/lib/query-keys.ts
48
+ export const queryKeys = {
49
+ users: {
50
+ all: ["users"] as const,
51
+ list: (filters: UserFilters) => ["users", "list", filters] as const,
52
+ detail: (id: string) => ["users", "detail", id] as const,
53
+ },
54
+ posts: {
55
+ all: ["posts"] as const,
56
+ list: (filters: PostFilters) => ["posts", "list", filters] as const,
57
+ detail: (id: string) => ["posts", "detail", id] as const,
58
+ comments: (postId: string) => ["posts", postId, "comments"] as const,
59
+ },
60
+ } as const;
61
+ ```
62
+
63
+ ## Queries
64
+
65
+ ### Basic Query
66
+
67
+ ```tsx
68
+ "use client";
69
+ import { useQuery } from "@tanstack/react-query";
70
+ import { queryKeys } from "@/lib/query-keys";
71
+
72
+ export function UserList() {
73
+ const { data: users, isLoading, error } = useQuery({
74
+ queryKey: queryKeys.users.all,
75
+ queryFn: () => fetch("/api/users").then((r) => r.json()),
76
+ });
77
+
78
+ if (isLoading) return <Skeleton />;
79
+ if (error) return <ErrorMessage error={error} />;
80
+
81
+ return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
82
+ }
83
+ ```
84
+
85
+ ### Query with Server Action
86
+
87
+ ```tsx
88
+ import { useQuery } from "@tanstack/react-query";
89
+ import { getUserList } from "@/actions/user.actions";
90
+
91
+ const { data } = useQuery({
92
+ queryKey: queryKeys.users.list(filters),
93
+ queryFn: () => getUserList(filters),
94
+ });
95
+ ```
96
+
97
+ ## Mutations
98
+
99
+ ### Basic Mutation
100
+
101
+ ```tsx
102
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
103
+ import { createUser } from "@/actions/user.actions";
104
+ import { queryKeys } from "@/lib/query-keys";
105
+ import { toast } from "sonner";
106
+
107
+ export function useCreateUser() {
108
+ const queryClient = useQueryClient();
109
+
110
+ return useMutation({
111
+ mutationFn: createUser,
112
+ onSuccess: () => {
113
+ queryClient.invalidateQueries({ queryKey: queryKeys.users.all });
114
+ toast.success("User created");
115
+ },
116
+ onError: (error) => {
117
+ toast.error(error.message);
118
+ },
119
+ });
120
+ }
121
+ ```
122
+
123
+ ### Optimistic Update
124
+
125
+ ```tsx
126
+ export function useUpdateUser() {
127
+ const queryClient = useQueryClient();
128
+
129
+ return useMutation({
130
+ mutationFn: ({ id, data }: { id: string; data: UpdateUserInput }) =>
131
+ updateUser(id, data),
132
+ onMutate: async ({ id, data }) => {
133
+ await queryClient.cancelQueries({ queryKey: queryKeys.users.detail(id) });
134
+ const previous = queryClient.getQueryData(queryKeys.users.detail(id));
135
+ queryClient.setQueryData(queryKeys.users.detail(id), (old: User) => ({
136
+ ...old,
137
+ ...data,
138
+ }));
139
+ return { previous };
140
+ },
141
+ onError: (_err, { id }, context) => {
142
+ queryClient.setQueryData(queryKeys.users.detail(id), context?.previous);
143
+ },
144
+ onSettled: (_data, _err, { id }) => {
145
+ queryClient.invalidateQueries({ queryKey: queryKeys.users.detail(id) });
146
+ },
147
+ });
148
+ }
149
+ ```
150
+
151
+ ## Custom Hooks
152
+
153
+ ```ts
154
+ // src/hooks/use-users.ts
155
+ export function useUsers(filters?: UserFilters) {
156
+ return useQuery({
157
+ queryKey: queryKeys.users.list(filters ?? {}),
158
+ queryFn: () => getUserList(filters),
159
+ });
160
+ }
161
+
162
+ export function useUser(id: string) {
163
+ return useQuery({
164
+ queryKey: queryKeys.users.detail(id),
165
+ queryFn: () => getUserById(id),
166
+ enabled: !!id,
167
+ });
168
+ }
169
+ ```
170
+
171
+ ## Prefetching with RSC
172
+
173
+ ```tsx
174
+ // app/users/page.tsx (Server Component)
175
+ import { HydrationBoundary, dehydrate, QueryClient } from "@tanstack/react-query";
176
+ import { getUserList } from "@/actions/user.actions";
177
+ import { queryKeys } from "@/lib/query-keys";
178
+ import { UserList } from "./user-list";
179
+
180
+ export default async function UsersPage() {
181
+ const queryClient = new QueryClient();
182
+ await queryClient.prefetchQuery({
183
+ queryKey: queryKeys.users.all,
184
+ queryFn: getUserList,
185
+ });
186
+
187
+ return (
188
+ <HydrationBoundary state={dehydrate(queryClient)}>
189
+ <UserList />
190
+ </HydrationBoundary>
191
+ );
192
+ }
193
+ ```