Web Development11 min read

React.js

Meta's declarative, component-based library for building user interfaces - the most widely adopted frontend framework, powering everything from SPAs to server-rendered apps via Next.js.

FrontendJavaScriptTypeScriptSPAHooks
---
name: react
description: >
  Idiomatic React - component design, hooks, state management, and render performance. Use this whenever the user is writing, reviewing, or debugging React or Next.js code, mentions JSX, hooks, useEffect, components, or a frontend UI in a JS/TS project, even if they don't name React explicitly.
---

React is a component model, not a full framework. Components are pure functions of props and state - side effects are quarantined in useEffect or custom hooks; server state (fetched data) and client UI state (toggles, selections) are managed by different tools. Correctness and readability come before performance: concurrent rendering makes React resilient to slow renders, and the React Compiler eliminates most hand-memoization entirely. Measure before optimizing; the Profiler shows the real bottleneck.

Workflow

  1. Understand the data shape before naming a componentThe component's props interface is its contract. Designing it from the data outward prevents the most common mistake: components that mix concerns because the data shape was never examined.
  2. Write the component as a pure render first - no hooks, no side effectsA pure function of props is the easiest thing to read, test, and debug. Add stateful complexity only after the rendering logic is correct.
  3. Add useState or useReducer for interactivity that belongs inside this componentLocal state first. Lift to the nearest common ancestor only when a sibling actually needs it - not speculatively. In React 19+, consider useActionState for form-driven state transitions.
  4. Quarantine side effects in useEffect with a complete dependency array - and add a cleanup return for fetch and subscription effectsMissing deps cause stale closures; extra deps cause infinite loops. Lint with eslint-plugin-react-hooks and fix every warning. For fetch effects, use the ignore flag pattern (let ignore = false; return () => { ignore = true; }) to prevent setting state on an unmounted component - this is the most common real production bug in effects.
  5. Manage server state with TanStack Query or SWR, not useState + useEffectHand-rolling a fetch into useState misses caching, deduplication, background refetch, stale-while-revalidate, optimistic updates, and error retry. In React 19+, use() with Suspense is a viable alternative for cases where the promise is passed in from outside.
  6. Extract shared stateful logic into a named custom hookuseUserSession is testable, reusable, and tells future readers what the component depends on rather than how it fetches. Anonymous inline logic inside useEffect cannot be tested without rendering the component.
  7. Add accessibility and test coverage before calling the component doneInteractive elements need aria labels, keyboard handlers, and focus management. A React Testing Library test that renders, acts, and asserts is 80% of the value at 20% of the cost of E2E.
  8. Memoize only what profiling proves is slow - and skip it entirely in React Compiler projectsReact.memo, useMemo, and useCallback add code weight and maintenance cost. The React Compiler (stable in React 19) auto-memoizes and makes hand-memoization redundant. In non-compiler projects, open DevTools Profiler, find the actual slow commit, then memoize the specific component. Never preemptively.

Output Format

Use this structure for TypeScript projects. For JavaScript, drop the interface and type annotations - keep the section ordering. Early returns for loading and error states legitimately sit between hooks and render; that is not a violation.

// [ComponentName].tsx
// Named export - anonymous default exports break Fast Refresh
export interface [ComponentName]Props {
  requiredProp: string;
  optionalProp?: number;        // default handled in destructure
}

export const [ComponentName] = ({
  requiredProp,
  optionalProp = 0,
}: [ComponentName]Props) => {
  // 1. Context / environment reads  (useContext, useTheme…)
  // 2. Server state                 (useQuery, useSWR, use())
  // 3. Local state                  (useState, useReducer, useActionState)
  // 4. Derived values               (useMemo - only if genuinely expensive)
  // 5. Effects                      (useEffect with cleanup)
  // 6. Handlers                     (useCallback - only for memoized children)
  // --- early returns for loading / error sit here ---
  // 7. Render
  return ( ... );
};

Examples

Happy path - data-fetching with TanStack Query

Input

Build a UserProfile component that fetches /api/users/:id and shows a skeleton while loading.

Output

const { data, isPending, isError, refetch } = useQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), }); if (isPending) return <Skeleton />; if (isError) return <ErrorBanner onRetry={refetch} />; return <UserProfile user={data} />; // Zero useEffect. Zero useState. Cache, dedup, and retry are free.

Failure mode - stale closure in a setInterval effect

Input

The interval reads count but always logs 0 no matter how many times the button is clicked.

Output

// Best fix: functional updater removes the dependency entirely setCount(c => c + 1); // If you need the current value for logic, not just incrementing: // Add count to deps - but the interval tears down and recreates on every tick useEffect(() => { const id = setInterval(() => console.log(count), 1000); return () => clearInterval(id); }, [count]); // Alternative: keep the ref current without re-running the effect const countRef = useRef(count); useEffect(() => { countRef.current = count; }); // then read countRef.current inside the interval

Failure mode - fetch race condition

Input

When the user types quickly into a search box, results from an earlier query sometimes overwrite results from the latest one.

Output

useEffect(() => { let ignore = false; // cleanup flag fetchResults(query).then(data => { if (!ignore) setResults(data); // drop stale responses }); return () => { ignore = true; }; // fires before next effect run }, [query]); // With TanStack Query this is handled automatically via query cancellation.

Failure mode - object reference in dependency array

Input

useEffect runs on every render even though the options prop looks the same every time.

Output

// Objects are compared by reference - { page: 1 } !== { page: 1 } // Fix A: destructure the primitives you actually need const { page, limit } = options; useEffect(() => { fetch(page, limit); }, [page, limit]); // Fix B: memoize the object upstream with useMemo const stableOptions = useMemo(() => options, [options.page, options.limit]);

Edge Cases & Gotchas

  • StrictMode double-invokes effects and state initializers in development to surface missing cleanup - don't treat double console logs or double fetches as bugs; add a cleanup function.
  • The key prop must be a stable, unique identifier - not array index. Index as key causes silent reconciliation failures when items are reordered, inserted, or deleted mid-list.
  • Controlled vs. uncontrolled input flipping: passing value={undefined} on the first render then a string later switches the input from uncontrolled to controlled mid-lifecycle, producing a React warning and unpredictable behavior. Always initialize string fields with '' not undefined.
  • Context re-renders every consumer when the context value changes by reference. A new object literal as the value triggers every useContext subscriber every render. Memoize the value object with useMemo.
  • Concurrent rendering may invoke a component's render function multiple times before committing. Never trigger mutations, network requests, or subscriptions directly in the render body - only in effects or event handlers.
  • Hook identity depends on call order. Hooks inside conditionals or loops violate the Rules of Hooks and produce runtime errors in development - they silently corrupt state in production builds without StrictMode.

When to Break These Rules

  • Error boundaries require a class component - React has no hook-based API for componentDidCatch. Use react-error-boundary rather than hand-rolling one; it adds retry, fallback render props, and onError logging with minimal boilerplate.
  • Skip TanStack Query for one-off mutations triggered by explicit user actions. A plain async function inside an onClick with local isPending state is simpler and correct; useMutation is worth it only when you need optimistic updates or cache invalidation.
  • Named default exports (export default function MyComponent) work fine with Fast Refresh. The rule that matters: avoid anonymous defaults (export default () => {…}) - those lose their name and break DevTools and Fast Refresh.
  • In React Compiler-enabled projects (React 19+), remove all hand-written React.memo, useMemo, and useCallback - the compiler makes them redundant and the dead code misleads future readers.
  • Lift state to a global store (Zustand, Redux Toolkit) when the same state is consumed by five or more unrelated components, or when the component tree between producer and consumer is three or more levels deep and prop drilling becomes unmanageable.

Reference Files

  • references/hooks-deep-dive.md - read when the task involves complex useEffect cleanup, dependency array debugging, or custom hook design
  • references/react-19.md - read when the task involves use(), useActionState, useOptimistic, useFormStatus, ref-as-prop (forwardRef replacement), or the React Compiler
  • references/state-management.md - read when the task involves choosing between useState, useReducer, Zustand, Jotai, or Redux Toolkit
  • references/performance.md - read when the task involves render profiling, virtualization (TanStack Virtual), or non-compiler memoization
  • references/testing.md - read when the task involves React Testing Library, act(), mocking hooks, or testing async state transitions
  • references/nextjs.md - read when the task involves Server Components, the App Router, Server Actions, or streaming SSR

More skills in this library

Browse all technical skills - Mobile, AI/ML, Web, Backend, Design, and Product.