---
name: sdlc-react-conventions
description: |
React component structure, hooks rules, file naming, project layout, composition patterns, performance idioms, and effects discipline. Apply when implementing or modifying React SPA code.
Use this skill to:
- Structure a new component or feature folder.
- Apply hooks correctly (rules, naming, dependency arrays).
- Compose components via children, render props, or compound patterns.
- Write effects only when needed (and avoid common misuses).
- Pick performance escape hatches (memo, useMemo, useCallback) when justified.
Do NOT use this skill for:
- State management lib choice (see react-state-management).
- Routing primitives (see react-routing).
- Form patterns (see react-forms).
- Testing (see react-testing).
paths: ["src/**/*.tsx", "src/**/*.jsx", "src/**/*.ts", "src/**/*.js"]
---
# React Conventions
This skill consolidates idioms that hold across React SPA projects. Apply alongside `sdlc-typescript-patterns` (general TS strictness).
## Project layout
Two common structures:
### Feature-based (preferred for medium+ apps)
```
src/
├── main.tsx # entry — ReactDOM.createRoot
├── App.tsx # root component
├── routes.tsx # route definitions (or routes/ folder)
├── features/
│ ├── users/
│ │ ├── UserList.tsx
│ │ ├── UserDetail.tsx
│ │ ├── UserForm.tsx
│ │ ├── api/
│ │ │ └── users.ts # fetcher / TanStack Query hooks
│ │ ├── hooks/
│ │ │ └── useUsers.ts
│ │ └── types.ts
│ └── orders/
│ └── ...
├── components/
│ ├── ui/ # primitives (Button, Input, Modal)
│ └── shared/ # cross-feature shared components
├── lib/ # framework-agnostic utilities
│ ├── http.ts
│ └── format.ts
├── hooks/ # cross-feature hooks (useDebounce, useMediaQuery)
├── styles/ # global CSS, design tokens
└── types/ # global types
```
### Type-based (acceptable for small apps)
```
src/
├── components/
├── hooks/
├── pages/ # route components
├── lib/
└── styles/
```
Mirror what exists. Don't refactor structure as part of feature work.
## File naming
| What | Convention |
|---|---|
| Component file | `PascalCase.tsx` (`UserCard.tsx`) |
| Hook file | `useCamelCase.ts` (`useDebounce.ts`) |
| Utility module | `kebab-case.ts` or `camelCase.ts` (match project) |
| Test file | `*.test.tsx` colocated, OR mirror in `tests/` |
| Story file | `*.stories.tsx` colocated (Storybook) |
| Type file | `types.ts` per feature, OR `*.types.ts` |
## Hooks rules
```tsx
// ✅ Top-level only
function MyComponent() {
const [count, setCount] = useState(0);
const ref = useRef(null);
useEffect(() => { /* ... */ }, []);
return
;
}
// ❌ Conditional hook
function Bad() {
if (someCondition) {
const [x, setX] = useState(0); // breaks Rules of Hooks
}
}
// ❌ Hook in loop
function AlsoBad() {
for (let i = 0; i < 5; i++) {
useState(0); // breaks
}
}
```
Custom hooks always start with `use*`:
```ts
// src/hooks/useDebounce.ts
import { useEffect, useState } from 'react';
export function useDebounce(value: T, delay = 300): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id);
}, [value, delay]);
return debounced;
}
```
The `use*` prefix is what enables the linter and React itself to recognize it as a hook.
### Dependency arrays
Trust the `eslint-plugin-react-hooks` `exhaustive-deps` rule. If it flags, fix the dep array — don't disable the rule.
If a function is recreated each render but its identity matters (used in effect dep array, or as prop to memoized child):
```tsx
// Stabilize the function reference
const handleClick = useCallback((id: string) => {
doSomething(id);
}, []); // deps: anything closed over from outer scope
```
Same for objects:
```tsx
const config = useMemo(() => ({ retries: 3, timeout: 5000 }), []);
```
But: don't pre-emptively wrap everything in `useCallback`/`useMemo`. Only when the deps array failure or memoization break is real.
## Component patterns
### Plain functional component
```tsx
type Props = {
title: string;
onClose: () => void;
children: React.ReactNode;
};
export function Modal({ title, onClose, children }: Props) {
return (
{title}
{children}
);
}
```
### Compound components (shared context)
```tsx
// Tabs.tsx
import { createContext, useContext, useState } from 'react';
type TabsContext = { active: string; setActive: (s: string) => void };
const Ctx = createContext(null);
export function Tabs({ defaultTab, children }: { defaultTab: string; children: React.ReactNode }) {
const [active, setActive] = useState(defaultTab);
return {children};
}
export function Tab({ name, children }: { name: string; children: React.ReactNode }) {
const ctx = useContext(Ctx);
if (!ctx) throw new Error('Tab must be inside Tabs');
return (
);
}
export function TabPanel({ name, children }: { name: string; children: React.ReactNode }) {
const ctx = useContext(Ctx);
if (!ctx) throw new Error('TabPanel must be inside Tabs');
return ctx.active === name ?