Components, hooks, state, and patterns.
3 topics
React uses JSX, a syntax extension that lets you write HTML-like markup inside JavaScript. JSX is not HTML — it compiles to React.createElement() calls and has subtle differences (className instead of class, htmlFor instead of for, camelCase attributes). Components are the building blocks of React — reusable, self-contained pieces of UI. Function components accept a single props object and return JSX. Props are read-only (one-way data flow from parent to child) and can be any JavaScript value including functions and other components. The children prop enables composition — wrapping components around other content. Master: conditional rendering (ternary, && short-circuit), rendering lists with map() and key props (keys help React identify which items changed), fragments (<></>) for grouping without extra DOM elements, and component composition over inheritance.
2 resources
State is data that changes over time and triggers re-renders when updated. useState is the fundamental hook: const [count, setCount] = useState(0). When you call setCount, React schedules a re-render with the new value. State updates are batched and asynchronous — use the function form setCount(prev => prev + 1) when the new value depends on the previous one. For complex state logic, useReducer provides a Redux-like pattern with actions and a reducer function. Events in React use camelCase naming (onClick, onChange, onSubmit) and receive synthetic events (cross-browser wrappers around native events). Controlled components store form input values in state (value + onChange), giving React full control. Understand lifting state up — when two components need to share state, move it to their closest common ancestor and pass it down as props.
useEffect lets you synchronize your component with external systems — things outside React's rendering, like fetching data, setting up subscriptions, manipulating the DOM directly, or starting timers. The basic pattern is useEffect(() => { /* setup */ return () => { /* cleanup */ }; }, [dependencies]). The dependency array controls when the effect runs: empty array ([]) runs once on mount, specific values run when those values change, and no array runs after every render (rarely wanted). Cleanup functions run before the effect re-runs and when the component unmounts — essential for preventing memory leaks from subscriptions, timers, and event listeners. Common pitfalls include missing dependencies (stale closures), infinite loops (effect updates a dependency), and unnecessary effects (computing derived state). The React team recommends you might not need an effect — derive values during render when possible, and handle events in event handlers, not effects.
useMemo and useCallback are optimization hooks that memoize values and functions to prevent unnecessary recalculations and re-renders. useMemo(() => expensiveComputation(a, b), [a, b]) caches the result of a computation and only recalculates when dependencies change — useful for expensive sorting, filtering, or data transformation. useCallback((args) => fn(args), [deps]) caches a function reference — useful when passing callbacks to memoized child components (React.memo) or as dependencies of other hooks. Important: do not memoize everything — memoization itself has a cost (memory, comparison overhead). Only use these hooks when you have measurable performance issues. Profile first with React DevTools Profiler to identify which components re-render unnecessarily, then apply memoization strategically. React Compiler (React 19+) aims to make manual memoization unnecessary.
useRef creates a mutable reference that persists across re-renders without causing them. It has two primary uses: (1) Accessing DOM elements directly — attach a ref to a JSX element with ref={myRef}, then access the DOM node via myRef.current. This is essential for focusing inputs, measuring element dimensions, integrating with third-party DOM libraries, and controlling video/audio playback. (2) Storing mutable values that should not trigger re-renders — previous state values, timer IDs, interval references, and any value you want to persist between renders without causing updates. Unlike state, mutating ref.current does not cause a re-render. useRef(initialValue) returns { current: initialValue } — the same object is returned on every render. Use forwardRef when a parent component needs to access a child's DOM node, and useImperativeHandle to customize what the ref exposes.
Custom hooks let you extract and reuse stateful logic between components. A custom hook is simply a function that starts with "use" and can call other hooks. Examples: useLocalStorage (state synced with localStorage), useDebounce (delay value updates for search inputs), useFetch (data fetching with loading/error states), useMediaQuery (responsive breakpoints), useOnClickOutside (detect clicks outside a ref), and useIntersectionObserver (lazy loading, infinite scroll). Custom hooks compose — you can build complex hooks from simpler ones. They share logic, not state — each component using the same custom hook gets its own independent state. Rules of hooks apply: only call hooks at the top level (not inside loops, conditions, or nested functions) and only call them from React functions. Well-designed custom hooks make your components cleaner by separating what (UI) from how (behavior).
Data fetching connects your React frontend to backend APIs. The simplest approach uses fetch inside useEffect, but managing loading, error, caching, and revalidation states manually is tedious. TanStack Query (React Query) is the industry standard — it provides: automatic caching and deduplication (multiple components requesting the same data share one network call), background refetching (stale-while-revalidate), optimistic updates for instant UI feedback, infinite scroll pagination, and DevTools for debugging cache state. SWR by Vercel is a lighter alternative with similar features. In Next.js App Router, you can fetch data directly in Server Components without any client-side library — the data is fetched on the server and streamed to the client as HTML. For real-time data, consider WebSockets or Server-Sent Events. The key is choosing the right tool: Server Components for initial data, TanStack Query for client-side interactions.
Routing maps URLs to components, enabling multi-page experiences in single-page applications. React Router is the most popular client-side routing library — it supports nested routes, route parameters (/users/:id), layout routes (shared layouts like sidebars and headers), protected routes (redirect unauthenticated users), lazy loading routes (code splitting per page), and programmatic navigation. React Router v6 uses a hooks-based API: useNavigate for navigation, useParams for URL parameters, useSearchParams for query strings, and useLocation for the current URL. If you are using Next.js, routing is built into the framework via the file system — each file in the app directory becomes a route, with folders for nesting, [dynamic] segments for parameters, and special files (layout.tsx, loading.tsx, error.tsx) for shared UI. Most production React apps use Next.js routing.
Forms are one of the most complex and common patterns in React. React Hook Form (RHF) is the industry standard for performant forms — it minimizes re-renders by using uncontrolled components internally while providing a controlled API. Key features: register() connects inputs to the form, handleSubmit() validates and processes submission, formState provides errors and dirty/touched states, useFieldArray manages dynamic lists of fields, and watch() observes field values reactively. Pair RHF with Zod for schema-based validation — define a Zod schema (z.object({ email: z.string().email(), password: z.string().min(8) })), then use zodResolver to connect it to your form. This gives you runtime validation, TypeScript types, and error messages from a single source of truth. Handle server-side validation errors by mapping API errors back to form fields with setError(). For simpler forms, useState with native HTML validation attributes may suffice.
Compound components are a pattern where related components work together to form a complete UI while giving consumers full control over rendering. Think of HTML's <select> and <option> — they are meaningless alone but powerful together. In React, compound components share implicit state through Context. Example: a <Tabs> component with <Tab> and <TabPanel> children — the parent manages which tab is active, and children read from context to show/hide themselves. This pattern provides a flexible, declarative API: consumers arrange children however they want, add custom markup between them, and conditionally render pieces. Other examples include: <Accordion>/<AccordionItem>, <Dropdown>/<DropdownItem>, and <Dialog>/<DialogTrigger>/<DialogContent>. Libraries like Radix UI and Headless UI use compound components extensively. The key advantage over prop-based APIs is flexibility — you do not need to anticipate every use case upfront.
Render props and Higher-Order Components (HOCs) are older patterns for sharing logic between components, largely replaced by custom hooks but still important to understand. A render prop is a function prop that a component calls to determine what to render — it inverts control, letting the parent decide the UI while the child provides the data/behavior. Example: <Mouse render={({ x, y }) => <Cursor position={{ x, y }} />} />. HOCs are functions that take a component and return an enhanced component: const EnhancedComponent = withAuth(MyComponent). They add capabilities (authentication checks, data fetching, logging) without modifying the original component. Common HOCs include connect() from Redux and withRouter from React Router. While custom hooks are now preferred for sharing stateful logic (simpler, no wrapper hell, better TypeScript support), you will encounter render props and HOCs in legacy codebases and libraries like Formik and React Motion.
React performance optimization starts with understanding what causes re-renders: state changes, parent re-renders, and context value changes. React DevTools Profiler shows which components render and how long they take. Optimization techniques: React.memo wraps a component to skip re-renders when props have not changed (shallow comparison). useMemo caches expensive computations, and useCallback caches function references passed as props. Code splitting with React.lazy and Suspense loads components on demand, reducing initial bundle size. For long lists (hundreds/thousands of items), use virtualization libraries like TanStack Virtual or react-window — they only render visible items, dramatically reducing DOM nodes. Image optimization with next/image (lazy loading, proper sizing, modern formats) improves LCP. Avoid common anti-patterns: creating objects/arrays in JSX props (new reference every render), inline function definitions in frequently re-rendering parents, and excessive context providers that trigger unnecessary updates.