mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-17 05:31:27 +08:00
1f0486b8d1
Five rule files mirroring per-language convention (coding-style, hooks, patterns, security, testing). Each has `paths:` glob frontmatter for auto-activation when editing matching files. - coding-style.md: file extensions, naming, JSX, RSC boundary - hooks.md: React hooks (NOT Claude Code hooks) — rules-of-hooks, dep arrays, cleanup, memoization, React 19 additions - patterns.md: container/presentational split, state location decision tree, Suspense + error boundaries, forms, data fetching - security.md: dangerouslySetInnerHTML, unsafe URL schemes, server-action validation, env-var leaks, CSP - testing.md: RTL queries, userEvent, async, MSW, axe, anti-patterns Each file extends typescript/* and common/* rules.
188 lines
6.4 KiB
Markdown
188 lines
6.4 KiB
Markdown
---
|
|
paths:
|
|
- "**/*.tsx"
|
|
- "**/*.jsx"
|
|
- "**/hooks/**/*.ts"
|
|
- "**/hooks/**/*.js"
|
|
- "**/use-*.ts"
|
|
- "**/use-*.tsx"
|
|
---
|
|
# React Hooks
|
|
|
|
> This file covers **React hooks** (`useState`, `useEffect`, `useMemo`, `useCallback`, custom hooks) — NOT the Claude Code `hooks/` runtime system. Naming matches the per-language convention `rules/<lang>/hooks.md` used across this repo.
|
|
>
|
|
> Extends [typescript/patterns.md](../typescript/patterns.md) and [common/patterns.md](../common/patterns.md).
|
|
|
|
## Rules of Hooks
|
|
|
|
Enforce `eslint-plugin-react-hooks` with `react-hooks/rules-of-hooks` set to error.
|
|
|
|
1. Hooks only at the top level of a function component or another hook
|
|
2. Never in loops, conditionals, nested functions, or after early returns
|
|
3. Always called in the same order on every render
|
|
4. Only inside React function components or custom hooks (functions starting with `use`)
|
|
|
|
```tsx
|
|
// WRONG: conditional hook
|
|
function Foo({ enabled }: { enabled: boolean }) {
|
|
if (enabled) {
|
|
const [x, setX] = useState(0); // rule violation
|
|
}
|
|
}
|
|
|
|
// CORRECT: hook unconditional, condition inside
|
|
function Foo({ enabled }: { enabled: boolean }) {
|
|
const [x, setX] = useState(0);
|
|
if (!enabled) return null;
|
|
return <span>{x}</span>;
|
|
}
|
|
```
|
|
|
|
## `useEffect` — When NOT to Use
|
|
|
|
`useEffect` is for synchronizing with external systems (subscriptions, browser APIs, third-party libraries). It is **not** the right tool for:
|
|
|
|
- Derived state — compute it during render
|
|
- Transforming data for rendering — compute it during render
|
|
- Resetting state when a prop changes — use a `key` on the parent or derive from props
|
|
- Notifying parents of state changes — call the callback in the event handler
|
|
- Initializing app-level singletons — call the function module-side or in `main.tsx`
|
|
|
|
```tsx
|
|
// WRONG: effect for derived state
|
|
const [fullName, setFullName] = useState("");
|
|
useEffect(() => {
|
|
setFullName(`${first} ${last}`);
|
|
}, [first, last]);
|
|
|
|
// CORRECT: derive during render
|
|
const fullName = `${first} ${last}`;
|
|
```
|
|
|
|
## Dependency Arrays
|
|
|
|
- Always include every reactive value referenced inside the effect/callback
|
|
- Enable `react-hooks/exhaustive-deps` lint rule — never silence it without a comment explaining why
|
|
- If the dep array grows unwieldy, the effect is doing too much — split it
|
|
- Stable identity for functions passed in deps: wrap in `useCallback` only when the function is itself a dependency of another hook or passed to a memoized child
|
|
|
|
## Cleanup
|
|
|
|
Every subscription, interval, listener, or in-flight request must clean up.
|
|
|
|
```tsx
|
|
useEffect(() => {
|
|
const controller = new AbortController();
|
|
fetch(url, { signal: controller.signal }).then(handleResponse);
|
|
return () => controller.abort();
|
|
}, [url]);
|
|
```
|
|
|
|
```tsx
|
|
useEffect(() => {
|
|
const id = setInterval(tick, 1000);
|
|
return () => clearInterval(id);
|
|
}, []);
|
|
```
|
|
|
|
Missing cleanup = race conditions when deps change, memory leaks on unmount.
|
|
|
|
## `useMemo` and `useCallback` — When Worth It
|
|
|
|
Default position: **do not memoize**. Add `useMemo` / `useCallback` only when:
|
|
|
|
1. The value is passed to a `React.memo`-wrapped child as a prop, and identity matters
|
|
2. The value is a dependency of another `useEffect` / `useMemo` / `useCallback`
|
|
3. The computation is measurably expensive (profile before assuming)
|
|
|
|
Premature memoization adds noise, hides bugs, and can be slower than the recompute it replaces.
|
|
|
|
## Custom Hooks
|
|
|
|
Extract a custom hook when:
|
|
|
|
- The same hook sequence (state + effect + computed) appears in 2+ components
|
|
- The logic has a clear, nameable purpose (`useDebounce`, `useOnClickOutside`, `useLocalStorage`)
|
|
- You want to test the logic independently of any component
|
|
|
|
Do NOT extract when:
|
|
|
|
- It would have a single caller — inline it
|
|
- The "hook" is just `useState` with a different name — adds indirection, no value
|
|
|
|
```tsx
|
|
export function useDebounce<T>(value: T, delay: number): T {
|
|
const [debounced, setDebounced] = useState(value);
|
|
useEffect(() => {
|
|
const id = setTimeout(() => setDebounced(value), delay);
|
|
return () => clearTimeout(id);
|
|
}, [value, delay]);
|
|
return debounced;
|
|
}
|
|
```
|
|
|
|
## `useState` Patterns
|
|
|
|
- Initial state from prop only at mount: pass a function `useState(() => computeInitial(prop))` when computation is expensive
|
|
- Functional updater when the new state depends on the old: `setCount(c => c + 1)` — never `setCount(count + 1)` inside async or batched contexts
|
|
- Group related state into one object only when they always change together; otherwise split into multiple `useState` calls
|
|
- Use `useReducer` once state transitions are conditional on the previous state or there are 3+ related values
|
|
|
|
## `useRef` Patterns
|
|
|
|
- DOM refs for imperative APIs (focus, scroll, third-party libs)
|
|
- Mutable container that does not trigger re-render (timer ids, previous values, "is mounted" flags)
|
|
- Never read or write `ref.current` during render — only inside effects or event handlers
|
|
- `useImperativeHandle` only when exposing a child API to a parent ref — last-resort escape hatch
|
|
|
|
## `useSyncExternalStore`
|
|
|
|
Use this hook to subscribe to any external store (browser API, third-party state lib, custom event emitter). It is the supported way to make external state safe with concurrent rendering.
|
|
|
|
```tsx
|
|
const isOnline = useSyncExternalStore(
|
|
(cb) => {
|
|
window.addEventListener("online", cb);
|
|
window.addEventListener("offline", cb);
|
|
return () => {
|
|
window.removeEventListener("online", cb);
|
|
window.removeEventListener("offline", cb);
|
|
};
|
|
},
|
|
() => navigator.onLine,
|
|
() => true,
|
|
);
|
|
```
|
|
|
|
## React 19 Additions
|
|
|
|
- `use()` — unwrap promises and contexts inline; usable conditionally (only hook with that property)
|
|
- `useFormStatus()` / `useFormState()` (or `useActionState`) — form submission state without prop drilling
|
|
- `useOptimistic()` — optimistic UI updates while a server action is pending
|
|
- `useTransition()` — mark non-urgent state updates so urgent ones stay responsive
|
|
|
|
When the project targets React 19+, prefer these over hand-rolled equivalents.
|
|
|
|
## Stale Closure Trap
|
|
|
|
Async handlers and intervals capture the values from the render where they were created. Fix by:
|
|
|
|
1. Using the functional updater form of `setState`
|
|
2. Putting the changing value in the dep array of `useEffect` and rebuilding the handler
|
|
3. Reading from a ref that is kept in sync
|
|
|
|
## Lint Configuration
|
|
|
|
Required rules:
|
|
|
|
```json
|
|
{
|
|
"rules": {
|
|
"react-hooks/rules-of-hooks": "error",
|
|
"react-hooks/exhaustive-deps": "warn"
|
|
}
|
|
}
|
|
```
|
|
|
|
Treat `exhaustive-deps` warnings as errors in CI for new code.
|