This skill should be used when the user asks to "review React hooks", "check useEffect usage", "optimize hooks performance", "design custom hooks", "refactor to use hooks", "remove unnecessary...
This skill provides guidance for writing clean, efficient React hooks code with emphasis on eliminating unnecessary hooks and following established best practices.
The primary principle is to use hooks only when necessary. Many React applications suffer from hook overuse, particularly:
useEffect for logic that should be in event handlersuseMemo/useCallback for cheap computationsuseState for derived stateBefore adding any hook, ask: "Can this be done without a hook?"
Never suppress react-hooks/exhaustive-deps warnings. The ESLint plugin understands React's rules better than manual reasoning. If a warning appears:
// eslint-disable-next-lineNever use useMemo for constant values. Define unchanging values at module scope:
// ❌ WRONG: useMemo for constants
function Component() {
const options = useMemo(
() => [
{ value: "a", label: "Option A" },
{ value: "b", label: "Option B" },
],
[],
);
}
// ✅ CORRECT: Module-level constants
const OPTIONS = [
{ value: "a", label: "Option A" },
{ value: "b", label: "Option B" },
] as const;
function Component() {
// Just use OPTIONS directly
}
All values returned from custom hooks must have stable references:
// ❌ WRONG: Unstable return values
function useData(id: string) {
const [data, setData] = useState(null);
return {
data,
refresh: () => fetch(id), // New function every render
meta: { id }, // New object every render
};
}
// ✅ CORRECT: All returns memoized or stable
function useData(id: string) {
const [data, setData] = useState(null);
const refresh = useCallback(() => fetch(id), [id]);
const meta = useMemo(() => ({ id }), [id]);
return { data, refresh, meta };
}
Use experimental/newer hooks when React version permits:
| Hook | Purpose | Replaces |
|---|---|---|
useEffectEvent |
Stable event callbacks in effects | useRef + manual sync |
use |
Read resources in render | useEffect + useState |
useOptimistic |
Optimistic UI updates | Manual state management |
useFormStatus |
Form submission state | Custom loading state |
Prefer React Suspense boundaries over manual loading state management:
// ❌ AVOID: Manual loading state in component
function UserProfile({ userId }: Props) {
const { data, isLoading, error } = useUser(userId);
if (isLoading) return <Spinner />;
if (error) return <ErrorDisplay error={error} />;
return <Profile user={data} />;
}
// ✅ PREFER: Suspense boundary with data hook
function UserProfile({ userId }: Props) {
const user = useUser(userId); // Suspends until ready
return <Profile user={user} />;
}
// Parent handles loading/error
<ErrorBoundary fallback={<ErrorDisplay />}>
<Suspense fallback={<Spinner />}>
<UserProfile userId={id} />
</Suspense>
</ErrorBoundary>;
Note: Internal hook implementations may still use { data, error, loading } structures. The preference for Suspense applies to component-level API design.
| Scenario | Wrong Approach | Correct Approach |
|---|---|---|
| Transform data for rendering | useEffect + useState |
Calculate during render |
| Respond to user event | useEffect watching state |
Event handler directly |
| Initialize from props | useEffect syncing props |
Compute in render or use key |
| Fetch on mount only | useEffect with [] |
Use data fetching library |
| Scenario | Skip Memoization When... |
|---|---|
| Simple calculations | Operation is O(1) or simple array methods |
| Non-object returns | Returning primitives (string, number, boolean) |
| No child optimization | Child components don't use React.memo |
| Development phase | Premature optimization without profiling |
// ❌ Anti-pattern: Storing derived state
const [items, setItems] = useState<Item[]>([]);
const [filteredItems, setFilteredItems] = useState<Item[]>([]);
useEffect(() => {
setFilteredItems(items.filter((item) => item.active));
}, [items]);
// ✅ Correct: Calculate during render
const [items, setItems] = useState<Item[]>([]);
const filteredItems = items.filter((item) => item.active);
// ❌ Anti-pattern: Effect watching state
const [query, setQuery] = useState("");
useEffect(() => {
if (query) {
analytics.track("search", { query });
}
}, [query]);
// ✅ Correct: Track in event handler
const handleSearch = (newQuery: string) => {
setQuery(newQuery);
if (newQuery) {
analytics.track("search", { query: newQuery });
}
};
// ❌ Anti-pattern: Effect to notify parent
useEffect(() => {
onChange(internalValue);
}, [internalValue, onChange]);
// ✅ Correct: Call during event
const handleChange = (value: string) => {
setInternalValue(value);
onChange(value);
};
// ❌ Anti-pattern: Sync props to state
const [value, setValue] = useState(initialValue);
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
// ✅ Correct: Use key to reset
<Editor key={documentId} initialValue={initialValue} />;
Effects are appropriate for:
// ✅ Legitimate: External system sync
useEffect(() => {
const map = new MapLibrary(mapRef.current);
map.setCenter(coordinates);
return () => map.destroy();
}, [coordinates]);
// ✅ Legitimate: Subscription
useEffect(() => {
const unsubscribe = store.subscribe(handleChange);
return unsubscribe;
}, []);
Never optimize without profiling. Use React DevTools Profiler to identify actual bottlenecks.
Try these before adding useMemo/useCallback:
When memoization is needed:
// Memoize expensive computations
const sortedData = useMemo(
() => data.toSorted((a, b) => complexSort(a, b)),
[data],
);
// Memoize callbacks for optimized children
const handleClick = useCallback(
(id: string) => dispatch({ type: "SELECT", id }),
[dispatch],
);
// Combine with React.memo for child optimization
const MemoizedChild = memo(function Child({ onClick }: Props) {
return <button onClick={onClick}>Click</button>;
});
Each custom hook should do one thing well:
// ✅ Focused hooks
function useLocalStorage<T>(key: string, initial: T) {
/* ... */
}
function useDebounce<T>(value: T, delay: number) {
/* ... */
}
function useMediaQuery(query: string) {
/* ... */
}
| Pattern | Use When | Example |
|---|---|---|
Tuple [value, setter] |
State-like API | useState, useLocalStorage |
Object { data, error, loading } |
Multiple related values | useFetch, useQuery |
| Single value | Read-only derived data | useMediaQuery, useOnline |
Build complex behavior from simple hooks:
function useSearchWithDebounce(initialQuery: string) {
const [query, setQuery] = useState(initialQuery);
const debouncedQuery = useDebounce(query, 300);
const results = useSearch(debouncedQuery);
return { query, setQuery, results };
}
| Mistake | Why It's Wrong | Fix |
|---|---|---|
useState + useEffect for filtering |
Extra render, sync bugs | Calculate during render |
useMemo(() => CONSTANT, []) |
Unnecessary overhead | Module-level constant |
// eslint-disable-next-line |
Hides real bugs | Fix the dependency issue |
| Unstable custom hook returns | Breaks consumer memoization | Memoize all non-primitives |
useEffect for analytics on click |
Delayed, indirect | Track in click handler |
| Manual loading/error state | Boilerplate, race conditions | Suspense + ErrorBoundary |
When reviewing React hooks code, verify:
Mandatory (Zero Tolerance)
exhaustive-deps warnings suppresseduseMemo for constant values (use module-level)Anti-Patterns
useEffect for derived state calculationsuseEffect for event response logicuseState for values computable from other state/propsuseMemo/useCallback without proven performance needQuality
For detailed guidance and examples, consult:
references/unnecessary-hooks.md - Comprehensive patterns for eliminating unnecessary hooks with before/after examplesreferences/custom-hooks.md - Advanced custom hook design patterns and composition strategiesreferences/dependency-array.md - Deep dive into dependency array management and common pitfallsWorking examples in examples/:
good-patterns.tsx - Correct hook usage examplesanti-patterns.tsx - Common mistakes with correctionsThe goal is writing React code that is:
Apply the principle: "The best hook is the one you don't need to write."