@ai-support-agent/cli 0.1.32 → 0.1.33-beta.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/dist/alert-processor.d.ts +0 -8
- package/dist/alert-processor.d.ts.map +1 -1
- package/dist/alert-processor.js +5 -73
- package/dist/alert-processor.js.map +1 -1
- package/dist/api-client.d.ts +0 -4
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +0 -4
- package/dist/api-client.js.map +1 -1
- package/dist/commands/claude-code-args.d.ts +1 -0
- package/dist/commands/claude-code-args.d.ts.map +1 -1
- package/dist/commands/claude-code-args.js +3 -0
- package/dist/commands/claude-code-args.js.map +1 -1
- package/dist/commands/claude-code-runner.d.ts.map +1 -1
- package/dist/commands/claude-code-runner.js +2 -1
- package/dist/commands/claude-code-runner.js.map +1 -1
- package/dist/commands/plugin-dir.d.ts +20 -0
- package/dist/commands/plugin-dir.d.ts.map +1 -0
- package/dist/commands/plugin-dir.js +71 -0
- package/dist/commands/plugin-dir.js.map +1 -0
- package/dist/constants.d.ts +0 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +0 -1
- package/dist/constants.js.map +1 -1
- package/dist/plugin/.claude-plugin/plugin.json +9 -0
- package/dist/plugin/LICENSE +26 -0
- package/dist/plugin/README.md +86 -0
- package/dist/plugin/SYNC.md +113 -0
- package/dist/plugin/agents/build-error-resolver.md +159 -0
- package/dist/plugin/agents/code-reviewer.md +277 -0
- package/dist/plugin/agents/django-reviewer.md +159 -0
- package/dist/plugin/agents/infra-reviewer.md +172 -0
- package/dist/plugin/agents/investigator.md +75 -0
- package/dist/plugin/agents/nextjs-reviewer.md +191 -0
- package/dist/plugin/agents/php-reviewer.md +167 -0
- package/dist/plugin/agents/planner.md +184 -0
- package/dist/plugin/agents/python-reviewer.md +161 -0
- package/dist/plugin/agents/react-reviewer.md +150 -0
- package/dist/plugin/agents/silent-failure-hunter.md +158 -0
- package/dist/plugin/agents/typescript-reviewer.md +179 -0
- package/dist/plugin/agents/ui-reviewer.md +203 -0
- package/dist/plugin/commands/add-feature.md +301 -0
- package/dist/plugin/commands/build-fix.md +47 -0
- package/dist/plugin/commands/code-review.md +228 -0
- package/dist/plugin/commands/fix-defect.md +393 -0
- package/dist/plugin/commands/learn-eval.md +94 -0
- package/dist/plugin/commands/learn.md +84 -0
- package/dist/plugin/commands/plan.md +211 -0
- package/dist/plugin/commands/test-coverage.md +64 -0
- package/dist/plugin/commands/update-docs.md +98 -0
- package/dist/plugin/hooks/hooks.json +59 -0
- package/dist/plugin/hooks/scripts/auto-format.sh +63 -0
- package/dist/plugin/hooks/scripts/check-secrets-before-commit.sh +50 -0
- package/dist/plugin/hooks/scripts/guard-dangerous-commands.sh +55 -0
- package/dist/plugin/hooks/scripts/on-command-resume.sh +112 -0
- package/dist/plugin/hooks/scripts/on-command-stop.sh +200 -0
- package/dist/plugin/hooks/scripts/protect-sensitive-files.sh +58 -0
- package/dist/plugin/rules/common/coding-guidelines.md +73 -0
- package/dist/plugin/rules/documentation/api-docs.md +46 -0
- package/dist/plugin/rules/documentation/docs-site.md +60 -0
- package/dist/plugin/rules/documentation/source-docs.md +89 -0
- package/dist/plugin/rules/documentation/test-docs.md +39 -0
- package/dist/plugin/rules/logging/logging-rules.md +83 -0
- package/dist/plugin/rules/php/coding-rules.md +40 -0
- package/dist/plugin/rules/python/coding-rules.md +40 -0
- package/dist/plugin/rules/typescript/coding-rules.md +45 -0
- package/dist/plugin/skills/api-design/SKILL.md +269 -0
- package/dist/plugin/skills/backend-patterns/SKILL.md +312 -0
- package/dist/plugin/skills/database-migrations/SKILL.md +323 -0
- package/dist/plugin/skills/docker-patterns/SKILL.md +308 -0
- package/dist/plugin/skills/docs-site/SKILL.md +341 -0
- package/dist/plugin/skills/e2e-testing/SKILL.md +334 -0
- package/dist/plugin/skills/frontend-patterns/SKILL.md +318 -0
- package/dist/plugin/skills/integration-testing/SKILL.md +273 -0
- package/package.json +2 -2
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: e2e-testing
|
|
3
|
+
description: A pattern collection for Playwright-based E2E testing. Covers how to choose what to test in Next.js/Vue apps, selector strategy, wait strategy, test structure, test data management, flaky test countermeasures, and CI integration. Use when writing new E2E tests, reviewing E2E tests, investigating flaky tests, or analyzing test failures in CI.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# E2E Testing Patterns (Playwright / Next.js / Vue)
|
|
7
|
+
|
|
8
|
+
E2E tests should be "few and elite." This skill assumes Playwright and
|
|
9
|
+
collects patterns — and anti-patterns — for writing E2E tests that stay
|
|
10
|
+
robust and easy to maintain.
|
|
11
|
+
|
|
12
|
+
## 1. What should you test with E2E?
|
|
13
|
+
|
|
14
|
+
### Scope it to critical business flows
|
|
15
|
+
|
|
16
|
+
E2E tests are expensive to run and expensive to maintain. Don't aim for
|
|
17
|
+
exhaustive coverage — target only "the flows that would directly hurt the
|
|
18
|
+
business if they broke."
|
|
19
|
+
|
|
20
|
+
Things worth covering:
|
|
21
|
+
|
|
22
|
+
- Login/logout and screen transitions involving authentication
|
|
23
|
+
- Flows that finalize money or data — applications, orders, payments
|
|
24
|
+
- End-to-end paths for major forms: submission through list/detail confirmation
|
|
25
|
+
- Permission-based differences in what's shown or actionable (e.g. admin vs. regular user)
|
|
26
|
+
|
|
27
|
+
Things that should NOT be E2E tests (leave these to unit/component tests):
|
|
28
|
+
|
|
29
|
+
- Every variation of a validation message
|
|
30
|
+
- Pure logic such as date formatting or amount calculations
|
|
31
|
+
- Individual component rendering variations (combinations of props)
|
|
32
|
+
- Fine-grained branches of error handling
|
|
33
|
+
|
|
34
|
+
### Where to draw the line with unit tests
|
|
35
|
+
|
|
36
|
+
| What you want to verify | Appropriate layer |
|
|
37
|
+
|---|---|
|
|
38
|
+
| Function/class logic | Unit test |
|
|
39
|
+
| Component rendering/events | Component test |
|
|
40
|
+
| API input/output contract | API test |
|
|
41
|
+
| Business flow spanning multiple screens | E2E test |
|
|
42
|
+
|
|
43
|
+
Always ask yourself "can this only be verified through E2E?" Don't push
|
|
44
|
+
something into E2E that could be written at a lower layer.
|
|
45
|
+
|
|
46
|
+
## 2. Selector strategy
|
|
47
|
+
|
|
48
|
+
### Priority order
|
|
49
|
+
|
|
50
|
+
Prefer attributes that are visible to the user. Work through this list top to bottom:
|
|
51
|
+
|
|
52
|
+
1. Role: `page.getByRole('button', { name: 'Save' })`
|
|
53
|
+
2. Label: `page.getByLabel('Email address')`
|
|
54
|
+
3. Placeholder/text: `page.getByPlaceholder(...)` / `page.getByText(...)`
|
|
55
|
+
4. Test ID: `page.getByTestId('order-summary')`
|
|
56
|
+
|
|
57
|
+
Always use role and label when an element can be identified that way —
|
|
58
|
+
it doubles as an accessibility check. Reserve test IDs for elements with
|
|
59
|
+
no visual cue (containers, rows in a dynamic list, etc.).
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// Good: identify by the name the user perceives
|
|
63
|
+
await page.getByRole('textbox', { name: 'Email address' }).fill('a@example.com');
|
|
64
|
+
await page.getByRole('button', { name: 'Log in' }).click();
|
|
65
|
+
|
|
66
|
+
// Acceptable: use a test ID for a row with no visual cue
|
|
67
|
+
const row = page.getByTestId('user-row-123');
|
|
68
|
+
await row.getByRole('button', { name: 'Edit' }).click();
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### No selectors that depend on CSS structure
|
|
72
|
+
|
|
73
|
+
Selectors that depend on DOM structure or framework-generated class names
|
|
74
|
+
are forbidden. They break easily during refactors, and when they break
|
|
75
|
+
it's not obvious why.
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// Forbidden: depends on structure / auto-generated classes
|
|
79
|
+
await page.locator('div.container > div:nth-child(2) > button').click();
|
|
80
|
+
await page.locator('.css-1q2w3e4').click(); // CSS-in-JS generated class
|
|
81
|
+
await page.locator('#app > main span').first().click(); // depends on ordering
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
This also applies to attributes generated by Vue's scoped CSS
|
|
85
|
+
(`data-v-xxxxx`) and to Next.js/Tailwind utility classes — don't depend on
|
|
86
|
+
those either.
|
|
87
|
+
|
|
88
|
+
## 3. Wait strategy
|
|
89
|
+
|
|
90
|
+
### Trust auto-waiting and web-first assertions
|
|
91
|
+
|
|
92
|
+
Playwright's locator actions and `expect(locator)`-style assertions
|
|
93
|
+
automatically retry until the element is actionable or the condition
|
|
94
|
+
holds. Rely on this by default.
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
// Good: an assertion that retries automatically
|
|
98
|
+
await expect(page.getByRole('heading', { name: 'Order complete' })).toBeVisible();
|
|
99
|
+
await expect(page.getByTestId('cart-count')).toHaveText('3');
|
|
100
|
+
|
|
101
|
+
// Forbidden: a fixed sleep. Flaky on slow environments, wasteful on fast ones
|
|
102
|
+
await page.waitForTimeout(3000);
|
|
103
|
+
|
|
104
|
+
// Forbidden: an assertion evaluated immediately, with no retry — vulnerable to races
|
|
105
|
+
expect(await page.getByTestId('cart-count').textContent()).toBe('3');
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Also avoid depending on `networkidle`. In Next.js/Vue apps that use
|
|
109
|
+
polling or SSE, the network may never go idle — or idle may trigger too
|
|
110
|
+
early. What you should wait for isn't network silence, but the result
|
|
111
|
+
that's visible to the user.
|
|
112
|
+
|
|
113
|
+
### Arm response waits before the action, not after
|
|
114
|
+
|
|
115
|
+
When you need to wait for a specific response, start `waitForResponse`
|
|
116
|
+
*before* the click that triggers it. If you start waiting afterward, you
|
|
117
|
+
can lose the race and miss it.
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
// Good: arm the wait first, then perform the action
|
|
121
|
+
const responsePromise = page.waitForResponse(
|
|
122
|
+
(res) => res.url().includes('/api/orders') && res.status() === 201
|
|
123
|
+
);
|
|
124
|
+
await page.getByRole('button', { name: 'Confirm order' }).click();
|
|
125
|
+
await responsePromise;
|
|
126
|
+
await expect(page.getByText('Your order has been received')).toBeVisible();
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## 4. Test structure
|
|
130
|
+
|
|
131
|
+
### Test independence
|
|
132
|
+
|
|
133
|
+
Every test must be able to run alone, in any order.
|
|
134
|
+
|
|
135
|
+
- Don't carry variables, created data, or login state across tests
|
|
136
|
+
- Set up prerequisite data at the start of each test (or in `beforeEach`) — don't rely on shared fixtures
|
|
137
|
+
- Never let one test's "output" become another test's "input"
|
|
138
|
+
- Avoid `test.describe.serial` as a rule; if a sequence is truly required, fold it into a single test
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
test.beforeEach(async ({ page }) => {
|
|
142
|
+
// Each test sets up its own prerequisites, independent of other tests' results
|
|
143
|
+
await page.goto('/projects');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('can create a project', async ({ page }) => { /* ... */ });
|
|
147
|
+
test('can search for a project', async ({ page }) => {
|
|
148
|
+
// The data to search for is created within this test (or via API setup)
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Use the Page Object Model in moderation
|
|
153
|
+
|
|
154
|
+
Use a Page Object to consolidate "a sequence of operations repeated
|
|
155
|
+
across multiple tests." Don't build a giant class that wraps every
|
|
156
|
+
element on the screen.
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
// Good: a thin Page Object that exposes only business-level operations
|
|
160
|
+
export class LoginPage {
|
|
161
|
+
constructor(private page: Page) {}
|
|
162
|
+
|
|
163
|
+
async login(email: string, password: string) {
|
|
164
|
+
await this.page.goto('/login');
|
|
165
|
+
await this.page.getByLabel('Email address').fill(email);
|
|
166
|
+
await this.page.getByLabel('Password').fill(password);
|
|
167
|
+
await this.page.getByRole('button', { name: 'Log in' }).click();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Signs of over-abstraction (avoid these):
|
|
173
|
+
|
|
174
|
+
- A pile of methods that are each called from only one test
|
|
175
|
+
- Assertions buried inside the Page Object, making failures hard to trace
|
|
176
|
+
- Three layers of wrapper around what could be a single `getByRole` call
|
|
177
|
+
|
|
178
|
+
Only extract a shared helper after you've repeated something two or three
|
|
179
|
+
times. Don't abstract preemptively.
|
|
180
|
+
|
|
181
|
+
## 5. Test data management
|
|
182
|
+
|
|
183
|
+
### Generate unique data
|
|
184
|
+
|
|
185
|
+
Fixed-value data collides under parallel execution or retries. Generate a
|
|
186
|
+
value that's unique per run.
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
// Include a unique identifier for each run
|
|
190
|
+
const unique = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
191
|
+
const email = `e2e-${unique}@example.com`;
|
|
192
|
+
const projectName = `E2E Test Project-${unique}`;
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Prioritize "a design that can't collide" over cleanup after the fact.
|
|
196
|
+
Make sure that even if cleanup fails, it doesn't drag other tests down
|
|
197
|
+
with it.
|
|
198
|
+
|
|
199
|
+
### Reuse authentication state (storageState)
|
|
200
|
+
|
|
201
|
+
Don't repeat a UI login in every test. Log in once in a setup project,
|
|
202
|
+
save the resulting state, and reuse it across all tests.
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// auth.setup.ts: create and save authentication state once
|
|
206
|
+
import { test as setup } from '@playwright/test';
|
|
207
|
+
|
|
208
|
+
setup('save authentication state', async ({ page }) => {
|
|
209
|
+
await page.goto('/login');
|
|
210
|
+
await page.getByLabel('Email address').fill(process.env.E2E_USER!);
|
|
211
|
+
await page.getByLabel('Password').fill(process.env.E2E_PASS!);
|
|
212
|
+
await page.getByRole('button', { name: 'Log in' }).click();
|
|
213
|
+
await page.waitForURL('/dashboard');
|
|
214
|
+
await page.context().storageState({ path: 'playwright/.auth/user.json' });
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
// playwright.config.ts: make setup a dependency and reuse the storageState
|
|
220
|
+
projects: [
|
|
221
|
+
{ name: 'setup', testMatch: /auth\.setup\.ts/ },
|
|
222
|
+
{
|
|
223
|
+
name: 'chromium',
|
|
224
|
+
use: { storageState: 'playwright/.auth/user.json' },
|
|
225
|
+
dependencies: ['setup'],
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The one exception is testing the login feature itself — that should still
|
|
231
|
+
go through the UI rather than reusing `storageState`.
|
|
232
|
+
|
|
233
|
+
### Mock external APIs (route)
|
|
234
|
+
|
|
235
|
+
Mock external APIs you don't control (payment providers, maps, other
|
|
236
|
+
external SaaS) with `page.route` to make the test deterministic.
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
// Mock the external payment API and pin down a success response
|
|
240
|
+
await page.route('**/api/external/payment', async (route) => {
|
|
241
|
+
await route.fulfill({
|
|
242
|
+
status: 200,
|
|
243
|
+
contentType: 'application/json',
|
|
244
|
+
body: JSON.stringify({ status: 'succeeded', id: 'pay_test_001' }),
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Don't mock your own backend as a rule. The moment you do, everything
|
|
250
|
+
downstream stops being an E2E test. Only mock things outside your own
|
|
251
|
+
system boundary.
|
|
252
|
+
|
|
253
|
+
## 6. Flaky test countermeasures
|
|
254
|
+
|
|
255
|
+
### Symptom-to-cause-to-fix table
|
|
256
|
+
|
|
257
|
+
| Symptom | Typical cause | Fix |
|
|
258
|
+
|---|---|---|
|
|
259
|
+
| Passes locally but fails in CI | Fixed sleeps, dependence on machine speed | Replace with web-first assertions |
|
|
260
|
+
| Element occasionally not found | Evaluated immediately, before rendering completes | Rely on `expect(locator)`'s auto-retry |
|
|
261
|
+
| Fails only under parallel execution | Data collisions between tests | Generate unique data; restore independence |
|
|
262
|
+
| Fails when order changes | Depends on a previous test's result | Have each test set up its own prerequisites |
|
|
263
|
+
| Click doesn't register | Competing with an animation or overlay | Assert the target is stably visible before acting |
|
|
264
|
+
| Can't capture the response you're waiting for | `waitForResponse` armed after the action | Arm the wait before the action |
|
|
265
|
+
|
|
266
|
+
### Where retries fit in
|
|
267
|
+
|
|
268
|
+
Retries (`retries`) are a safety net for absorbing transient
|
|
269
|
+
infrastructure failures — they are not a "cure" for flakiness. It's fine
|
|
270
|
+
to set something like `retries: 2` in CI, but any test that only passes
|
|
271
|
+
after a retry should be logged as flaky and investigated. "It has
|
|
272
|
+
retries, so we don't need to fix it" is not an acceptable conclusion.
|
|
273
|
+
|
|
274
|
+
### Quarantining requires a ticket number
|
|
275
|
+
|
|
276
|
+
If root-causing takes time, you may temporarily quarantine a test — but
|
|
277
|
+
only with a ticket number attached. A `skip`/`fixme` with no ticket
|
|
278
|
+
number should be sent back in review.
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
// Always leave a ticket number and reason. skip without one is forbidden
|
|
282
|
+
test.fixme('can export order history as CSV', async ({ page }) => {
|
|
283
|
+
// TICKET-1234: investigating an issue where the download event isn't captured in CI only
|
|
284
|
+
});
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## 7. CI integration
|
|
288
|
+
|
|
289
|
+
### Traces and screenshots on failure
|
|
290
|
+
|
|
291
|
+
Always retain the evidence needed to reproduce a failure. Recording
|
|
292
|
+
everything all the time wastes storage and run time, so limit it to
|
|
293
|
+
failures and retries.
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
// playwright.config.ts: keep evidence only on failure
|
|
297
|
+
export default defineConfig({
|
|
298
|
+
retries: process.env.CI ? 2 : 0,
|
|
299
|
+
use: {
|
|
300
|
+
trace: 'on-first-retry', // record a trace on retry
|
|
301
|
+
screenshot: 'only-on-failure', // screenshot only on failure
|
|
302
|
+
video: 'retain-on-failure', // keep video only on failure
|
|
303
|
+
},
|
|
304
|
+
reporter: process.env.CI ? [['html', { open: 'never' }], ['github']] : 'list',
|
|
305
|
+
});
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
In CI, save the trace and HTML report as artifacts, and investigate
|
|
309
|
+
failures with `npx playwright show-trace` rather than guessing from logs
|
|
310
|
+
alone.
|
|
311
|
+
|
|
312
|
+
### Parallelism and sharding basics
|
|
313
|
+
|
|
314
|
+
- Control in-machine parallelism with workers (`workers`). If tests are
|
|
315
|
+
truly independent, the default parallel execution works as-is
|
|
316
|
+
- As the test count grows, split across multiple machines:
|
|
317
|
+
`npx playwright test --shard=1/4`
|
|
318
|
+
- If a test starts failing under parallel execution, fix the test's
|
|
319
|
+
independence (data collisions, shared state) rather than lowering
|
|
320
|
+
parallelism
|
|
321
|
+
|
|
322
|
+
```yaml
|
|
323
|
+
# CI example: matrix run with 4-way sharding
|
|
324
|
+
strategy:
|
|
325
|
+
matrix:
|
|
326
|
+
shard: [1/4, 2/4, 3/4, 4/4]
|
|
327
|
+
steps:
|
|
328
|
+
- run: npx playwright test --shard=${{ matrix.shard }}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## Related
|
|
332
|
+
|
|
333
|
+
- Use `/test-coverage` to check coverage
|
|
334
|
+
- Use `/code-review` to review implementation and test code
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: frontend-patterns
|
|
3
|
+
description: A pattern collection for component design, state management, performance, forms, error handling, and accessibility in React (Next.js App Router) and Vue.js (Composition API). Use when creating or refactoring frontend components, deciding on a state management or data-fetching approach, or checking what to look for when reviewing frontend code.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Frontend Development Patterns
|
|
7
|
+
|
|
8
|
+
This guide centers on React (Next.js App Router), with the equivalent Vue.js (Composition API)
|
|
9
|
+
pattern noted alongside each topic. When in doubt, follow the principle of "start simple, add
|
|
10
|
+
complexity only when you actually need it."
|
|
11
|
+
|
|
12
|
+
## 1. Component Design
|
|
13
|
+
|
|
14
|
+
### Splitting responsibilities
|
|
15
|
+
|
|
16
|
+
Aim for one responsibility per component. If you can't describe what a component does in a
|
|
17
|
+
single sentence, consider splitting it.
|
|
18
|
+
|
|
19
|
+
- Separate presentation (display) from data fetching and state management (container).
|
|
20
|
+
- In the Next.js App Router, fetch data in Server Components and carve out only the
|
|
21
|
+
interactive parts into small `"use client"` Client Components.
|
|
22
|
+
- In Vue, once the logic inside `setup` starts spanning multiple areas of the screen, split it
|
|
23
|
+
into child components or composables.
|
|
24
|
+
|
|
25
|
+
### Signs a component has grown too large
|
|
26
|
+
|
|
27
|
+
If several of these apply, it's a signal to split the component:
|
|
28
|
+
|
|
29
|
+
- The file is over 300 lines, or the JSX/template is over 100 lines.
|
|
30
|
+
- Seven or more `useState`/`ref` calls are lined up.
|
|
31
|
+
- Boolean props (`isCompact`, `showHeader`, etc.) keep growing and their conditional branches
|
|
32
|
+
start interacting with each other.
|
|
33
|
+
- The component performs multiple unrelated data fetches.
|
|
34
|
+
- Writing a test for it requires mocking a pile of unrelated functionality.
|
|
35
|
+
|
|
36
|
+
### Composing with children / slots
|
|
37
|
+
|
|
38
|
+
Express variants through composition rather than piling on flag props.
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
// Bad: variants expressed as a growing set of flag props
|
|
42
|
+
<Card title="Sales" showIcon iconType="chart" footerText="Details" footerLink="/sales" />
|
|
43
|
+
|
|
44
|
+
// Good: composition via children lets the caller decide the structure
|
|
45
|
+
<Card>
|
|
46
|
+
<Card.Header icon={<ChartIcon />}>Sales</Card.Header>
|
|
47
|
+
<Card.Body>{children}</Card.Body>
|
|
48
|
+
<Card.Footer><Link href="/sales">Details</Link></Card.Footer>
|
|
49
|
+
</Card>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```vue
|
|
53
|
+
<!-- In Vue, named slots express the same structure -->
|
|
54
|
+
<Card>
|
|
55
|
+
<template #header>Sales</template>
|
|
56
|
+
<template #default>...</template>
|
|
57
|
+
<template #footer><RouterLink to="/sales">Details</RouterLink></template>
|
|
58
|
+
</Card>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 2. Reusing Logic (Custom Hooks / Composables)
|
|
62
|
+
|
|
63
|
+
### When to extract
|
|
64
|
+
|
|
65
|
+
- The same combination of state and effects shows up in two or more places.
|
|
66
|
+
- There's logic you want to unit test independently of the component.
|
|
67
|
+
- You can give it a name that describes what it does (`useDebounce`, `usePagination`, etc.).
|
|
68
|
+
|
|
69
|
+
Conversely, don't force an extraction for logic that's used in only one place and is hard to
|
|
70
|
+
name meaningfully.
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
// Good: extract the state + effect boilerplate into a hook (React)
|
|
74
|
+
function useDebouncedValue<T>(value: T, delayMs = 300): T {
|
|
75
|
+
const [debounced, setDebounced] = useState(value);
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
const id = setTimeout(() => setDebounced(value), delayMs);
|
|
78
|
+
return () => clearTimeout(id);
|
|
79
|
+
}, [value, delayMs]);
|
|
80
|
+
return debounced;
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
// Good: the same logic implemented as a Vue composable
|
|
86
|
+
export function useDebouncedValue<T>(source: Ref<T>, delayMs = 300) {
|
|
87
|
+
const debounced = ref(source.value) as Ref<T>;
|
|
88
|
+
let id: ReturnType<typeof setTimeout>;
|
|
89
|
+
watch(source, (v) => {
|
|
90
|
+
clearTimeout(id);
|
|
91
|
+
id = setTimeout(() => (debounced.value = v), delayMs);
|
|
92
|
+
});
|
|
93
|
+
onUnmounted(() => clearTimeout(id));
|
|
94
|
+
return debounced;
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
- Hooks/composables should not return UI. If the reusable unit involves UI, make it a
|
|
99
|
+
component instead.
|
|
100
|
+
- Return an object so callers can destructure only what they need.
|
|
101
|
+
|
|
102
|
+
## 3. State Management
|
|
103
|
+
|
|
104
|
+
### Where state should live: priority order
|
|
105
|
+
|
|
106
|
+
1. Local state (`useState` / `ref`). Always start here.
|
|
107
|
+
2. Lift state up to a parent, once it needs to be shared between siblings.
|
|
108
|
+
3. Context (`createContext` / `provide` and `inject`), once passing props down deeply becomes
|
|
109
|
+
painful.
|
|
110
|
+
4. A global store (Zustand / Pinia), once it meets the criteria below.
|
|
111
|
+
|
|
112
|
+
### When a global store is warranted
|
|
113
|
+
|
|
114
|
+
- State that outlives a single screen (authenticated user, theme, cart, etc.).
|
|
115
|
+
- State that must survive route changes without being discarded.
|
|
116
|
+
- State that's read and written by many far-apart components.
|
|
117
|
+
|
|
118
|
+
If the only issue is "props get passed through 2-3 levels," component composition or context
|
|
119
|
+
can usually solve it — that alone isn't a reason to introduce a store.
|
|
120
|
+
|
|
121
|
+
### Let a data-fetching library own server state
|
|
122
|
+
|
|
123
|
+
Don't hand-roll caching, refetching, and loading-state management for API responses. Delegate
|
|
124
|
+
to SWR or TanStack Query (React Query / Vue Query).
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
// Bad: manually managing server data with useEffect + a global store
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
fetch("/api/users").then((r) => r.json()).then((d) => store.setUsers(d));
|
|
130
|
+
}, []);
|
|
131
|
+
|
|
132
|
+
// Good: delegate to a data-fetching library (caching, revalidation, and dedup included)
|
|
133
|
+
const { data, error, isLoading } = useQuery({
|
|
134
|
+
queryKey: ["users"],
|
|
135
|
+
queryFn: fetchUsers,
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
- Reserve the global store for genuinely client-only state.
|
|
140
|
+
- Copying server data into a store means you end up reimplementing freshness tracking and
|
|
141
|
+
invalidation yourself.
|
|
142
|
+
|
|
143
|
+
## 4. Derived State
|
|
144
|
+
|
|
145
|
+
Values that can be computed from existing state should be derived during render (React) or via
|
|
146
|
+
`computed` (Vue) — don't duplicate them into a separate piece of state via an effect or watcher.
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
// Bad: duplicating a derived value into separate state via useEffect
|
|
150
|
+
// (lags a render behind and is a common source of bugs)
|
|
151
|
+
const [filtered, setFiltered] = useState<Item[]>([]);
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
setFiltered(items.filter((i) => i.name.includes(query)));
|
|
154
|
+
}, [items, query]);
|
|
155
|
+
|
|
156
|
+
// Good: compute it during render; add useMemo only if profiling shows it's expensive
|
|
157
|
+
const filtered = items.filter((i) => i.name.includes(query));
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
// Bad: duplicating into a separate ref via watch (Vue)
|
|
162
|
+
watch([items, query], () => {
|
|
163
|
+
filtered.value = items.value.filter((i) => i.name.includes(query.value));
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Good: derive it declaratively with computed
|
|
167
|
+
const filtered = computed(() =>
|
|
168
|
+
items.value.filter((i) => i.name.includes(query.value))
|
|
169
|
+
);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
- Whenever you see "an effect that updates state B when state A changes," ask first whether B
|
|
173
|
+
can just be a derived value instead.
|
|
174
|
+
- Effects/watchers are legitimately for syncing with external systems (DOM manipulation,
|
|
175
|
+
subscriptions, sending logs, etc.) — not for mirroring state.
|
|
176
|
+
|
|
177
|
+
## 5. Performance
|
|
178
|
+
|
|
179
|
+
### Memoization discipline: measure first
|
|
180
|
+
|
|
181
|
+
- Check actual re-render cost with React DevTools Profiler / Vue DevTools before optimizing.
|
|
182
|
+
- Don't reflexively slap `useMemo` / `useCallback` / `memo` on everything — it costs
|
|
183
|
+
readability, and a wrong dependency array turns into a bug.
|
|
184
|
+
- It's worth adding when a computation is measurably expensive, or when referential stability
|
|
185
|
+
matters for a `memo`-ized child or a dependency array.
|
|
186
|
+
- In environments where the React Compiler or Vue's built-in optimizations are effective,
|
|
187
|
+
manual memoization can often be reduced further.
|
|
188
|
+
|
|
189
|
+
### Code splitting and lazy loading
|
|
190
|
+
|
|
191
|
+
- Route-level splitting is handled automatically by Next.js / Vue Router — rely on that first.
|
|
192
|
+
- Lazy-load heavy components that aren't needed on initial render (modals, charts, editors)
|
|
193
|
+
with `next/dynamic` or `defineAsyncComponent`.
|
|
194
|
+
|
|
195
|
+
```tsx
|
|
196
|
+
// Good: lazily load a heavy component that's only needed once it's opened
|
|
197
|
+
const ChartPanel = dynamic(() => import("./ChartPanel"), { ssr: false });
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Virtualizing long lists
|
|
201
|
+
|
|
202
|
+
Don't render every row of a list with hundreds of items — render only the visible range with
|
|
203
|
+
`@tanstack/react-virtual` / `@tanstack/vue-virtual` or similar. Before reaching for
|
|
204
|
+
virtualization, first consider whether pagination or a "load more" pattern can simply reduce
|
|
205
|
+
the item count.
|
|
206
|
+
|
|
207
|
+
## 6. Forms
|
|
208
|
+
|
|
209
|
+
### Schema-based validation
|
|
210
|
+
|
|
211
|
+
Define validation rules as a schema (Zod, Valibot, etc.) and wire it into your form library
|
|
212
|
+
(React Hook Form / VeeValidate). Re-validate with the same schema on the server.
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
// Good: the schema is the single source of truth for validation rules
|
|
216
|
+
const schema = z.object({
|
|
217
|
+
email: z.string().email("Please enter a valid email address"),
|
|
218
|
+
age: z.coerce.number().int().min(18, "Must be 18 or older"),
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const { register, handleSubmit, formState } = useForm({
|
|
222
|
+
resolver: zodResolver(schema),
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
- Display error messages near the relevant field and associate them with the input via
|
|
227
|
+
`aria-describedby`.
|
|
228
|
+
- Don't hardcode validation logic inline in JSX or templates.
|
|
229
|
+
|
|
230
|
+
### Submission state and preventing double submits
|
|
231
|
+
|
|
232
|
+
```tsx
|
|
233
|
+
// Bad: no submission-state guard, so rapid clicks trigger duplicate POSTs
|
|
234
|
+
<button onClick={() => submit(values)}>Submit</button>
|
|
235
|
+
|
|
236
|
+
// Good: disable the button while submitting and show the state to the user
|
|
237
|
+
const { isSubmitting } = formState;
|
|
238
|
+
<button type="submit" disabled={isSubmitting}>
|
|
239
|
+
{isSubmitting ? "Submitting..." : "Submit"}
|
|
240
|
+
</button>
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
- Using `useMutation` from React Query / Vue Query for mutations standardizes handling of
|
|
244
|
+
`isPending`, errors, and retries.
|
|
245
|
+
- After a successful submission, reset the form or navigate away so the user isn't left in a
|
|
246
|
+
state where they could resubmit the same data.
|
|
247
|
+
|
|
248
|
+
## 7. Error Handling
|
|
249
|
+
|
|
250
|
+
### Error boundaries / onErrorCaptured
|
|
251
|
+
|
|
252
|
+
- React: place an error boundary around each layout region so one failure doesn't blank out
|
|
253
|
+
the whole screen. In the Next.js App Router, add an `error.tsx` per segment.
|
|
254
|
+
- Vue: catch descendant rendering errors in a parent's `onErrorCaptured` and switch to a
|
|
255
|
+
fallback UI.
|
|
256
|
+
- Never swallow the error silently in the boundary — always report it to your error monitoring
|
|
257
|
+
service.
|
|
258
|
+
|
|
259
|
+
### The three states of a data-fetching UI
|
|
260
|
+
|
|
261
|
+
Always explicitly handle the three states: loading, error, and empty (zero results).
|
|
262
|
+
|
|
263
|
+
```tsx
|
|
264
|
+
// Good: handle all three states before rendering the real content
|
|
265
|
+
if (isLoading) return <Spinner />;
|
|
266
|
+
if (error) return <ErrorMessage error={error} onRetry={refetch} />;
|
|
267
|
+
if (data.length === 0) return <EmptyState message="No data available" />;
|
|
268
|
+
return <UserList users={data} />;
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
- Give the error state a retry action (a retry button).
|
|
272
|
+
- Word the empty state so it clearly reads as "not an error," and point toward a next action
|
|
273
|
+
(e.g., create new).
|
|
274
|
+
- To avoid flicker, consider a skeleton UI or `placeholderData` (keeping the previous data
|
|
275
|
+
visible while refetching).
|
|
276
|
+
|
|
277
|
+
## 8. Accessibility
|
|
278
|
+
|
|
279
|
+
### Semantic elements
|
|
280
|
+
|
|
281
|
+
- Use `button` / `a` for anything clickable. Don't attach `onClick` to a `div`.
|
|
282
|
+
- Use `a` (`Link`) for navigation/URL changes and `button` for in-place actions.
|
|
283
|
+
- Keep heading levels in order starting from `h1`; don't pick a heading level for its visual
|
|
284
|
+
size.
|
|
285
|
+
- Use landmarks (`main`, `nav`, `header`, `footer`) to convey document structure.
|
|
286
|
+
|
|
287
|
+
```tsx
|
|
288
|
+
// Bad: onClick on a div (unreachable via keyboard or assistive technology)
|
|
289
|
+
<div onClick={save}>Save</div>
|
|
290
|
+
|
|
291
|
+
// Good: use a native button (focus and Enter/Space work out of the box)
|
|
292
|
+
<button type="button" onClick={save}>Save</button>
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Labels
|
|
296
|
+
|
|
297
|
+
- Associate every form input with a `label` (`htmlFor` / `for`).
|
|
298
|
+
- Add `aria-label` to icon-only buttons.
|
|
299
|
+
- Image `alt` text should describe the content; use `alt=""` for purely decorative images.
|
|
300
|
+
|
|
301
|
+
### Keyboard operation basics
|
|
302
|
+
|
|
303
|
+
- Everything should be reachable via Tab, operable with Enter/Space, and modals should close
|
|
304
|
+
with Esc.
|
|
305
|
+
- Don't remove the focus ring via CSS. If you do, provide an alternative visible style.
|
|
306
|
+
- When a modal opens, move focus into it, and restore focus to the triggering element when it
|
|
307
|
+
closes.
|
|
308
|
+
- Before building custom UI, consider whether a native element or a proven headless UI library
|
|
309
|
+
already covers the need.
|
|
310
|
+
|
|
311
|
+
## Related
|
|
312
|
+
|
|
313
|
+
- For reviews, use the react-reviewer / typescript-reviewer subagents, or `/code-review`.
|
|
314
|
+
- For Next.js-specific concerns (Server/Client boundaries, Server Actions, caching, etc.), use
|
|
315
|
+
the nextjs-reviewer.
|
|
316
|
+
- For usability and consistency (UI/UX) concerns, use the ui-reviewer.
|
|
317
|
+
- Treat this skill as the decision criteria to apply during implementation and refactoring;
|
|
318
|
+
delegate the actual application of review criteria to the subagents listed above.
|