React + TypeScript
Pathrule2 Rules • 3 Memories • 1 Skill
Rules, memories, and a review skill for React codebases written in TypeScript. Pre-scoped to your source and component directories so your AI assistant writes typed, accessible, and predictable components.
Suggested path map
Pathrule places each piece on the matching path, so your assistant only sees it where it belongs. This is the scoping you get on import; you can adjust it in your workspace.
/ workspace root
react-component-review
src/
React Compiler handles memoization for new code
components/
Do not sync derived state or fetch data inside effects
Interactive elements must be accessible
How we type component props
Component structure and state placement
Rules
2Do not sync derived state or fetch data inside effects/src/componentshighadvisoryCompute derived values during render and fetch via a data layer; effects are only for external-system sync.
| 1 | Effects synchronize a component with an external system (DOM nodes, browser APIs, third-party widgets, subscriptions). They are not for deriving state from props/state or for fetching data. The official `eslint-plugin-react-hooks` recommended preset flags `set-state-in-effect` for this reason. |
| 2 | |
| 3 | Do not: |
| 4 | |
| 5 | - Mirror props/state into state with an effect plus `setState`. Compute it during render: `const visible = items.filter(i => i.active)`. Wrap only genuinely expensive work in `useMemo`. |
| 6 | - Reset state on a prop change with an effect. Pass a `key` so React remounts the subtree with fresh state: `<Profile userId={userId} key={userId} />`. |
| 7 | - Run logic that should happen because of a user action inside an effect. Put it in the event handler, which knows what the user actually did. |
| 8 | - Fetch data in a bare effect. This causes duplicate requests, loading flicker on every navigation, and StrictMode double-fetch in dev. Use a data layer (TanStack Query, RTK Query, the framework's server data layer / Server Components, or the `use()` hook with a cached promise). |
| 9 | |
| 10 | If you keep an effect, it must subscribe to something outside React and return a cleanup function. Prefer `useSyncExternalStore` for external stores. |
Interactive elements must be accessible/src/componentshighadvisoryUse native semantics, accessible names, visible focus, and full keyboard operability.
| 1 | A `div` or `span` with an `onClick` is not a button. It is unreachable by keyboard, invisible to screen readers, and a real defect for affected users. |
| 2 | |
| 3 | - Reach for native `button`, `a`, `label`, `input`, `select` before any custom control. A `button` gives you focus, Enter/Space activation, and the correct role for free. |
| 4 | - Every control has an accessible name (visible text, `aria-label`, or an associated `<label htmlFor>`), a visible `:focus-visible` state, and full keyboard operability. |
| 5 | - Associate inputs with labels using `useId()` for the `id`/`htmlFor` pair so ids stay stable and hydration-safe. |
| 6 | - Images that convey meaning have `alt`; decorative images have `alt=""`. Never use color as the only signal. |
| 7 | - If you must build a custom widget, follow the matching ARIA Authoring Practices pattern (role, states, key handling) in full rather than partially. |
Memories
3How we type component props/src/componentsExplicit prop types, no any, discriminated unions for variant components, import type under verbatimModuleSyntax.
| 1 | Conventions for typing components in this codebase. |
| 2 | |
| 3 | - Type props with an explicit `type` or `interface` on a plain function component. We do not use `React.FC`: it adds an implicit `children` even for components that take none and complicates generics. |
| 4 | - No `any`. When a shape is truly unknown use `unknown` and narrow. Type event handlers precisely (`React.ChangeEvent<HTMLInputElement>`, `React.MouseEvent<HTMLButtonElement>`). |
| 5 | - Derive types from a single source of truth (Zod schema, generated API types, a const object with `as const`) instead of restating shapes that can drift apart. |
| 6 | - For components with mutually exclusive modes, model props as a discriminated union on a literal discriminant rather than a bag of optional props. Example: `{ status: 'error'; message: string; onRetry: () => void } | { status: 'success'; message: string }`. This makes invalid prop combinations a compile error instead of a runtime check. |
| 7 | - Under `verbatimModuleSyntax: true` (our tsconfig), type-only imports must use `import type { Props } from './x'`. Mixing a value and a type in one statement without the modifier is an error; the lint autofix handles most of it. |
React Compiler handles memoization for new code/srcDon't hand-write useMemo/useCallback/React.memo in new components; never strip existing manual memoization.
| 1 | React Compiler 1.0 (stable, Oct 2025) auto-memoizes components and hooks at build time. This changes how we optimize here. |
| 2 | |
| 3 | - New code: write components without `useMemo`, `useCallback`, or `React.memo`. The compiler inserts memoization where it helps. Reaching for these manually is usually noise and can fight the compiler. |
| 4 | - Keep `useMemo` only for a measured expensive computation, or where a stable reference is semantically required (e.g. a value passed to a non-React API or a dependency array the compiler cannot see through, such as some third-party hooks). |
| 5 | - Do not delete existing manual memoization to "modernize" a file. The compiler's `preserve-manual-memoization` rule expects it left in place; removing it can change behavior. Migrate deliberately, not opportunistically. |
| 6 | - The compiler relies on the Rules of React. Run the `eslint-plugin-react-hooks` recommended (or recommended-latest) preset; its rules (`rules-of-hooks`, `exhaustive-deps`, `set-state-in-effect`, `purity`, `refs`) are how the compiler surfaces violations. Code that breaks these rules silently opts out of optimization. |
| 7 | - Performance work starts with the React DevTools profiler, not with sprinkling memo hooks. |
Component structure and state placement/src/componentsSmall single-responsibility components; colocate state, lift only when shared; split presentational from data access.
| 1 | How we structure components so the tree stays predictable. |
| 2 | |
| 3 | - One responsibility per component. When a component grows a second job (fetching + layout + a modal), split it. |
| 4 | - Colocate state with the component that owns it. Lift state up only when two siblings genuinely need the same value; do not hoist to a parent or context "just in case". |
| 5 | - Separate presentational components (props in, JSX out) from components that do data access or own significant state. Presentational components are trivial to test and reuse. |
| 6 | - Use lazy `useState` initialization for expensive initial values: `useState(() => readFromStorage())`, not `useState(readFromStorage())`, so the work runs once instead of every render. |
| 7 | - Custom hooks encapsulate reusable stateful logic and are named `useX`. Keep them focused; a hook that returns ten unrelated things is a refactor signal. |
| 8 | - Name files and exports consistently (one component per file, file name matches the export) so navigation is mechanical. |
Skills
1react-component-review/rootChecklist for reviewing a new or changed React + TypeScript component.
| 1 | --- |
| 2 | name: react-component-review |
| 3 | description: Review a React + TypeScript component for prop types, accessibility, hook correctness, and effect/memoization discipline before merge. |
| 4 | --- |
| 5 | |
| 6 | # React component review |
| 7 | |
| 8 | Run this before approving a new or changed component. |
| 9 | |
| 10 | ## Types |
| 11 | - [ ] Props are explicitly typed; no `any` (use `unknown` + narrowing if truly unknown) |
| 12 | - [ ] Variant/mode components use a discriminated union, not a pile of optional props |
| 13 | - [ ] Types derive from a single source of truth (schema / generated API types), not restated shapes |
| 14 | - [ ] Type-only imports use `import type` (verbatimModuleSyntax) |
| 15 | |
| 16 | ## Effects and state |
| 17 | - [ ] No effect that only computes derived state from props/state (compute during render) |
| 18 | - [ ] No effect resetting state on a prop change (use a `key` instead) |
| 19 | - [ ] No data fetching in a bare effect (use the data layer / Server Components / `use()`) |
| 20 | - [ ] Any remaining effect syncs an external system and has a cleanup function |
| 21 | - [ ] Hooks are called unconditionally at the top level; dependency arrays are honest |
| 22 | |
| 23 | ## Memoization |
| 24 | - [ ] No reflexive `useMemo`/`useCallback`/`React.memo` in new code (compiler handles it) |
| 25 | - [ ] Existing manual memoization left intact, not stripped |
| 26 | - [ ] `eslint-plugin-react-hooks` (recommended preset) passes with no disables |
| 27 | |
| 28 | ## Accessibility |
| 29 | - [ ] Interactive elements use native semantics (`button`, `a`, `input`, `label`) |
| 30 | - [ ] Every control has an accessible name, visible focus, and keyboard operability |
| 31 | - [ ] Inputs are associated to labels via a stable `useId` |
| 32 | - [ ] Images have correct `alt`; color is not the only signal |
| 33 | |
| 34 | ## Structure |
| 35 | - [ ] One clear responsibility; reasonable size |
| 36 | - [ ] State colocated, lifted only when genuinely shared |
| 37 | - [ ] Expensive initial state uses lazy `useState(() => ...)` |
Why this pattern
React and TypeScript components drift into any-typed props, inaccessible controls, and effect-driven data fetching.
Built for frontend teams writing React in TypeScript.
Keeps your assistant from:
- Untyped or any-typed props and event handlers
- div-with-onClick controls that fail keyboard and screen-reader users
- Fetching data inside effects instead of a real data layer
- License
- Apache-2.0
- Version
- 1.0.0
- Updated
- 2026-06-09