plugins/frontend-toolkit/skills/component-quality/SKILL.md
Improve React component design — split Container/Presentational, add useEffect cleanup, extract custom hooks, apply cva + cn() consistently, decide extraction with explicit criteria. Use when a pattern repeats 2+ times, a file exceeds 500 LOC, or PR review flags component complexity. Not for non-component code changes (use code-refactoring) or extraction into the shared component library (use design-system-construction).
npx skillsauth add jaykim88/claude-ai-engineering component-qualityInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Apply component design principles to maximize reusability and testability. Avoid both extremes: premature abstraction (one-off "reusable" components) and copy-paste blindness.
Universal — separating logic from presentation, effect cleanup, extraction criteria, typed variant systems, and composition over wrapper proliferation apply to any component-based framework. The default Procedure illustrates them with the React + shadcn/ui idiom (cva, cn(), Radix Slot, hooks); the Other stacks section maps each concept to Vue / Svelte / Angular equivalents.
Separate logic from presentation
Place Error Boundaries strategically
async-ux-statesAudit useEffect — first "does it need to exist?", then cleanup
setTimeout, setInterval)addEventListener)Extract custom hooks
useState + useEffect pattern in 2+ components → extract to useXApply a typed variant system
cn() — see Implementation)Prefer composition (children / slots) over deep prop chains
children (or named slots) instead of threading props down many levels — cuts prop drilling, and a component that holds state but renders children from above re-renders itself without re-rendering those children (same element reference). Composition is the first tool against drilling AND re-renders, before reaching for context or memo.asChild + <Slot>: when a wrapper only adds styles/behavior to one child, accept asChild and forward className/refs/props to the consumer's child — avoids <ButtonLink> / <ButtonAnchor> / <ButtonDiv> proliferation. Pattern: <Button asChild><Link href="/x">Go</Link></Button> renders as <a> with button styles.Decide extraction with explicit criteria
Candidates (OR — any one warrants review):
{/* Modal */} {/* Divider */}Execute extraction (AND — all must hold):
Write hook tests
renderHook — see Implementation)| ❌ Anti-pattern | ✅ Correct |
|---|---|
| useEffect(() => setInterval(fn, 1000), []) (no cleanup) | useEffect(() => { const id = setInterval(...); return () => clearInterval(id) }, []) |
| <div className={cn('base', isActive && 'active', size === 'sm' && 'small')} (3+ conditionals) | Extract to cva({ variants: { state, size } }) |
| <ButtonAnchor>, <ButtonLink>, <ButtonDiv> (wrapper proliferation) | One <Button asChild> + Radix <Slot> |
| Premature React.memo on every component | Profile first; memo only where Profiler shows wasted renders |
| 800-LOC component with {/* Header */}, {/* Sidebar */}, {/* Modal */} markers | Extract each marked section (if it passes the AND criteria) |
| Tier | Examples | Action SLA |
|---|---|---|
| Critical | useEffect with missing cleanup on timer/listener/subscription (memory leak); file > 1000 LOC with no extraction decision | Fix immediately |
| Major | File 500-1000 LOC mixing 3+ concerns; conditional class chain > 4 without cva; missing cn(internal, className) on shared components | Fix this sprint |
| Minor | Premature memoization without Profiler evidence; minor naming inconsistencies in custom hooks | Schedule within 2 sprints |
useEffect audited for cleanupcn(internal, className) used in shared components for consumer overridessrc/hooks/use<X>.ts, services in src/lib/<domain>/, each with co-located test file// inline because [reason] for explicit non-extraction decisionsrefactor(component): <description> for cleanup; feat(hook): add use<X> for new extractions- Components touched: N
- Hooks extracted: N (with tests)
- useEffect cleanup added: N
- Extraction decisions: extracted X / kept inline Y (with reasons)
grep -rn 'useEffect' src/, then check eachuseEffect(() => { ... return cleanup }, []) — timer/listener/subscriptionuseX naming, return value or destructured objectclass-variance-authority (cva) — minimal signature:
const buttonVariants = cva('inline-flex items-center', {
variants: {
size: { sm: 'h-8 px-3', md: 'h-10 px-4', lg: 'h-12 px-6' },
intent: { primary: 'bg-primary text-primary-foreground', ghost: 'bg-transparent' },
},
defaultVariants: { size: 'md', intent: 'primary' },
});
cn(internal, className) (clsx + tailwind-merge)<Slot> + asChild propReact.memo, useCallback, useMemo (after Profiler proves cost)@testing-library/react's renderHookuse<Name>) replace custom hooks; cleanup via onUnmounted(); variants via cva (works in Vue) or tailwind-variants; composition via <slot> + scoped slots; memo via defineComponent + <KeepAlive> or computed$state.raw for shared logic; cleanup in onDestroy(); variants via tailwind-variants; composition via <slot> + slot props; Svelte 5 fine-grained reactivity reduces memo needngOnDestroy() or takeUntilDestroyed(); variants via service or directive; composition via content projection (<ng-content>); OnPush change detection + signals for performancecode-refactoring — for non-component code changesdesign-system-construction — when extraction targets the shared component librarytest-strategy — write tests for newly-extracted hooks/componentsasync-ux-states — owns the loading/error/empty fallback UI that error boundaries rendercva (typed, declarative, compound-variant-aware) and always merge consumer overrides with cn(internal, className) left-to-right so consumers can override. Never string-concatenate Tailwind classes — that pattern silently breaks when the same utility appears in both internal and consumer values.development
Design webhooks correctly on both sides — sending (HMAC signing, retries with backoff, at-least-once) and receiving (verify signature on raw body, enqueue + 200 fast, dedupe on event id). Use when adding webhook delivery or consuming a provider's webhooks. Not for internal service-to-service events (use async-messaging) or general outbound-call retry policy (use resilience-patterns).
testing
Use transactions and isolation levels correctly — keep them short, no network calls inside, explicit isolation, retry on serialization conflicts, and choose optimistic vs pessimistic locking. Use when a write spans multiple tables, when concurrent updates corrupt data, or when designing money/inventory flows. Not for cross-service event delivery (use async-messaging Outbox) or schema-level constraints (use schema-design).
development
Backend testing pyramid — unit for pure logic, integration against a real DB (Testcontainers), and consumer-driven contract testing (Pact) for service boundaries. Use before a feature, after a bug fix, or when services break each other on deploy. Not for load testing (use performance-profiling) or security testing (use backend-security-audit).
data-ai
Design a relational schema — normalize to 3NF then denormalize with justification, choose the right Postgres index type per data shape, enforce constraints at the DB. Use when modeling a new domain, when queries are slow, or before a migration. Not for diagnosing slow queries (use query-optimization) or shipping the change without downtime (use migration-strategy).