Engineering / react-stinky
Detect React and TypeScript maintainability smells across the whole component, hook, and module, explain the cost of each, and propose a fix with a source link.
Signierte, abgestufte Provenienz für diesen Skill. Integrität, Urheberschaft, Capability und Aktualität als Beleg, den du abwägst, nicht als Verifiziert-Badge.
A holistic code-smell detector for React and TypeScript. It finds the patterns that make a component, a hook, or a module hard to read, reason about, and change, explains the cost of each, and proposes a concrete fix. Coverage spans prop and API design, state and data flow, effects and lifecycle, component structure, rendering correctness, accessibility, async and error handling, and TypeScript discipline. The full catalog with detection signals, fixes, exceptions, and sources is in catalog.md; read it before running a scan.
It defers two neighboring concerns to sibling skills so it does not duplicate them: memoization (useMemo, useCallback, React.memo) to react-compiler, and color literals to theme-colors. If those are not installed, note the finding in one line and move on. Everything else about day-to-day React maintainability is in scope.
Nine pillars. The categories under each, with detection signals and sources, are in catalog.md. The background models the findings lean on (controlled vs uncontrolled, render snapshots, effects as synchronization, one fact one home, impossible states, the composition layer, hydration) are separate linked concepts under references/concepts/; read one when a finding needs the underlying model explained, not just named.
useState, props copied into state, two sources of truth for one fact, prop drilling, stale closures in timers and async continuations, useState/useRef role confusion, god contexts and unstable provider values.key.key on a list that reorders or edits, direct mutation of state or props, impure renders (randomness and clock reads, setState during render, hydration-unsafe browser reads), the stray 0 leaked by &&, nested ternaries, copy-pasted JSX blocks.onClick on a non-interactive element with no role, tabIndex, or keyboard handler; div soup where semantic elements belong; form controls with no associated label; broken focus management in hand-rolled modals and menus.dangerouslySetInnerHTML, javascript: hrefs).any and as any and @ts-ignore, lying as casts and non-null !, loose internal types (object, Function, stringly-typed enums), untyped catch blocks, unions switched on without an exhaustiveness check.Match the scope to the request, then run the workflow below over it.
| Mode | Trigger | What to scan |
|---|---|---|
| Repo sweep | "smell-check the codebase" | Glob **/*.tsx plus hook and module .ts (use-*.ts, lib/). Skip node_modules, build output, generated code, *.test.*, *.stories.* unless asked. Prioritize shared, component, hook, and ui directories and exported symbols. |
| Folder scan | one or more directories named | Same, scoped to those directories. |
| File scan | specific files named | Read each fully. Check every component, hook, prop interface, and exported function. |
| Fragment sniff | a pasted function or component, or one named symbol | Check only that surface. State what you assumed about anything off-screen. |
Folder and repo-sweep scope additionally run a cross-file duplication pass (duplication-pass.md) to catch DRY smells a single-file scan cannot: a component re-implemented inline elsewhere, the same hook or type copy-pasted across files. Single-file and fragment scope cannot see this, so say cross-file duplication was not checked rather than implying the code is unique.
aria-label or keyboard handler on a control, a function across the RSC boundary, value || 50 eating value={0}, a mutated state array, conditional hooks, props copied into state that then drift, a stray 0 rendered by {count && ...}, Date.now() ids that break hydration, unsanitized dangerouslySetInnerHTML.)loading on a custom prop, a loose Record<string, unknown> sx type, JSDoc that restates the name.)The catalog carries a per-smell exception line. These cut across all of them. Honor them or this skill becomes a nuisance.
is-prefix disabled, required, checked, open, or rename onChange on a native <input>.open, slots, sx; Radix asChild).defaultValue). Flag only when a later prop change is expected to update it.any at a genuine untyped boundary is sometimes pragmatic. Prefer unknown plus narrowing, but do not block on it.react-compiler and color literals to theme-colors rather than duplicating them.React Stinky report, <scope>src/components/SeedRow.tsx[Rancid] clickable-nonsemantic (a11y markup), line 297Smell: a <Stack> (renders a div) has onClick but no role, tabIndex, or keyboard handler.Cost: keyboard and screen-reader users cannot trigger it; it is invisible to assistive tech.Fix: render a real control (component="button" or an IconButton), or addrole="button" tabIndex={0} and an onKeyDown for Enter and Space.Source: MDN button role (https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/button_role)[Funky] effect-for-derived (state and effects), line 40Smell: a useEffect plus setState computes `fullName` from `first` and `last`.Cost: an extra render and a state value that can drift from its inputs.Fix: compute during render, `const fullName = `${first} ${last}``. Delete the effect and the state.Source: React, You Might Not Need an Effect (https://react.dev/learn/you-might-not-need-an-effect)Summary: 1 rancid, 1 funky across 1 file.
When the scope is clean, say so plainly: "Smells fresh. No maintainability smells found in <scope>."
The 18 component-API categories (Pillar 1) are distilled from the cant-maintain React API-design challenge set, each traced to its React, TypeScript, MDN, Next.js, or MUI source. The remaining pillars extend the same maintainability lens to state, effects, structure, rendering, accessibility, async and error handling, and types, each sourced to the canonical React docs, MDN, or TypeScript handbook in catalog.md.