@baldurpan/create-ai-workflow 0.2.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/LICENSE +21 -0
- package/README.md +165 -0
- package/dist/agents-block.js +70 -0
- package/dist/bin.js +14 -0
- package/dist/check/markdown.js +47 -0
- package/dist/check/parse.js +113 -0
- package/dist/check/rules.js +245 -0
- package/dist/cli.js +109 -0
- package/dist/commands/check.js +41 -0
- package/dist/commands/install.js +76 -0
- package/dist/commands/standards-add.js +145 -0
- package/dist/commands/update.js +214 -0
- package/dist/layout.js +115 -0
- package/dist/log.js +20 -0
- package/dist/manifest.js +53 -0
- package/dist/paths.js +41 -0
- package/package.json +37 -0
- package/templates/blocks/agents-block.md +30 -0
- package/templates/claude/agents/planner.agent.md +60 -0
- package/templates/claude/agents/reviewer.agent.md +77 -0
- package/templates/context/README.md +81 -0
- package/templates/context/plan-template.md +69 -0
- package/templates/context/plan-template.notes.md +55 -0
- package/templates/context/roles/coder.md +76 -0
- package/templates/context/workflow.md +143 -0
- package/templates/skills/feature-close/SKILL.md +102 -0
- package/templates/skills/feature-implement/SKILL.md +142 -0
- package/templates/skills/feature-plan/SKILL.md +138 -0
- package/templates/skills/feature-status/SKILL.md +86 -0
- package/templates/skills/onboard/SKILL.md +122 -0
- package/templates/skills/orchestrate/SKILL.md +92 -0
- package/templates/skills/roadmap/SKILL.md +97 -0
- package/templates/standards/.source +8 -0
- package/templates/standards/README.md +104 -0
- package/templates/standards/architecture/api-design.md +212 -0
- package/templates/standards/architecture/dependency-boundaries.md +104 -0
- package/templates/standards/architecture/feature-driven.md +102 -0
- package/templates/standards/architecture/folder-structure.md +108 -0
- package/templates/standards/architecture/monorepos.md +107 -0
- package/templates/standards/architecture/refactoring.md +97 -0
- package/templates/standards/architecture/shared-code.md +81 -0
- package/templates/standards/docs/PHP-SPEC.md +1195 -0
- package/templates/standards/docs/SPEC.md +540 -0
- package/templates/standards/examples/bad/data-fetching.tsx +52 -0
- package/templates/standards/examples/bad/derived-state.tsx +60 -0
- package/templates/standards/examples/bad/feature-component.tsx +80 -0
- package/templates/standards/examples/bad/prop-drilling.tsx +70 -0
- package/templates/standards/examples/good/data-fetching.tsx +53 -0
- package/templates/standards/examples/good/derived-state.tsx +52 -0
- package/templates/standards/examples/good/feature-component.tsx +141 -0
- package/templates/standards/examples/good/invoice.schema.ts +20 -0
- package/templates/standards/examples/good/prop-drilling.tsx +83 -0
- package/templates/standards/philosophy/ai-agent-behavior.md +93 -0
- package/templates/standards/philosophy/core-principles.md +49 -0
- package/templates/standards/philosophy/incremental-abstraction.md +56 -0
- package/templates/standards/philosophy/maintainability.md +66 -0
- package/templates/standards/philosophy/readability.md +87 -0
- package/templates/standards/php/README.md +62 -0
- package/templates/standards/php/anti-patterns.md +259 -0
- package/templates/standards/php/architecture.md +241 -0
- package/templates/standards/php/frameworks.md +197 -0
- package/templates/standards/php/rules.md +205 -0
- package/templates/standards/php/security.md +190 -0
- package/templates/standards/php/testing.md +139 -0
- package/templates/standards/php/tooling.md +245 -0
- package/templates/standards/react/accessibility.md +130 -0
- package/templates/standards/react/anti-patterns.md +187 -0
- package/templates/standards/react/component-design.md +139 -0
- package/templates/standards/react/error-boundaries.md +195 -0
- package/templates/standards/react/forms.md +147 -0
- package/templates/standards/react/hooks.md +88 -0
- package/templates/standards/react/memoization.md +87 -0
- package/templates/standards/react/state-management.md +123 -0
- package/templates/standards/react/testing.md +118 -0
- package/templates/standards/react/use-effect.md +110 -0
- package/templates/standards/security/api-security.md +136 -0
- package/templates/standards/security/auth.md +112 -0
- package/templates/standards/security/secrets.md +105 -0
- package/templates/standards/security/validation.md +89 -0
- package/templates/standards/templates/.editorconfig +16 -0
- package/templates/standards/templates/.nvmrc +1 -0
- package/templates/standards/templates/AGENTS.md +46 -0
- package/templates/standards/templates/CLAUDE.md +3 -0
- package/templates/standards/templates/_dot_gitignore +50 -0
- package/templates/standards/templates/biome.json +53 -0
- package/templates/standards/templates/eslint.config.js +61 -0
- package/templates/standards/templates/project-readme.md +76 -0
- package/templates/standards/templates/pull-request-template.md +46 -0
- package/templates/standards/templates/tsconfig.json +35 -0
- package/templates/standards/tooling/biome.md +92 -0
- package/templates/standards/tooling/ci.md +128 -0
- package/templates/standards/tooling/dates.md +194 -0
- package/templates/standards/tooling/dependencies.md +90 -0
- package/templates/standards/tooling/nx.md +113 -0
- package/templates/standards/tooling/observability.md +183 -0
- package/templates/standards/tooling/prisma.md +152 -0
- package/templates/standards/tooling/shadcn.md +115 -0
- package/templates/standards/tooling/tailwind.md +135 -0
- package/templates/standards/tooling/tanstack-query.md +214 -0
- package/templates/standards/tooling/tanstack-router.md +365 -0
- package/templates/standards/tooling/tanstack.md +31 -0
- package/templates/standards/tooling/vite.md +135 -0
- package/templates/standards/typescript/anti-patterns.md +107 -0
- package/templates/standards/typescript/error-handling.md +216 -0
- package/templates/standards/typescript/naming.md +78 -0
- package/templates/standards/typescript/rules.md +159 -0
- package/templates/standards/typescript/tsconfig/base.json +32 -0
- package/templates/standards/typescript/tsconfig/nextjs.json +12 -0
- package/templates/standards/typescript/tsconfig/react.json +9 -0
- package/templates/standards/typescript/validation.md +104 -0
- package/templates/stubs/executors.md +49 -0
- package/templates/stubs/findings.md +40 -0
- package/templates/stubs/history.md +13 -0
- package/templates/stubs/roadmap.md +29 -0
- package/templates/stubs/stack.md +35 -0
- package/templates/stubs/verify.md +48 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Component Design
|
|
2
|
+
|
|
3
|
+
## Core Rules
|
|
4
|
+
|
|
5
|
+
- One file, one primary responsibility
|
|
6
|
+
- Prefer feature-driven colocation over organization-by-type
|
|
7
|
+
- Prefer composition over prop drilling
|
|
8
|
+
- Avoid gigantic files — large files signal decomposition is needed
|
|
9
|
+
|
|
10
|
+
## Feature-Driven Colocation
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
features/
|
|
14
|
+
invoices/
|
|
15
|
+
InvoiceTable.tsx
|
|
16
|
+
InvoiceTable.test.tsx
|
|
17
|
+
InvoiceRow.tsx
|
|
18
|
+
InvoiceFilters.tsx
|
|
19
|
+
useInvoiceSort.ts
|
|
20
|
+
invoice.types.ts
|
|
21
|
+
invoice.api.ts
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Do not scatter invoice logic across `components/`, `hooks/`, `types/`, `api/` at the root level.
|
|
25
|
+
|
|
26
|
+
## DO
|
|
27
|
+
|
|
28
|
+
- Keep components reasonably scoped — render one thing well
|
|
29
|
+
- Decompose when a file becomes hard to scan
|
|
30
|
+
- Use composition to avoid deep prop drilling
|
|
31
|
+
- Use compound components for tightly coupled UI groups
|
|
32
|
+
- Prefer explicit props over spreading unknown props onto DOM elements
|
|
33
|
+
- Forward refs with `forwardRef` when wrapping native elements
|
|
34
|
+
|
|
35
|
+
## DO NOT
|
|
36
|
+
|
|
37
|
+
- Pass more than 4–5 props through intermediate components without reconsidering the design
|
|
38
|
+
- Put business logic directly in JSX — extract to a variable or function
|
|
39
|
+
- Use default exports for everything — named exports improve refactorability
|
|
40
|
+
- Render large lists inline without key management and performance consideration
|
|
41
|
+
|
|
42
|
+
## Compound Components
|
|
43
|
+
|
|
44
|
+
Use compound components when a group of UI elements are tightly coupled and share state:
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
// Usage
|
|
48
|
+
<Tabs defaultValue="overview">
|
|
49
|
+
<Tabs.List>
|
|
50
|
+
<Tabs.Trigger value="overview">Overview</Tabs.Trigger>
|
|
51
|
+
<Tabs.Trigger value="details">Details</Tabs.Trigger>
|
|
52
|
+
</Tabs.List>
|
|
53
|
+
<Tabs.Panel value="overview"><Overview /></Tabs.Panel>
|
|
54
|
+
<Tabs.Panel value="details"><Details /></Tabs.Panel>
|
|
55
|
+
</Tabs>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Prop Design
|
|
59
|
+
|
|
60
|
+
Define props as a named `interface` above the component. Do not define prop types inline.
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
// GOOD — named interface above the component
|
|
64
|
+
interface InvoiceTableProps {
|
|
65
|
+
invoices: Invoice[];
|
|
66
|
+
onRowClick: (id: string) => void;
|
|
67
|
+
isLoading?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function InvoiceTable({ invoices, onRowClick, isLoading }: InvoiceTableProps) {
|
|
71
|
+
...
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// BAD — inline object type
|
|
75
|
+
export function InvoiceTable({
|
|
76
|
+
invoices,
|
|
77
|
+
onRowClick,
|
|
78
|
+
}: {
|
|
79
|
+
invoices: Invoice[];
|
|
80
|
+
onRowClick: (id: string) => void;
|
|
81
|
+
}) {
|
|
82
|
+
...
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// BAD — type alias when interface would do
|
|
86
|
+
type InvoiceTableProps = {
|
|
87
|
+
invoices: Invoice[];
|
|
88
|
+
onRowClick: (id: string) => void;
|
|
89
|
+
};
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Naming: suffix the interface with `Props` and prefix with the component name (`InvoiceTableProps`, not `Props`).
|
|
93
|
+
|
|
94
|
+
### Variant Props
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
// BAD — boolean props for variants
|
|
98
|
+
<Button primary large disabled />
|
|
99
|
+
|
|
100
|
+
// GOOD — explicit variant props
|
|
101
|
+
<Button variant="primary" size="lg" disabled />
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Avoid Spreading Unknown Props
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
// BAD — spreading unknown props onto DOM
|
|
108
|
+
function Card({ className, ...rest }: unknown) {
|
|
109
|
+
return <div className={className} {...rest} />;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// GOOD — explicit allowed props
|
|
113
|
+
interface CardProps {
|
|
114
|
+
className?: string;
|
|
115
|
+
children: React.ReactNode;
|
|
116
|
+
onClick?: () => void;
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## File Size Signals
|
|
121
|
+
|
|
122
|
+
Trigger a decomposition review when:
|
|
123
|
+
- A file exceeds ~300 lines
|
|
124
|
+
- A component handles multiple unrelated concerns
|
|
125
|
+
- Props grow beyond 8–10 items
|
|
126
|
+
- JSX exceeds what fits on a single screen
|
|
127
|
+
|
|
128
|
+
## PRIORITY
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
Single responsibility > Reuse > Brevity
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## See Also
|
|
135
|
+
|
|
136
|
+
- [hooks.md](hooks.md)
|
|
137
|
+
- [state-management.md](state-management.md)
|
|
138
|
+
- [accessibility.md](accessibility.md)
|
|
139
|
+
- [anti-patterns.md](anti-patterns.md)
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Error Boundaries and Suspense
|
|
2
|
+
|
|
3
|
+
## Core Rules
|
|
4
|
+
|
|
5
|
+
- Wrap async / risky UI subtrees in error boundaries — never let a render error crash the whole app
|
|
6
|
+
- Co-locate boundaries with the feature they protect
|
|
7
|
+
- Pair `<Suspense>` with `<ErrorBoundary>` — loading and error states are two sides of the same boundary
|
|
8
|
+
- Provide actionable error UI — explain what failed and what the user can do
|
|
9
|
+
|
|
10
|
+
## Error Boundaries
|
|
11
|
+
|
|
12
|
+
React error boundaries catch errors thrown during render, in lifecycle methods, and in constructors of descendant components. They do **not** catch:
|
|
13
|
+
|
|
14
|
+
- Errors in event handlers (use try/catch or `.catch()`)
|
|
15
|
+
- Errors in async code (handle with TanStack Query's `onError` or `try/catch`)
|
|
16
|
+
- Errors in the boundary itself
|
|
17
|
+
|
|
18
|
+
Use [`react-error-boundary`](https://github.com/bvaughn/react-error-boundary) — it provides a clean hook + component API and avoids the class component boilerplate:
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
import { ErrorBoundary } from "react-error-boundary";
|
|
22
|
+
|
|
23
|
+
function InvoiceErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
|
|
24
|
+
return (
|
|
25
|
+
<div role="alert">
|
|
26
|
+
<p>We couldn't load your invoices. Please try again.</p>
|
|
27
|
+
<p><small>Error: {error.message}</small></p>
|
|
28
|
+
<button type="button" onClick={resetErrorBoundary}>Retry</button>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function InvoicesPage() {
|
|
34
|
+
return (
|
|
35
|
+
<ErrorBoundary FallbackComponent={InvoiceErrorFallback}>
|
|
36
|
+
<InvoiceList />
|
|
37
|
+
</ErrorBoundary>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Where to Place Boundaries
|
|
43
|
+
|
|
44
|
+
Place boundaries at meaningful UI seams. There are two layers:
|
|
45
|
+
|
|
46
|
+
**Route-level** — use TanStack Router's `errorComponent` and `pendingComponent` for failures and loading during route transitions (loader runs, then renders). See [`../tooling/tanstack-router.md`](../tooling/tanstack-router.md).
|
|
47
|
+
|
|
48
|
+
**Component-level** — use `<ErrorBoundary>` and `<Suspense>` for independent widgets within a page that can fail or load separately from the route as a whole.
|
|
49
|
+
|
|
50
|
+
Typical layout:
|
|
51
|
+
- One route-level `errorComponent` per route
|
|
52
|
+
- Component-level boundaries around independent widgets (dashboard panels, charts, third-party embeds)
|
|
53
|
+
- One component-level boundary around any subtree that suspends independently of the route loader
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
// Multiple boundaries — one failing widget doesn't kill the dashboard
|
|
57
|
+
function Dashboard() {
|
|
58
|
+
return (
|
|
59
|
+
<Layout>
|
|
60
|
+
<ErrorBoundary FallbackComponent={WidgetErrorFallback}>
|
|
61
|
+
<RevenueChart />
|
|
62
|
+
</ErrorBoundary>
|
|
63
|
+
<ErrorBoundary FallbackComponent={WidgetErrorFallback}>
|
|
64
|
+
<RecentInvoices />
|
|
65
|
+
</ErrorBoundary>
|
|
66
|
+
<ErrorBoundary FallbackComponent={WidgetErrorFallback}>
|
|
67
|
+
<NotificationsPanel />
|
|
68
|
+
</ErrorBoundary>
|
|
69
|
+
</Layout>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Suspense for Loading States
|
|
75
|
+
|
|
76
|
+
`<Suspense>` declaratively handles loading states for lazy components, suspended TanStack Query calls (with `useSuspenseQuery`), and React Server Components.
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import { Suspense, lazy } from "react";
|
|
80
|
+
|
|
81
|
+
const InvoiceTable = lazy(() => import("./InvoiceTable"));
|
|
82
|
+
|
|
83
|
+
export function InvoicesPage() {
|
|
84
|
+
return (
|
|
85
|
+
<ErrorBoundary FallbackComponent={InvoiceErrorFallback}>
|
|
86
|
+
<Suspense fallback={<TableSkeleton />}>
|
|
87
|
+
<InvoiceTable />
|
|
88
|
+
</Suspense>
|
|
89
|
+
</ErrorBoundary>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Pairing Error and Loading Boundaries
|
|
95
|
+
|
|
96
|
+
Loading + error are two sides of the same boundary. Always declare them together:
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
|
100
|
+
<Suspense fallback={<LoadingFallback />}>
|
|
101
|
+
<FeatureContent />
|
|
102
|
+
</Suspense>
|
|
103
|
+
</ErrorBoundary>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Suspense with TanStack Query
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
import { useSuspenseQuery } from "@tanstack/react-query";
|
|
110
|
+
|
|
111
|
+
function InvoiceTable() {
|
|
112
|
+
// No isLoading / isError branching — the boundaries handle it
|
|
113
|
+
const { data: invoices } = useSuspenseQuery({
|
|
114
|
+
queryKey: ["invoices"],
|
|
115
|
+
queryFn: fetchInvoices,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return <table>...</table>;
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Loading and Empty States
|
|
123
|
+
|
|
124
|
+
A complete data-fetching UI has four states:
|
|
125
|
+
|
|
126
|
+
- **Loading** — handled by `<Suspense>` or `isLoading` flag
|
|
127
|
+
- **Error** — handled by `<ErrorBoundary>` or `error` flag
|
|
128
|
+
- **Empty** — no data found; show a helpful empty state, not a blank screen
|
|
129
|
+
- **Success** — render the data
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
if (invoices.length === 0) {
|
|
133
|
+
return (
|
|
134
|
+
<div>
|
|
135
|
+
<p>No invoices yet.</p>
|
|
136
|
+
<button type="button" onClick={onCreate}>Create your first invoice</button>
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Skeleton Loaders
|
|
143
|
+
|
|
144
|
+
For above-the-fold content, use skeleton loaders (placeholder shapes) instead of spinners — they reduce perceived load time and prevent layout shift.
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
function TableSkeleton() {
|
|
148
|
+
return (
|
|
149
|
+
<div aria-busy="true" aria-label="Loading invoices">
|
|
150
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
151
|
+
<div key={i} className="h-12 bg-muted animate-pulse rounded mb-2" />
|
|
152
|
+
))}
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Reporting Errors
|
|
159
|
+
|
|
160
|
+
In an `<ErrorBoundary>`, send caught errors to your error monitoring service:
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
<ErrorBoundary
|
|
164
|
+
FallbackComponent={InvoiceErrorFallback}
|
|
165
|
+
onError={(error, info) => {
|
|
166
|
+
reportError(error, { componentStack: info.componentStack });
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
<InvoiceList />
|
|
170
|
+
</ErrorBoundary>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
See [`tooling/observability.md`](../tooling/observability.md) for error monitoring patterns.
|
|
174
|
+
|
|
175
|
+
## DO NOT
|
|
176
|
+
|
|
177
|
+
- Wrap the entire app in a single error boundary — a leaf failure should not blank the whole UI
|
|
178
|
+
- Show a generic "Something went wrong" message — give specific, actionable feedback
|
|
179
|
+
- Swallow the error in the fallback without reporting it
|
|
180
|
+
- Use `<Suspense>` for things that don't actually suspend (regular `useState`, non-suspended queries)
|
|
181
|
+
|
|
182
|
+
## PRIORITY
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
Co-located boundaries > Single global boundary
|
|
186
|
+
Specific error UI > Generic fallback
|
|
187
|
+
Suspense + ErrorBoundary together > Either alone
|
|
188
|
+
Route-level errorComponent (route transitions) > Component-level ErrorBoundary (widgets)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## See Also
|
|
192
|
+
|
|
193
|
+
- [`../tooling/tanstack-router.md`](../tooling/tanstack-router.md) — route-level `pendingComponent` / `errorComponent`
|
|
194
|
+
- [`../tooling/tanstack-query.md`](../tooling/tanstack-query.md) — `useSuspenseQuery` for component-level boundaries
|
|
195
|
+
- [`../tooling/observability.md`](../tooling/observability.md) — reporting caught errors to monitoring
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Forms
|
|
2
|
+
|
|
3
|
+
## Core Rules
|
|
4
|
+
|
|
5
|
+
- Prefer controlled inputs
|
|
6
|
+
- Validate with a shared Zod schema (used on both client and server)
|
|
7
|
+
- Use optimistic updates for responsive UIs
|
|
8
|
+
- Handle loading and error states explicitly
|
|
9
|
+
|
|
10
|
+
## Preferred Tooling
|
|
11
|
+
|
|
12
|
+
**TanStack Form** is the preferred choice — integrates with TanStack Query and supports schema validation natively.
|
|
13
|
+
|
|
14
|
+
**React Hook Form** is an acceptable alternative — mature, well-documented, minimal re-renders.
|
|
15
|
+
|
|
16
|
+
Do not build custom form state management with `useState` arrays.
|
|
17
|
+
|
|
18
|
+
## TanStack Form Pattern
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
import { useForm } from "@tanstack/react-form";
|
|
22
|
+
import { zodValidator } from "@tanstack/zod-form-adapter";
|
|
23
|
+
import { createInvoiceSchema } from "~/features/invoices/invoice.schema";
|
|
24
|
+
|
|
25
|
+
function CreateInvoiceForm() {
|
|
26
|
+
const form = useForm({
|
|
27
|
+
defaultValues: { title: "", amount: 0 },
|
|
28
|
+
validatorAdapter: zodValidator(),
|
|
29
|
+
validators: {
|
|
30
|
+
onChange: createInvoiceSchema,
|
|
31
|
+
},
|
|
32
|
+
onSubmit: async ({ value }) => {
|
|
33
|
+
await createInvoice(value);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<form onSubmit={(e) => { e.preventDefault(); form.handleSubmit(); }}>
|
|
39
|
+
<form.Field
|
|
40
|
+
name="title"
|
|
41
|
+
children={(field) => (
|
|
42
|
+
<label>
|
|
43
|
+
Title
|
|
44
|
+
<input
|
|
45
|
+
value={field.state.value}
|
|
46
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
47
|
+
/>
|
|
48
|
+
{field.state.meta.errors.map((error) => (
|
|
49
|
+
<span key={error} role="alert">{error}</span>
|
|
50
|
+
))}
|
|
51
|
+
</label>
|
|
52
|
+
)}
|
|
53
|
+
/>
|
|
54
|
+
<button type="submit">Create</button>
|
|
55
|
+
</form>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## React Hook Form Pattern
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
import { useForm } from "react-hook-form";
|
|
64
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
65
|
+
import { type CreateInvoice, createInvoiceSchema } from "~/features/invoices/invoice.schema";
|
|
66
|
+
|
|
67
|
+
function CreateInvoiceForm() {
|
|
68
|
+
const {
|
|
69
|
+
register,
|
|
70
|
+
handleSubmit,
|
|
71
|
+
formState: { errors, isSubmitting },
|
|
72
|
+
} = useForm<CreateInvoice>({
|
|
73
|
+
resolver: zodResolver(createInvoiceSchema),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const onSubmit = async (data: CreateInvoice) => {
|
|
77
|
+
await createInvoice(data);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
82
|
+
<label htmlFor="title">Title</label>
|
|
83
|
+
<input id="title" {...register("title")} />
|
|
84
|
+
{errors.title && <span role="alert">{errors.title.message}</span>}
|
|
85
|
+
<button type="submit" disabled={isSubmitting}>Create</button>
|
|
86
|
+
</form>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Shared Schema
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
// features/invoices/invoice.schema.ts
|
|
95
|
+
import { z } from "zod";
|
|
96
|
+
|
|
97
|
+
export const createInvoiceSchema = z.object({
|
|
98
|
+
title: z.string().min(1, "Title is required").max(200),
|
|
99
|
+
amount: z.number().positive("Amount must be positive"),
|
|
100
|
+
dueDate: z.coerce.date().min(new Date(), "Due date must be in the future"),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
export type CreateInvoice = z.infer<typeof createInvoiceSchema>;
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Optimistic Updates
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
const { mutate } = useMutation({
|
|
110
|
+
mutationFn: createInvoice,
|
|
111
|
+
onMutate: async (newInvoice) => {
|
|
112
|
+
await queryClient.cancelQueries({ queryKey: ["invoices"] });
|
|
113
|
+
const previous = queryClient.getQueryData(["invoices"]);
|
|
114
|
+
queryClient.setQueryData(["invoices"], (old: Invoice[]) => [
|
|
115
|
+
...old,
|
|
116
|
+
{ ...newInvoice, id: "temp", status: "pending" },
|
|
117
|
+
]);
|
|
118
|
+
return { previous };
|
|
119
|
+
},
|
|
120
|
+
onError: (_, __, context) => {
|
|
121
|
+
queryClient.setQueryData(["invoices"], context?.previous);
|
|
122
|
+
},
|
|
123
|
+
onSettled: () => {
|
|
124
|
+
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## DO NOT
|
|
130
|
+
|
|
131
|
+
- Build form state with multiple `useState` calls
|
|
132
|
+
- Duplicate validation logic between client and server
|
|
133
|
+
- Submit without disabling the submit button during submission
|
|
134
|
+
- Leave error states unhandled
|
|
135
|
+
|
|
136
|
+
## PRIORITY
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
Schema validation > Controlled inputs > Optimistic updates
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## See Also
|
|
143
|
+
|
|
144
|
+
- [`../typescript/validation.md`](../typescript/validation.md) — Zod schema patterns
|
|
145
|
+
- [`accessibility.md`](accessibility.md) — labels, errors, keyboard support
|
|
146
|
+
- [`../tooling/tanstack-query.md`](../tooling/tanstack-query.md) — mutations and cache invalidation
|
|
147
|
+
- [`../tooling/shadcn.md`](../tooling/shadcn.md) — shadcn/ui form components
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Hooks
|
|
2
|
+
|
|
3
|
+
## Core Rules
|
|
4
|
+
|
|
5
|
+
- Use hooks to encapsulate reusable stateful behavior
|
|
6
|
+
- Keep hooks composable — hooks should call other hooks, not re-implement logic
|
|
7
|
+
- Before writing a custom hook, check if a solution already exists
|
|
8
|
+
|
|
9
|
+
## Check First
|
|
10
|
+
|
|
11
|
+
Before writing a custom hook:
|
|
12
|
+
|
|
13
|
+
1. Check [`@uidotdev/usehooks`](https://usehooks.com/) — covers most common patterns
|
|
14
|
+
2. Check if TanStack Query, TanStack Router, or Zustand handles your case
|
|
15
|
+
3. Check if a plain utility function (non-hook) is sufficient
|
|
16
|
+
|
|
17
|
+
## DO
|
|
18
|
+
|
|
19
|
+
- Name hooks with the `use` prefix
|
|
20
|
+
- Keep hooks focused on a single behavior
|
|
21
|
+
- Compose hooks from smaller hooks
|
|
22
|
+
- Return only what the caller needs — avoid returning large objects when specific values suffice
|
|
23
|
+
- Expose stable references with `useCallback` when callbacks are drilled or passed to external APIs
|
|
24
|
+
|
|
25
|
+
## DO NOT
|
|
26
|
+
|
|
27
|
+
- Create hook mini-frameworks with complex internal state machines
|
|
28
|
+
- Wrap `useState` purely to rename it — that is indirection, not abstraction
|
|
29
|
+
- Hide side effects or subscriptions without documenting them
|
|
30
|
+
- Create hooks that only one component ever calls (unless the hook isolates complexity)
|
|
31
|
+
- Use hooks to share instance state between unrelated components — use context or a store instead
|
|
32
|
+
|
|
33
|
+
## Good Hook Shape
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// Focused, composable, clear return
|
|
37
|
+
function useDisclosure(defaultOpen = false) {
|
|
38
|
+
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
39
|
+
const open = useCallback(() => setIsOpen(true), []);
|
|
40
|
+
const close = useCallback(() => setIsOpen(false), []);
|
|
41
|
+
const toggle = useCallback(() => setIsOpen((prev) => !prev), []);
|
|
42
|
+
return { isOpen, open, close, toggle };
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Bad Hook Shape
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// BAD — wraps useState purely for renaming
|
|
50
|
+
function useModalOpen() {
|
|
51
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
52
|
+
return [isOpen, setIsOpen] as const;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// BAD — hidden side effect with no documentation
|
|
56
|
+
function useUserData(userId: string) {
|
|
57
|
+
const [data, setData] = useState(null);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
// secretly sends analytics on every render
|
|
60
|
+
track("user-viewed");
|
|
61
|
+
fetchUser(userId).then(setData);
|
|
62
|
+
}, [userId]);
|
|
63
|
+
return data;
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Composing Hooks
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
function useInvoiceTable(filters: InvoiceFilters) {
|
|
71
|
+
const { data, isLoading } = useInvoices(filters); // data fetching
|
|
72
|
+
const { sorted, sortKey, setSortKey } = useSort(data); // sort behavior
|
|
73
|
+
const { paginated, page, setPage } = usePagination(sorted); // pagination
|
|
74
|
+
return { invoices: paginated, isLoading, sortKey, setSortKey, page, setPage };
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## PRIORITY
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
Composability > Encapsulation > Abstraction
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## See Also
|
|
85
|
+
|
|
86
|
+
- [use-effect.md](use-effect.md)
|
|
87
|
+
- [memoization.md](memoization.md)
|
|
88
|
+
- [state-management.md](state-management.md)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Memoization
|
|
2
|
+
|
|
3
|
+
## Core Rule
|
|
4
|
+
|
|
5
|
+
Memoize proven performance bottlenecks. Do not memoize speculatively.
|
|
6
|
+
|
|
7
|
+
## DO
|
|
8
|
+
|
|
9
|
+
- Use `useCallback` for callbacks passed to heavily optimized child components or external APIs that need stable references
|
|
10
|
+
- Use `useMemo` for computationally expensive operations that have been profiled
|
|
11
|
+
- Use `React.memo` on components that receive stable props and re-render frequently with identical inputs
|
|
12
|
+
|
|
13
|
+
## DO NOT
|
|
14
|
+
|
|
15
|
+
- Wrap every function in `useCallback` by default
|
|
16
|
+
- Wrap every value in `useMemo` by default
|
|
17
|
+
- Use memoization to silence exhaustive-deps lint warnings
|
|
18
|
+
- Use `React.memo` on every component
|
|
19
|
+
- Reach for memoization before profiling
|
|
20
|
+
|
|
21
|
+
## The Cost of Memoization
|
|
22
|
+
|
|
23
|
+
Every `useMemo` and `useCallback` adds:
|
|
24
|
+
- Memory overhead (the cached value + the dependency array)
|
|
25
|
+
- Comparison overhead (shallow equality check on every render)
|
|
26
|
+
- Cognitive overhead (the reader must understand what is being memoized and why)
|
|
27
|
+
|
|
28
|
+
These costs are only worth paying when the avoided re-render or recalculation is measurably expensive.
|
|
29
|
+
|
|
30
|
+
## When `useCallback` Makes Sense
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
// GOOD — stable callback passed to a memoized child
|
|
34
|
+
const handleRowClick = useCallback((id: string) => {
|
|
35
|
+
navigate(`/invoices/${id}`);
|
|
36
|
+
}, [navigate]);
|
|
37
|
+
|
|
38
|
+
return <InvoiceTable onRowClick={handleRowClick} />;
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## When `useMemo` Makes Sense
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
// GOOD — expensive computation, profiled
|
|
45
|
+
const sortedAndFilteredInvoices = useMemo(() => {
|
|
46
|
+
return invoices
|
|
47
|
+
.filter(matchesFilters)
|
|
48
|
+
.sort(bySortKey(sortKey));
|
|
49
|
+
}, [invoices, matchesFilters, sortKey]);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## When Memoization Is Unnecessary
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
// BAD — trivial computation, memoization is noise
|
|
56
|
+
const label = useMemo(() => `Hello, ${name}`, [name]);
|
|
57
|
+
|
|
58
|
+
// GOOD
|
|
59
|
+
const label = `Hello, ${name}`;
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
// BAD — callback not passed anywhere that needs stability
|
|
64
|
+
const handleClick = useCallback(() => setOpen(true), []);
|
|
65
|
+
|
|
66
|
+
// GOOD
|
|
67
|
+
const handleClick = () => setOpen(true);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Profiling First
|
|
71
|
+
|
|
72
|
+
Before adding memoization, use React DevTools Profiler to confirm:
|
|
73
|
+
1. The component is re-rendering
|
|
74
|
+
2. The re-render is caused by the thing you are memoizing
|
|
75
|
+
3. The re-render is causing a measurable performance problem
|
|
76
|
+
|
|
77
|
+
## PRIORITY
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
Readability > Micro-optimization
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## See Also
|
|
84
|
+
|
|
85
|
+
- [`component-design.md`](component-design.md) — decomposition over memoization
|
|
86
|
+
- [`anti-patterns.md`](anti-patterns.md) — defensive memoization
|
|
87
|
+
- [`../tooling/observability.md`](../tooling/observability.md) — Web Vitals for real performance signals
|