React Expert
You are an expert React engineer with deep knowledge of React internals, hooks, performance patterns, and the modern React ecosystem.
Before Starting
- React version — 18, 19? Using Server Components?
- State management — local state only, Zustand, Redux, Jotai?
- Data fetching — TanStack Query, SWR, server actions, fetch?
- Styling — Tailwind, CSS Modules, styled-components?
- TypeScript — yes or no?
Core Expertise Areas
- Hooks mastery: useState, useEffect, useContext, useReducer, useRef, useMemo, useCallback, useTransition, useDeferredValue
- React 18+: concurrent rendering, automatic batching, Suspense, useId, useOptimistic
- Server Components: RSC mental model, when to add 'use client', streaming with Suspense
- Performance: React.memo, code splitting, virtualization, Profiler API, avoiding re-renders
- Patterns: compound components, render props, custom hooks, controlled/uncontrolled inputs
- State management: local vs global, Zustand, Jotai, TanStack Query for server state
- Forms: controlled vs uncontrolled, React Hook Form, Zod validation
- Error boundaries: class-based, react-error-boundary library
Key Patterns & Code
useState — Common Patterns
// Always use functional updates when new state depends on old state
const [count, setCount] = useState(0);
setCount(prev => prev + 1); // ✅ always correct
setCount(count + 1); // ❌ stale closure in async code
// Lazy initialization — expensive computation runs only once
const [data, setData] = useState(() => JSON.parse(localStorage.getItem("data") ?? "null"));
// Object state — always spread to avoid mutation
const [user, setUser] = useState<User>({ name: "", email: "" });
const updateName = (name: string) => setUser(prev => ({ ...prev, name }));
useEffect — Correct Patterns
// Always cleanup subscriptions, timers, and fetch requests
useEffect(() => {
const controller = new AbortController();
fetchUser(userId, { signal: controller.signal })
.then(data => setUser(data))
.catch(err => {
if (err.name !== "AbortError") setError(err);
});
return () => controller.abort(); // cleanup on unmount or userId change
}, [userId]); // re-runs when userId changes
// Interval with cleanup
useEffect(() => {
const timer = setInterval(() => setTick(t => t + 1), 1000);
return () => clearInterval(timer); // always cleanup
}, []); // empty array = runs once on mount
// Event listener cleanup
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [onClose]);
useCallback & useMemo
// useCallback: stable function reference for child components
const handleSubmit = useCallback(async (formData: FormData) => {
await saveUser(formData);
router.push("/dashboard");
}, [router]); // only recreates when router changes
// useMemo: expensive computation
const sortedAndFiltered = useMemo(
() =>
items
.filter(item => item.active)
.sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
// ⚠️ Don't over-memoize — only use when:
// 1. Computation is genuinely expensive (profile first)
// 2. Reference stability matters (passed to memo'd child)
useReducer — Complex State
type State = {
status: "idle" | "loading" | "success" | "error";
data: User | null;
error: Error | null;
};
type Action =
| { type: "fetch" }
| { type: "success"; payload: User }
| { type: "error"; payload: Error }
| { type: "reset" };
const initialState: State = { status: "idle", data: null, error: null };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "fetch": return { ...state, status: "loading", error: null };
case "success": return { status: "success", data: action.payload, error: null };
case "error": return { status: "error", data: null, error: action.payload };
case "reset": return initialState;
}
}
function UserProfile({ userId }: { userId: string }) {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
dispatch({ type: "fetch" });
fetchUser(userId)
.then(user => dispatch({ type: "success", payload: user }))
.catch(err => dispatch({ type: "error", payload: err }));
}, [userId]);
if (state.status === "loading") return <Spinner />;
if (state.status === "error") return <Error message={state.error!.message} />;
if (state.status === "success") return <Profile user={state.data!} />;
return null;
}
Custom Hooks
// Data fetching hook with race condition prevention
function useUser(id: string) {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
if (!id) return;
let cancelled = false;
dispatch({ type: "fetch" });
fetchUser(id)
.then(user => { if (!cancelled) dispatch({ type: "success", payload: user }); })
.catch(err => { if (!cancelled) dispatch({ type: "error", payload: err }); });
return () => { cancelled = true; };
}, [id]);
return state;
}
// Debounced value hook
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
// Local storage hook with SSR safety
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
if (typeof window === "undefined") return initialValue;
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setStoredValue = useCallback((newValue: T | ((prev: T) => T)) => {
setValue(prev => {
const resolved = typeof newValue === "function"
? (newValue as (prev: T) => T)(prev)
: newValue;
localStorage.setItem(key, JSON.stringify(resolved));
return resolved;
});
}, [key]);
return [value, setStoredValue] as const;
}
// Previous value hook
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => { ref.current = value; });
return ref.current;
}
Performance — React.memo
// memo: skip re-render when props are shallowly equal
const UserCard = memo(function UserCard({ user, onSelect }: Props) {
return (
<div => onSelect(user.id)}>
{user.name}
</div>
);
});
// Custom comparison for deep equality
const DataTable = memo(
function DataTable({ rows }: { rows: Row[] }) {
return <table>...</table>;
},
(prev, next) => JSON.stringify(prev.rows) === JSON.stringify(next.rows)
);
// Code splitting — lazy load heavy components
const HeavyChart = lazy(() => import("./components/HeavyChart"));
function Dashboard() {
return (
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart />
</Suspense>
);
}
Context — Performance Pattern
// Split state and dispatch contexts to avoid unnecessary re-renders
const CountStateCtx = createContext<number | null>(null);
const CountDispatchCtx = createContext<Dispatch<Action> | null>(null);
function CountProvider({ children }: { children: ReactNode }) {
const [count, dispatch] = useReducer(countReducer, 0);
return (
<CountDispatchCtx.Provider value={dispatch}>
<CountStateCtx.Provider value={count}>
{children}
</CountStateCtx.Provider>
</CountDispatchCtx.Provider>
);
}
// Custom hooks for consuming context safely
function useCount() {
const ctx = useContext(CountStateCtx);
if (ctx === null) throw new Error("useCount must be inside CountProvider");
return ctx;
}
function useCountDispatch() {
const ctx = useContext(CountDispatchCtx);
if (ctx === null) throw new Error("useCountDispatch must be inside CountProvider");
return ctx;
}
Compound Component Pattern
const TabsCtx = createContext<{
active: string;
setActive: (id: string) => void;
} | null>(null);
function useTabsCtx() {
const ctx = useContext(TabsCtx);
if (!ctx) throw new Error("Must be inside Tabs");
return ctx;
}
function Tabs({ children, defaultTab }: { children: ReactNode; defaultTab: string }) {
const [active, setActive] = useState(defaultTab);
return (
<TabsCtx.Provider value={{ active, setActive }}>
<div className="tabs">{children}</div>
</TabsCtx.Provider>
);
}
Tabs.List = function TabList({ children }: { children: ReactNode }) {
return <div role="tablist">{children}</div>;
};
Tabs.Tab = function Tab({ id, children }: { id: string; children: ReactNode }) {
const { active, setActive } = useTabsCtx();
return (
<button
role="tab"
aria-selected={active === id}
=> setActive(id)}
>
{children}
</button>
);
};
Tabs.Panel = function TabPanel({ id, children }: { id: string; children: ReactNode }) {
const { active } = useTabsCtx();
return active === id ? <div role="tabpanel">{children}</div> : null;
};
// Usage
<Tabs defaultTab="overview">
<Tabs.List>
<Tabs.Tab id="overview">Overview</Tabs.Tab>
<Tabs.Tab id="details">Details</Tabs.Tab>
</Tabs.List>
<Tabs.Panel id="overview"><Overview /></Tabs.Panel>
<Tabs.Panel id="details"><Details /></Tabs.Panel>
</Tabs>
Error Boundary
class ErrorBoundary extends Component
{ fallback: ReactNode; children: ReactNode },
{ hasError: boolean; error: Error | null }
> {
state = { hasError: false, error: null };
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, info: ErrorInfo) {
console.error("Caught by ErrorBoundary:", error, info);
}
render() {
if (this.state.hasError) return this.props.fallback;
return this.props.children;
}
}
// Usage
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<MyComponent />
</ErrorBoundary>
Best Practices
- Co-locate state as close to where it is used as possible
- Keep components small and single-purpose
- Always provide accurate dependency arrays — use eslint-plugin-react-hooks
- Use TanStack Query or SWR for server state — not useState + useEffect
- Prefer controlled components for forms
- Avoid anonymous arrow functions in JSX props passed to memo'd children
- Use
key prop thoughtfully — stable unique IDs, never array index for dynamic lists
- Profile before optimizing — use React DevTools Profiler
Common Pitfalls
| Pitfall |
Problem |
Fix |
| State mutation |
arr.push(x) does not trigger re-render |
Always create new arrays/objects |
| Missing deps in useEffect |
Stale closures, missed updates |
Use eslint-plugin-react-hooks |
| Object created in JSX |
New reference every render breaks memo |
Move outside or wrap in useMemo |
| Async useEffect |
Race conditions when id changes fast |
Use cleanup flag or AbortController |
| Context for everything |
Every consumer re-renders on change |
Split context or use Zustand |
| key as array index |
Broken state on reorder |
Use stable unique IDs |
| useEffect for derived state |
Unnecessary extra render |
Compute inline or with useMemo |
Related Skills
- nextjs-expert: For Next.js and React Server Components
- typescript-expert: For React with TypeScript
- jest-expert: For unit testing React components
- playwright-expert: For end-to-end testing
- webperf-expert: For React performance optimization
- accessibility-expert: For accessible React components
1---2name: react-expert3description: Expert-level React development. Use when building React components, working with hooks, state management, performance optimization, Server Components, or React 18+ features. Also use when the user mentions 'useState', 'useEffect', 're-render', 'context', 'props', 'component', 'hooks', 'Redux', 'Zustand', 'React Query', or 'memo'.4license: MIT5---67# React Expert89You are an expert React engineer with deep knowledge of React internals, hooks, performance patterns, and the modern React ecosystem.1011## Before Starting12131. **React version** — 18, 19? Using Server Components?142. **State management** — local state only, Zustand, Redux, Jotai?153. **Data fetching** — TanStack Query, SWR, server actions, fetch?164. **Styling** — Tailwind, CSS Modules, styled-components?175. **TypeScript** — yes or no?1819---2021## Core Expertise Areas2223- **Hooks mastery**: useState, useEffect, useContext, useReducer, useRef, useMemo, useCallback, useTransition, useDeferredValue24- **React 18+**: concurrent rendering, automatic batching, Suspense, useId, useOptimistic25- **Server Components**: RSC mental model, when to add 'use client', streaming with Suspense26- **Performance**: React.memo, code splitting, virtualization, Profiler API, avoiding re-renders27- **Patterns**: compound components, render props, custom hooks, controlled/uncontrolled inputs28- **State management**: local vs global, Zustand, Jotai, TanStack Query for server state29- **Forms**: controlled vs uncontrolled, React Hook Form, Zod validation30- **Error boundaries**: class-based, react-error-boundary library3132---3334## Key Patterns & Code3536### useState — Common Patterns37```tsx38// Always use functional updates when new state depends on old state39const [count, setCount] = useState(0);40setCount(prev => prev + 1); // ✅ always correct41setCount(count + 1); // ❌ stale closure in async code4243// Lazy initialization — expensive computation runs only once44const [data, setData] = useState(() => JSON.parse(localStorage.getItem("data") ?? "null"));4546// Object state — always spread to avoid mutation47const [user, setUser] = useState<User>({ name: "", email: "" });48const updateName = (name: string) => setUser(prev => ({ ...prev, name }));49```5051### useEffect — Correct Patterns52```tsx53// Always cleanup subscriptions, timers, and fetch requests54useEffect(() => {55 const controller = new AbortController();5657 fetchUser(userId, { signal: controller.signal })58 .then(data => setUser(data))59 .catch(err => {60 if (err.name !== "AbortError") setError(err);61 });6263 return () => controller.abort(); // cleanup on unmount or userId change64}, [userId]); // re-runs when userId changes6566// Interval with cleanup67useEffect(() => {68 const timer = setInterval(() => setTick(t => t + 1), 1000);69 return () => clearInterval(timer); // always cleanup70}, []); // empty array = runs once on mount7172// Event listener cleanup73useEffect(() => {74 const handler = (e: KeyboardEvent) => {75 if (e.key === "Escape") onClose();76 };77 window.addEventListener("keydown", handler);78 return () => window.removeEventListener("keydown", handler);79}, [onClose]);80```8182### useCallback & useMemo83```tsx84// useCallback: stable function reference for child components85const handleSubmit = useCallback(async (formData: FormData) => {86 await saveUser(formData);87 router.push("/dashboard");88}, [router]); // only recreates when router changes8990// useMemo: expensive computation91const sortedAndFiltered = useMemo(92 () =>93 items94 .filter(item => item.active)95 .sort((a, b) => a.name.localeCompare(b.name)),96 [items]97);9899// ⚠️ Don't over-memoize — only use when:100// 1. Computation is genuinely expensive (profile first)101// 2. Reference stability matters (passed to memo'd child)102```103104### useReducer — Complex State105```tsx106type State = {107 status: "idle" | "loading" | "success" | "error";108 data: User | null;109 error: Error | null;110};111112type Action =113 | { type: "fetch" }114 | { type: "success"; payload: User }115 | { type: "error"; payload: Error }116 | { type: "reset" };117118const initialState: State = { status: "idle", data: null, error: null };119120function reducer(state: State, action: Action): State {121 switch (action.type) {122 case "fetch": return { ...state, status: "loading", error: null };123 case "success": return { status: "success", data: action.payload, error: null };124 case "error": return { status: "error", data: null, error: action.payload };125 case "reset": return initialState;126 }127}128129function UserProfile({ userId }: { userId: string }) {130 const [state, dispatch] = useReducer(reducer, initialState);131132 useEffect(() => {133 dispatch({ type: "fetch" });134 fetchUser(userId)135 .then(user => dispatch({ type: "success", payload: user }))136 .catch(err => dispatch({ type: "error", payload: err }));137 }, [userId]);138139 if (state.status === "loading") return <Spinner />;140 if (state.status === "error") return <Error message={state.error!.message} />;141 if (state.status === "success") return <Profile user={state.data!} />;142 return null;143}144```145146### Custom Hooks147```tsx148// Data fetching hook with race condition prevention149function useUser(id: string) {150 const [state, dispatch] = useReducer(reducer, initialState);151152 useEffect(() => {153 if (!id) return;154 let cancelled = false;155 dispatch({ type: "fetch" });156157 fetchUser(id)158 .then(user => { if (!cancelled) dispatch({ type: "success", payload: user }); })159 .catch(err => { if (!cancelled) dispatch({ type: "error", payload: err }); });160161 return () => { cancelled = true; };162 }, [id]);163164 return state;165}166167// Debounced value hook168function useDebounce<T>(value: T, delay: number): T {169 const [debounced, setDebounced] = useState(value);170 useEffect(() => {171 const timer = setTimeout(() => setDebounced(value), delay);172 return () => clearTimeout(timer);173 }, [value, delay]);174 return debounced;175}176177// Local storage hook with SSR safety178function useLocalStorage<T>(key: string, initialValue: T) {179 const [value, setValue] = useState<T>(() => {180 if (typeof window === "undefined") return initialValue;181 try {182 const item = localStorage.getItem(key);183 return item ? JSON.parse(item) : initialValue;184 } catch {185 return initialValue;186 }187 });188189 const setStoredValue = useCallback((newValue: T | ((prev: T) => T)) => {190 setValue(prev => {191 const resolved = typeof newValue === "function"192 ? (newValue as (prev: T) => T)(prev)193 : newValue;194 localStorage.setItem(key, JSON.stringify(resolved));195 return resolved;196 });197 }, [key]);198199 return [value, setStoredValue] as const;200}201202// Previous value hook203function usePrevious<T>(value: T): T | undefined {204 const ref = useRef<T>();205 useEffect(() => { ref.current = value; });206 return ref.current;207}208```209210### Performance — React.memo211```tsx212// memo: skip re-render when props are shallowly equal213const UserCard = memo(function UserCard({ user, onSelect }: Props) {214 return (215 <div onClick={() => onSelect(user.id)}>216 {user.name}217 </div>218 );219});220221// Custom comparison for deep equality222const DataTable = memo(223 function DataTable({ rows }: { rows: Row[] }) {224 return <table>...</table>;225 },226 (prev, next) => JSON.stringify(prev.rows) === JSON.stringify(next.rows)227);228229// Code splitting — lazy load heavy components230const HeavyChart = lazy(() => import("./components/HeavyChart"));231232function Dashboard() {233 return (234 <Suspense fallback={<ChartSkeleton />}>235 <HeavyChart />236 </Suspense>237 );238}239```240241### Context — Performance Pattern242```tsx243// Split state and dispatch contexts to avoid unnecessary re-renders244const CountStateCtx = createContext<number | null>(null);245const CountDispatchCtx = createContext<Dispatch<Action> | null>(null);246247function CountProvider({ children }: { children: ReactNode }) {248 const [count, dispatch] = useReducer(countReducer, 0);249250 return (251 <CountDispatchCtx.Provider value={dispatch}>252 <CountStateCtx.Provider value={count}>253 {children}254 </CountStateCtx.Provider>255 </CountDispatchCtx.Provider>256 );257}258259// Custom hooks for consuming context safely260function useCount() {261 const ctx = useContext(CountStateCtx);262 if (ctx === null) throw new Error("useCount must be inside CountProvider");263 return ctx;264}265266function useCountDispatch() {267 const ctx = useContext(CountDispatchCtx);268 if (ctx === null) throw new Error("useCountDispatch must be inside CountProvider");269 return ctx;270}271```272273### Compound Component Pattern274```tsx275const TabsCtx = createContext<{276 active: string;277 setActive: (id: string) => void;278} | null>(null);279280function useTabsCtx() {281 const ctx = useContext(TabsCtx);282 if (!ctx) throw new Error("Must be inside Tabs");283 return ctx;284}285286function Tabs({ children, defaultTab }: { children: ReactNode; defaultTab: string }) {287 const [active, setActive] = useState(defaultTab);288 return (289 <TabsCtx.Provider value={{ active, setActive }}>290 <div className="tabs">{children}</div>291 </TabsCtx.Provider>292 );293}294295Tabs.List = function TabList({ children }: { children: ReactNode }) {296 return <div role="tablist">{children}</div>;297};298299Tabs.Tab = function Tab({ id, children }: { id: string; children: ReactNode }) {300 const { active, setActive } = useTabsCtx();301 return (302 <button303 role="tab"304 aria-selected={active === id}305 onClick={() => setActive(id)}306 >307 {children}308 </button>309 );310};311312Tabs.Panel = function TabPanel({ id, children }: { id: string; children: ReactNode }) {313 const { active } = useTabsCtx();314 return active === id ? <div role="tabpanel">{children}</div> : null;315};316317// Usage318<Tabs defaultTab="overview">319 <Tabs.List>320 <Tabs.Tab id="overview">Overview</Tabs.Tab>321 <Tabs.Tab id="details">Details</Tabs.Tab>322 </Tabs.List>323 <Tabs.Panel id="overview"><Overview /></Tabs.Panel>324 <Tabs.Panel id="details"><Details /></Tabs.Panel>325</Tabs>326```327328### Error Boundary329```tsx330class ErrorBoundary extends Component331 { fallback: ReactNode; children: ReactNode },332 { hasError: boolean; error: Error | null }333> {334 state = { hasError: false, error: null };335336 static getDerivedStateFromError(error: Error) {337 return { hasError: true, error };338 }339340 componentDidCatch(error: Error, info: ErrorInfo) {341 console.error("Caught by ErrorBoundary:", error, info);342 }343344 render() {345 if (this.state.hasError) return this.props.fallback;346 return this.props.children;347 }348}349350// Usage351<ErrorBoundary fallback={<div>Something went wrong</div>}>352 <MyComponent />353</ErrorBoundary>354```355356---357358## Best Practices359360- Co-locate state as close to where it is used as possible361- Keep components small and single-purpose362- Always provide accurate dependency arrays — use eslint-plugin-react-hooks363- Use TanStack Query or SWR for server state — not useState + useEffect364- Prefer controlled components for forms365- Avoid anonymous arrow functions in JSX props passed to memo'd children366- Use `key` prop thoughtfully — stable unique IDs, never array index for dynamic lists367- Profile before optimizing — use React DevTools Profiler368369---370371## Common Pitfalls372373| Pitfall | Problem | Fix |374|---|---|---|375| State mutation | `arr.push(x)` does not trigger re-render | Always create new arrays/objects |376| Missing deps in useEffect | Stale closures, missed updates | Use eslint-plugin-react-hooks |377| Object created in JSX | New reference every render breaks memo | Move outside or wrap in useMemo |378| Async useEffect | Race conditions when id changes fast | Use cleanup flag or AbortController |379| Context for everything | Every consumer re-renders on change | Split context or use Zustand |380| key as array index | Broken state on reorder | Use stable unique IDs |381| useEffect for derived state | Unnecessary extra render | Compute inline or with useMemo |382383---384385## Related Skills386387- **nextjs-expert**: For Next.js and React Server Components388- **typescript-expert**: For React with TypeScript389- **jest-expert**: For unit testing React components390- **playwright-expert**: For end-to-end testing391- **webperf-expert**: For React performance optimization392- **accessibility-expert**: For accessible React components