⚛️

Top 81 React.js Interview Questions

Top 100 React.js interview questions covering hooks, state management, performance optimization, component patterns, and modern React best practices.

81 Questions
Filter:

React is an open-source JavaScript library for building user interfaces, developed and maintained by Meta (formerly Facebook) and a large community of contributors. First released in 2013, React introduced a component-based architecture where UIs are broken into small, reusable pieces called components. React's core innovation is the Virtual DOM — an in-memory representation of the real DOM that React uses to compute the minimal set of changes needed, then batch-updates the actual DOM efficiently. React follows a declarative programming model: you describe what the UI should look like based on state, and React takes care of updating the DOM to match. React is unopinionated about routing, state management, and data fetching — it is a view library, not a full framework. It is used to build single-page applications (SPAs), mobile apps via React Native, and server-rendered apps via Next.js.

Beginner

JSX (JavaScript XML) is a syntax extension for JavaScript that lets you write HTML-like markup directly inside JavaScript code. JSX is not valid JavaScript — it must be transformed by a compiler (Babel or the new React compiler) into regular JavaScript function calls. For example, <h1>Hello, {name}!</h1> compiles to React.createElement("h1", null, "Hello, ", name, "!"). Key JSX rules: every element must be closed (self-closing tags like <img />); use className instead of class and htmlFor instead of for; JavaScript expressions go inside { }; components must start with an uppercase letter to distinguish them from HTML elements; adjacent JSX elements must be wrapped in a single parent (or a Fragment). JSX is optional — you can use React without it — but it is universally adopted because it makes component markup far more readable and maintainable.

Beginner

The Virtual DOM is a lightweight JavaScript object tree that is an in-memory copy of the real DOM. When your React component's state or props change: (1) React creates a new Virtual DOM tree representing the updated UI. (2) React diffs this new tree against the previous Virtual DOM using a highly optimized reconciliation algorithm. (3) React computes the minimal set of changes (a "patch"). (4) React applies only those changes to the real DOM in a single batch. Why this matters: direct DOM manipulation is expensive because the browser has to recalculate styles, reflow layout, and repaint. By batching updates and minimizing real DOM operations, React avoids unnecessary reflows and repaints, making updates significantly faster. The Virtual DOM itself is a performance optimization strategy — not a feature unique to React (Vue also uses it). React 18 introduced concurrent rendering where React can interrupt and resume rendering work, further improving perceived performance.

Beginner

A React component is a reusable, self-contained piece of UI. Components are the fundamental building blocks of React applications — you compose them together to build complex interfaces. Modern React uses function components: plain JavaScript functions that accept props as an argument and return JSX: function Welcome({ name }) { return <h1>Hello, {name}!</h1>; }. Class components (legacy): extend React.Component and implement a render() method. Components must start with an uppercase letter so React knows to treat them as components rather than plain HTML elements. Components can contain other components (composition), manage their own state with hooks, and receive data from parents via props. The component tree forms the structure of your application. There is no hard rule for how large a component should be — generally, a component should do one thing well. Components promote code reuse, separation of concerns, and easier testing.

Beginner

Props (short for properties) are the mechanism for passing data from a parent component to a child component. Props are read-only — a component should never modify its own props. Example: <Button color="blue" onClick={handleClick} label="Submit" />. Inside the Button component, props.color, props.onClick, and props.label are accessible. With destructuring: function Button({ color, onClick, label }) { ... }. Any JavaScript value can be passed as a prop: strings, numbers, booleans, arrays, objects, functions, and even other React elements. Default props: function Button({ color = "blue" }) { ... }. Prop spreading: <Component {...props} /> passes all properties of an object as props. Children prop: content between component tags is passed as props.children. Props flow unidirectionally — from parent to child (top-down) — which makes data flow predictable and easier to debug. To share data from child to parent, pass a callback function as a prop.

Beginner

State is data that a component manages internally and that can change over time. When state changes, React re-renders the component (and its children) to reflect the updated UI. Unlike props (which come from outside), state is owned and managed by the component itself. In function components, state is created with the useState hook: const [count, setCount] = useState(0);. count is the current state value; setCount is the updater function. Call setCount(newValue) to update the state — React queues a re-render. Important rules: (1) Never mutate state directly — always use the setter function. (2) State updates may be batched — React 18 batches all updates by default. (3) State updates are asynchronous — reading state right after calling the setter gives the old value. (4) For state based on the previous value, use the functional update form: setCount(prev => prev + 1). State should be the minimum data needed to represent the UI — derive everything else from state and props.

Beginner

useState is a React hook that adds state to a function component. const [state, setState] = useState(initialValue); returns a pair: the current state value and a function to update it. The initial value is used only on the first render. Updating state: setState(newValue) — React schedules a re-render with the new state. Functional updates: when the new state depends on the old state, use setState(prev => prev + 1) — this guarantees you get the latest value even if updates are batched. Object/array state: you must create new objects/arrays instead of mutating — setUser({ ...user, name: "Alice" }), not user.name = "Alice". Lazy initialization: pass a function to useState to compute the initial value only once (useful for expensive calculations): useState(() => computeExpensiveValue()). Multiple useState calls are independent. State is preserved across re-renders and reset only when the component unmounts or its key changes.

Beginner

useEffect is a React hook for running side effects in function components — operations that interact with systems outside React (network requests, subscriptions, timers, direct DOM manipulation, logging). Signature: useEffect(setup, dependencies?). When it runs: after every render (no dependency array); once after the first render (empty array []); when specific values change ([dep1, dep2]). Cleanup: return a function from the effect to clean up — called before the next effect runs and when the component unmounts: useEffect(() => { const sub = subscribe(id); return () => sub.unsubscribe(); }, [id]);. Common mistakes: missing dependencies (stale closure), putting functions in the dependency array that are recreated every render, running effects without cleanup (memory leaks). In React 18 with Strict Mode, effects run twice in development to surface cleanup bugs. Note: useEffect is being gradually supplemented by purpose-built hooks like useSyncExternalStore for subscriptions and use() for async data in future React versions.

Beginner

In React, form elements are classified as controlled or uncontrolled based on how their value is managed. Controlled component: React controls the form element's value — it is stored in component state, and every change triggers a state update. The DOM input is always in sync with React state. const [value, setValue] = useState(""); <input value={value} onChange={e => setValue(e.target.value)} />. You have full control: validation, formatting, conditional disabling. This is the recommended approach. Uncontrolled component: the DOM manages its own state — React does not track the value. You access it via a ref when needed: const ref = useRef(); <input ref={ref} />, then ref.current.value. Useful for: file inputs (which cannot be controlled), integrating with non-React code, or when performance of large forms is critical. Rule of thumb: use controlled components by default — they give you a single source of truth and make form state predictable and testable. Use uncontrolled only when necessary (file inputs, third-party DOM libraries).

Beginner

useRef returns a mutable ref object whose .current property persists across renders without causing re-renders when changed. Two primary uses: (1) Accessing DOM elements: const inputRef = useRef(null); <input ref={inputRef} /> — after mounting, inputRef.current is the DOM input element. Use this to imperatively focus, scroll, or measure elements: inputRef.current.focus(). (2) Storing mutable values that persist across renders but do not trigger re-renders — unlike state. Use cases: storing previous state values, interval/timeout IDs for cleanup, debounce timers, tracking if a component is mounted. Example: const countRef = useRef(0); countRef.current++; — this increments but does NOT cause a re-render. Key difference from state: changing ref.current does not trigger a re-render; changing state does. Do not read or write refs during rendering — refs are for imperative operations. useRef(initialValue) only uses the initial value on the first render.

Beginner

useContext is a React hook that lets a component read and subscribe to a React Context. Context provides a way to share values (like themes, user data, or locale) across the component tree without passing props through every level ("prop drilling"). Setup: (1) Create context: const ThemeContext = createContext("light");. (2) Provide a value: <ThemeContext.Provider value="dark"><App /></ThemeContext.Provider>. (3) Consume anywhere in the tree: const theme = useContext(ThemeContext);. When the context value changes, all components using that context re-render. This is the main performance concern — if the context value is an object created inline (value={{ user, logout }}), it is a new object every render and causes all consumers to re-render. Fix by memoizing the value or splitting into multiple contexts. Use context for: authentication, themes, localization, and other "ambient" data. For frequently changing data (like a counter), state or external libraries (Zustand, Redux) are better.

Beginner

Prop drilling occurs when you pass props down through multiple levels of components just to get data to a deeply nested component — intermediate components receive and pass props they don't actually use. Example: App → Header → Nav → UserMenu where only UserMenu needs the user data, but it passes through Header and Nav unnecessarily. This makes code brittle (every intermediate component must pass the prop), harder to refactor (changing the prop name requires touching every level), and clutters component signatures. Solutions: (1) React Context — provide data at a high level, consume anywhere without passing through intermediaries. (2) Component composition — pass components as children or props instead of data: <Header userMenu={<UserMenu user={user} />} />. (3) State management libraries (Redux, Zustand, Jotai) — store state globally, access from any component. (4) Custom hooks — encapsulate context access. Component composition is often underused and can eliminate prop drilling without introducing context complexity.

Beginner

Keys are special props that help React identify which items in a list have changed, been added, or removed. When rendering a list of elements, React uses keys to match children across renders. Without keys (or with index as key), React uses element position — reordering the list causes React to think all elements changed, resulting in wasteful re-renders and, for stateful components, incorrect state preservation. items.map(item => <Item key={item.id} {...item} />). Key rules: keys must be unique among siblings (not globally); keys must be stable (not change between renders); keys must be predictable. Why not use array index as key: if the list can be reordered, filtered, or items can be deleted, using index means keys change — React destroys and recreates components instead of reordering them. Use a stable, unique identifier from your data (like a database ID). Keys are NOT accessible as props inside the component — they are a hint to React's reconciliation algorithm, not a data prop. If you need the ID value, pass it as a separate prop.

Beginner

Reconciliation is the process React uses to determine what changes need to be made to the DOM when a component's state or props change. React compares the new Virtual DOM tree with the previous one (diffing) and computes the minimal set of changes. The algorithm has two main assumptions that make it O(n) instead of O(n³) like a general tree diff: (1) Different types produce different trees: if an element changes from <div> to <span>, React tears down the entire subtree and builds a new one. (2) Keys identify stable elements across renders: elements with the same key in the same position are assumed to be the same component instance. Same type: React updates the existing DOM element's attributes/properties instead of replacing it. For class components, the instance is preserved; componentDidUpdate is called. For function components, the function is re-called. The reconciler in React 18 is called Fiber — it breaks rendering work into small units that can be interrupted and resumed, enabling concurrent features like transitions and Suspense.

Beginner

React Fragments let you return multiple elements from a component without adding an extra wrapping DOM node. Without fragments, you must wrap multiple siblings in a <div>, which adds unnecessary DOM nodes and can break CSS layouts (like flexbox or CSS grid where extra wrappers cause issues). Syntax: <React.Fragment><Child1 /><Child2 /></React.Fragment> or the shorthand <><Child1 /><Child2 /></>. The shorthand cannot accept any props; the long form accepts a key prop (useful when mapping over a list of fragments). Fragments render nothing to the DOM — the children appear as direct children of the parent in the actual DOM tree. Common use case: table rows (<tr> cannot have a wrapping <div> as a child), definition lists, CSS grid items, or any case where a semantic wrapper element would be incorrect or disruptive. Fragments make your component output cleaner and HTML-compliant.

Beginner

Conditional rendering in React means rendering different UI based on conditions. Several patterns: (1) if/else statements: use in the component body before the return: if (isLoading) return <Spinner />; return <Content />;. (2) Ternary operator: inline in JSX — {isLoggedIn ? <Dashboard /> : <Login />}. (3) Logical AND: render something or nothing — {error && <ErrorMessage error={error} />}. Caution: {count && <Counter />} renders 0 when count is 0 because 0 is falsy but still rendered. Use {count > 0 && ...} or ternary instead. (4) Nullish patterns: return null from a component to render nothing. (5) IIFE or helper functions: for complex logic. (6) Switch statements: for multiple conditions. The ternary operator is the most common pattern for two-branch conditions; logical AND for optional rendering. Keep component JSX readable by extracting complex conditions into variables or helper functions.

Beginner

State and props are both JavaScript objects that hold information influencing the rendered output, but they serve different purposes. Props: passed FROM parent TO child; read-only inside the receiving component; the component cannot modify its own props; changes come from the parent re-rendering with new props. State: managed WITHIN the component; can be changed by the component itself via the state setter; triggers a re-render when changed; persists across renders (unlike local variables). Analogy: think of a React component as a function — props are the function's parameters (input from outside), state is the function's local variables (internal data). Decision guide: if the data is passed in from a parent, use props; if the data is owned by this component and can change over time, use state. Never modify props — this is a React rule. State changes are what drive UI updates. Start with props and only introduce state when you need to track something that changes and is not derivable from props.

Beginner

Lifting state up is the pattern of moving shared state to the closest common ancestor of the components that need it, then passing it down via props. When two sibling components need to share and synchronize state, neither can access the other's state directly. Instead: (1) Remove the state from both child components. (2) Add it to their common parent. (3) Pass the state and state-setter functions down as props. Example: a temperature converter where Celsius and Fahrenheit inputs must stay in sync — lift the temperature value into the parent that contains both inputs. The parent owns the truth; both children are controlled by it. This is React's single-source-of-truth principle — state lives in one place, and all dependent components derive their values from it. Lifting state can cause more prop drilling — if it becomes excessive, consider Context or an external state manager. Always start by lifting to the closest common ancestor, not immediately jumping to global state.

Beginner

useReducer is an alternative to useState for managing complex state logic. It is inspired by Redux and follows the reducer pattern. Signature: const [state, dispatch] = useReducer(reducer, initialState);. The reducer function takes the current state and an action, and returns the new state: function reducer(state, action) { switch (action.type) { case "increment": return { ...state, count: state.count + 1 }; } }. Dispatch an action: dispatch({ type: "increment" }). When to use useReducer over useState: (1) Complex state with multiple sub-values where updates depend on each other. (2) Multiple state transitions that share logic. (3) State that involves many different "actions." (4) When the next state depends on the previous state in complex ways. Advantages: the reducer is a pure function — easy to test in isolation; all state transitions are explicit and traceable; makes it easier to add logging, undo/redo. useReducer pairs well with useContext to create a Redux-like global state without external libraries.

Beginner

useMemo memoizes the result of a computation, recalculating it only when its dependencies change. Syntax: const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);. On subsequent renders, if a and b have not changed, React returns the cached result without re-running the function. When to use: (1) Expensive calculations (complex array transformations, sorting large lists, heavy math). (2) Referential stability — when you need the same object/array reference across renders to prevent child re-renders or satisfy useEffect dependencies: const options = useMemo(() => ({ limit: 100, filter }), [filter]);. When NOT to use: For simple computations (adding two numbers, string formatting) — the overhead of useMemo exceeds the computation cost. Common mistake: using useMemo for everything (premature optimization). Only memoize when there is a measurable performance problem. React may also discard the memoized value in the future (e.g., for memory pressure) — do not rely on it for correctness, only for performance.

Beginner

useCallback returns a memoized version of a callback function — the same function reference is returned on subsequent renders unless the dependencies change. Syntax: const memoizedFn = useCallback(() => doSomething(a, b), [a, b]);. Why it matters: in JavaScript, every function defined inside a component is a new function reference on every render. When you pass a callback to a child component, the child receives a new reference each time, causing it to re-render even if nothing meaningful changed. With useCallback, the function reference stays stable as long as dependencies do not change — preventing unnecessary child re-renders when combined with React.memo. Key relationship: useCallback(fn, deps) is equivalent to useMemo(() => fn, deps). When to use: (1) Passing callbacks to memoized child components (React.memo). (2) Dependencies in useEffect that should not re-run just because a function was recreated. (3) Event handlers passed to frequently re-rendering children. Overusing useCallback is a performance anti-pattern — it adds overhead and memory usage.

Beginner

React.memo is a higher-order component that memoizes a function component, preventing re-renders when its props have not changed. Wrap a component: const MemoizedChild = React.memo(ChildComponent);. Now if the parent re-renders but passes the same props (by shallow comparison), MemoizedChild does not re-render. Shallow comparison: React.memo compares props with Object.is() for primitives (correct) but for objects and arrays, it compares references — if a parent creates a new object literal each render, memo does nothing because it is a new reference. Fix: use useMemo or useCallback to stabilize object/function props. Custom comparison: pass a comparison function as the second argument: React.memo(Component, (prevProps, nextProps) => prevProps.id === nextProps.id). When to use: components that render often with the same props (list items, icons, static UI sections). When to skip: components that almost always receive different props, components that are cheap to render, or components near the top of the tree.

Beginner

React lifecycle methods are special methods in class components that run at specific points in a component's life. Mounting (component is added to the DOM): constructor()render()componentDidMount() (ideal for data fetching, subscriptions). Updating (state or props change): render()componentDidUpdate(prevProps, prevState) (run after updates, compare with prev to avoid infinite loops). Unmounting (component removed): componentWillUnmount() (cleanup: cancel requests, unsubscribe). Error handling: componentDidCatch(error, info) and static getDerivedStateFromError() — only available in class components. Function component equivalents with hooks: componentDidMountuseEffect(() => { ... }, []); componentDidUpdateuseEffect(() => { ... }, [deps]); componentWillUnmount → cleanup returned from useEffect. Modern React prefers function components — class components are considered legacy but are not deprecated.

Beginner

An error boundary is a React class component that catches JavaScript errors anywhere in its child component tree and displays a fallback UI instead of crashing the entire application. Without error boundaries, a runtime error in any component crashes the whole React app. Implement by defining static getDerivedStateFromError(error) (to trigger the fallback render) and/or componentDidCatch(error, info) (to log errors). Example: class ErrorBoundary extends React.Component { state = { hasError: false }; static getDerivedStateFromError() { return { hasError: true }; } render() { return this.state.hasError ? <FallbackUI /> : this.props.children; } }. Usage: <ErrorBoundary><MyComponent /></ErrorBoundary>. Limitations: error boundaries do NOT catch: errors in event handlers (use try/catch), asynchronous errors (setTimeout, Promises), errors in the error boundary itself, and errors during server-side rendering. Note: there is no function-component equivalent — error boundaries must be class components. Libraries like react-error-boundary provide a ready-made wrapper.

Beginner

Class components extend React.Component, use a render() method, manage state with this.state and this.setState(), use lifecycle methods, and require understanding of this. Function components are plain JavaScript functions that return JSX, manage state with hooks (useState), handle side effects with useEffect, and avoid this entirely. Why function components are preferred today: (1) Less boilerplate — no constructor, no binding event handlers. (2) Hooks are more composable — custom hooks allow sharing logic in ways mixins/HOCs could not. (3) Smaller bundle size — function components compile to less code. (4) Easier to read and test. (5) Better TypeScript integration. (6) React team focuses new features (Concurrent Mode, Server Components) on hooks. When class components are still needed: error boundaries (no hook equivalent yet), codebases that have not migrated. Class components are NOT deprecated — existing code will continue to work. But for all new development, function components with hooks are the standard.

Beginner

The children prop is a special prop automatically passed to every React component — it represents the content placed between a component's opening and closing tags. Example: <Card><p>Content here</p></Card> — inside Card, props.children is the <p> element. Children can be: a single element, a string, an array of elements, other components, or nothing (undefined). Access: function Card({ children }) { return <div className="card">{children}</div>; }. React.Children utilities: React.Children.map(children, fn), React.Children.count(children), React.Children.toArray(children) for safely working with children. The children pattern is React's most powerful composition tool — it lets you build generic containers (Modal, Card, Layout, Tooltip) without knowing what will be rendered inside them. Render props extend this: children can be a function: <DataProvider>{data => <Table data={data} />}</DataProvider>.

Beginner

React Strict Mode is a development-mode tool that highlights potential problems in a React application. It renders components twice (in development) to detect side effects, and helps identify unsafe lifecycle usage, legacy API usage, and unexpected side effects. Enable it by wrapping your app: <React.StrictMode><App /></React.StrictMode>. What it does: (1) Renders components twice in development to detect impure rendering (unexpected side effects during render). (2) Runs effects twice in development to surface missing cleanup. (3) Warns about deprecated APIs (findDOMNode, legacy context, string refs). (4) Warns about using componentWillMount, componentWillReceiveProps, componentWillUpdate. Important: Strict Mode has NO effect in production — it only adds checks in development. The double-invocation behavior (render twice, effect twice) is intentional and can surprise developers who are not aware of it. If your app breaks in Strict Mode, it reveals a real bug that would occur in production anyway. Always develop with Strict Mode enabled.

Beginner

A Higher-Order Component (HOC) is a function that takes a component and returns a new component with additional props or behavior. It is a pattern for reusing component logic. Convention: HOC names start with "with": function withAuth(WrappedComponent) { return function AuthedComponent(props) { if (!user) return <Redirect to="/login" />; return <WrappedComponent {...props} user={user} />; }; }. Usage: const AuthedDashboard = withAuth(Dashboard);. HOCs were the primary way to share logic before hooks. Common uses: authentication, data fetching, analytics, logging, theming. Problems with HOCs: wrapper hell (deeply nested component trees in DevTools), prop collisions (HOC and wrapped component use same prop name), opaque component origin, harder to debug. Modern alternatives: custom hooks solve most HOC use cases with less complexity. But HOCs remain useful for: adding error boundaries, adding class-component behavior to existing class components, or code-sharing with non-hook-compatible patterns.

Beginner

The render props pattern passes a function as a prop (usually called render or children) that a component calls to know what to render. This allows sharing code between components. Example: class MouseTracker extends React.Component { state = { x: 0, y: 0 }; handleMove = e => this.setState({ x: e.clientX, y: e.clientY }); render() { return <div onMouseMove={this.handleMove}>{this.props.render(this.state)}</div>; } }. Usage: <MouseTracker render={({ x, y }) => <p>{x}, {y}</p>} />. The MouseTracker encapsulates the mouse-tracking logic; the render prop decides what to display with that data. Children as a function: same pattern using children: <MouseTracker>{({ x, y }) => <p>{x}, {y}</p>}</MouseTracker>. Render props vs HOCs vs Hooks: hooks have largely replaced both patterns for sharing stateful logic — they are simpler, less nesting, and more composable. Render props remain useful for cases that require rendering flexibility based on encapsulated logic.

Beginner

The Context API provides a way to share values across the component tree without explicitly passing props through every level. It is designed for "global" data that many components need: current authenticated user, theme, language preference. Creating context: const UserContext = React.createContext(defaultValue);. The default value is used when a component consumes the context but is not inside a Provider. Providing context: <UserContext.Provider value={{ user, logout }}><App /></UserContext.Provider>. Consuming context: const { user, logout } = useContext(UserContext);. Any component inside the Provider that calls useContext(UserContext) re-renders when the context value changes. Context is NOT a state manager — it is a dependency injection mechanism. It does not replace Redux for complex state; it eliminates prop drilling. Context is best for infrequently changing values. For frequently changing values, use it with a reducer or an optimized state library. Multiple contexts can be nested for different concerns (theme, user, locale).

Beginner

Lazy loading in React defers loading a component's code until it is actually needed, reducing the initial bundle size and improving page load time. React.lazy() dynamically imports a component: const LazyComponent = React.lazy(() => import("./LazyComponent"));. The component must be a default export. Use it with <Suspense> to show a fallback while the component loads: <Suspense fallback={<Spinner />}><LazyComponent /></Suspense>. Code splitting: bundlers (webpack, Vite) create a separate JavaScript "chunk" for lazily imported modules. When React needs the component, it fetches that chunk from the server on demand. Good candidates for lazy loading: route-level components (each page loads its own code), modal dialogs (only load when opened), admin/rarely-visited sections. Not suitable for: components above the fold that must render immediately, tiny components where the overhead of a network request outweighs savings. Works with React Router: lazy-load each route's component for automatic route-based code splitting.

Beginner

React Suspense is a component that lets you declaratively specify what to show while waiting for something to load. Initially (React 16.6), Suspense only worked with React.lazy() for code splitting. React 18 expanded Suspense to work with data fetching through frameworks that implement the Suspense protocol (Next.js App Router, Relay, SWR experimental). Syntax: <Suspense fallback={<LoadingSpinner />}><ComponentThatLoads /></Suspense>. When a component inside Suspense "suspends" (throws a Promise), React renders the fallback instead. When the Promise resolves, React replaces the fallback with the component. Nested Suspense: multiple Suspense boundaries let different parts of the UI show their own loading states independently. React 18 features: startTransition + Suspense enables non-blocking UI updates; use() hook (React 19) integrates Suspense with arbitrary promises. Suspense represents a shift from imperative loading state management (loading booleans, if-else checks) to declarative loading states.

Beginner

In React Strict Mode (development only), React intentionally invokes certain functions twice to help detect side effects in rendering. This includes: render functions, function component bodies, state initializers, and effect setup/cleanup pairs. The purpose is to surface bugs caused by components that are not "pure" — they produce different results on each invocation or have side effects during rendering. Since React's concurrent features may invoke render functions multiple times, your component body must be pure (no side effects during render). Effect double-invocation (React 18): effects also run twice in Strict Mode — mount → effect runs → cleanup runs → effect runs again. This detects effects that do not properly clean up. Example: if you open a WebSocket connection in useEffect but forget to close it in cleanup, you will see two connections in Strict Mode — revealing the bug. The double invocation behavior is ONLY in development and ONLY with Strict Mode. If your component behaves incorrectly with double invocations, it has a real bug that will manifest in production with concurrent rendering.

Beginner

React uses synthetic events — a cross-browser wrapper around the browser's native event system. React event handlers are attached as props using camelCase names: onClick, onChange, onSubmit, onKeyDown. Pass a function reference (not a call): <button onClick={handleClick}> NOT <button onClick={handleClick()}>. The event handler receives a SyntheticEvent object with the same interface as the native event (plus React normalizations for cross-browser consistency). Preventing default: call event.preventDefault() (e.g., preventing form submission). Stopping propagation: event.stopPropagation(). Passing arguments: use an arrow function: onClick={() => handleDelete(item.id)} or bind. Event delegation: React attaches a single event listener to the root element (not each DOM node) and routes events — this is efficient and is how React's synthetic event system works under the hood. React 17+ changed event delegation from document to the React root element for better compatibility with other React trees.

Beginner

React form handling typically uses controlled components — form elements whose values are driven by React state. For each input: (1) Bind its value to state: value={formData.email}. (2) Update state on change: onChange={e => setFormData({...formData, email: e.target.value})}. (3) Handle submission: onSubmit={e => { e.preventDefault(); submitData(formData); }}. Multiple inputs with one handler: use the input's name attribute: const handleChange = e => setFormData(prev => ({ ...prev, [e.target.name]: e.target.value }));. Checkboxes: use checked instead of value. Select: use value on the <select> element, not on <option>. Validation: track errors in state and display conditionally. Libraries: React Hook Form, Formik, and Zod (for validation) are popular for complex forms because they reduce boilerplate, optimize re-renders, and handle validation declaratively. React Hook Form is particularly performant because it uses uncontrolled inputs with refs.

Beginner

React.Component re-renders whenever setState() is called or when its parent re-renders, regardless of whether the props or state actually changed. React.PureComponent implements shouldComponentUpdate() with a shallow comparison of current and next props and state — it only re-renders if something actually changed (by shallow reference). This prevents unnecessary re-renders when parent components re-render but pass the same props. Shallow comparison: primitives are compared by value; objects and arrays by reference. So if you pass a new object { count: 1 } every render (even with the same data), PureComponent still re-renders because it is a new reference. Function component equivalent: React.memo is the function-component equivalent of PureComponent. Caution: PureComponent can produce bugs if you mutate objects directly (the reference does not change, so PureComponent skips the render even though the data changed). Always treat props and state as immutable when using PureComponent. For complex objects, consider using Immer or structural sharing.

Beginner

React Fiber is the complete rewrite of React's reconciliation engine, introduced in React 16. The original Stack reconciler processed the entire component tree synchronously in one pass — if it started, it could not be interrupted. This caused jank for large trees because JavaScript is single-threaded and a long synchronous task blocks the browser from handling user input or animations. Fiber's key innovation: incremental rendering. It breaks rendering work into small units called fibers (one per component) that can be paused, resumed, prioritized, and discarded. This enables: (1) Prioritized updates — user interactions are higher priority than background data fetches. (2) Concurrent rendering (React 18) — React can work on multiple versions of the UI at once, interrupting low-priority work for high-priority updates. (3) Time-slicing — React yields control back to the browser between fiber units to keep the UI responsive. (4) Suspense — pause rendering waiting for data. The Fiber tree mirrors the component tree and stores all the information needed to perform, pause, or retry work.

Beginner

Both provide fallback values for component props, but they work differently. defaultProps: a static property on the component class or function that React merges with the passed props before rendering. Deprecated for function components in React 18.3. MyComponent.defaultProps = { color: "blue", size: 16 };. Pros: works in class components; was the original React way. Cons: the defaults are defined separately from the component signature, making them less visible; deprecated for function components. Default parameter values: standard JavaScript destructuring defaults in the function signature. function Button({ color = "blue", size = 16, onClick }) { ... }. Pros: co-located with props; type inference works better in TypeScript; more idiomatic modern JavaScript; works for function components without any React-specific API. Cons: does not work in class components. Recommendation: always use JavaScript default parameter values for function components — they are cleaner, more visible, and better supported by tooling. Use defaultProps only if maintaining legacy class components.

Beginner

findDOMNode(componentInstance) is a legacy React API that returns the underlying DOM node of a class component instance. It is deprecated because it creates a coupling between a component and its DOM output that should not exist — it breaks encapsulation and is incompatible with React's concurrent rendering. Refs are the correct, modern way to access DOM nodes. With function components: const ref = useRef(); <input ref={ref} />ref.current is the input DOM node. With class components: this.myRef = React.createRef(); <input ref={this.myRef} />. Forwarding refs: when a parent needs a ref to a child component's DOM node, use React.forwardRef: const Input = React.forwardRef((props, ref) => <input ref={ref} {...props} />);. Refs should be used sparingly — only for imperative operations that cannot be done declaratively (focus management, scroll control, animations, integration with non-React libraries). Most UI can and should be built declaratively.

Beginner

Custom hooks are JavaScript functions whose names start with "use" that call other hooks inside them. They allow extracting and reusing stateful logic across multiple components. Example: function useWindowSize() { const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight }); useEffect(() => { const handler = () => setSize({ width: window.innerWidth, height: window.innerHeight }); window.addEventListener("resize", handler); return () => window.removeEventListener("resize", handler); }, []); return size; }. Usage: const { width, height } = useWindowSize();. Why custom hooks are powerful: (1) Extract complex logic from components into testable, reusable functions. (2) Share stateful logic without render props or HOCs (no wrapper hell). (3) Each call to a custom hook has isolated state — two components using the same hook do not share state. (4) They compose naturally — custom hooks can call other custom hooks. Common custom hooks: useFetch, useLocalStorage, useDebounce, useInterval, usePrevious, useMediaQuery. The only rule: custom hook names must start with "use" so React can enforce hook rules.

Intermediate

React enforces two rules of hooks that must be followed to ensure hooks work correctly. Rule 1: Only call hooks at the top level. Do not call hooks inside loops, conditions, or nested functions. They must always be called in the same order on every render. React identifies which state belongs to which hook call by the call order — if you conditionally skip a hook, the order changes and React loses track of which state is which. Wrong: if (condition) { const [state, setState] = useState(); }. Rule 2: Only call hooks from React functions. Call hooks from function components or from custom hooks — not from regular JavaScript functions, class components, or outside React. The eslint-plugin-react-hooks package enforces both rules automatically with linting. These rules exist because React relies on the call order of hooks to associate state with the correct hook between renders. Violating them produces cryptic bugs that are hard to debug. If you need conditional logic around a hook, put the condition inside the hook: const [state, setState] = useState(); if (!condition) return null;.

Intermediate

Both useLayoutEffect and useEffect run after React has committed changes to the DOM. The difference is when. useEffect runs asynchronously after the browser has painted — the user sees the updated UI first, then the effect runs. useLayoutEffect runs synchronously after DOM mutations but before the browser paints — it fires at the same phase as componentDidMount/componentDidUpdate. React waits for useLayoutEffect to finish before letting the browser paint. When to use useLayoutEffect: when you need to read layout information from the DOM (element dimensions, scroll position) and then synchronously update the DOM or state before the browser paints — prevents a visual flash. Example: measuring an element's height to position a tooltip. Caution: useLayoutEffect blocks the browser from painting — if it does heavy work, it creates jank. Prefer useEffect for most cases. useLayoutEffect also causes SSR issues (server has no DOM) — use a guard: typeof window !== "undefined".

Intermediate

useImperativeHandle customizes the ref value that a parent receives when using React.forwardRef. Instead of exposing the entire DOM node or component instance, you can expose only specific methods or values. Syntax: useImperativeHandle(ref, () => ({ focus: () => inputRef.current.focus(), reset: () => setInput("") }), []);. Full example: const FancyInput = React.forwardRef((props, ref) => { const inputRef = useRef(); useImperativeHandle(ref, () => ({ focus: () => inputRef.current.focus() })); return <input ref={inputRef} {...props} />; });. The parent calls fancyInputRef.current.focus() — it only has access to focus(), not the raw DOM node. When to use: building reusable input/form components where the parent needs to trigger actions (focus, submit, reset, scroll to error) without exposing internal DOM details. Keep the exposed API minimal — only expose what is necessary. This is an advanced escape hatch; most cases should use props and callbacks instead.

Intermediate

useDeferredValue (React 18) accepts a value and returns a deferred (possibly stale) version of it. React will initially render with the old deferred value, then re-render with the new value in the background when the browser is idle. This prevents expensive renders from blocking user input. Use case: const [query, setQuery] = useState(""); const deferredQuery = useDeferredValue(query); return <><input onChange={e => setQuery(e.target.value)} /><SearchResults query={deferredQuery} /></>;. The input stays responsive (uses query), while the expensive SearchResults renders with the deferred value, lagging behind if it's slow. Optimization: pair with React.memo. Wrap SearchResults with React.memo so it only re-renders when deferredQuery changes, not when query changes. useDeferredValue vs useTransition: useTransition wraps the state update call; useDeferredValue defers a derived value from a state you do not control (e.g., from props or a third-party hook). Use useDeferredValue when you cannot modify the state update.

Intermediate

useTransition (React 18) marks state updates as non-urgent transitions, allowing React to keep the current UI responsive while preparing the new UI in the background. Syntax: const [isPending, startTransition] = useTransition();. Wrap non-urgent updates: startTransition(() => setPage(newPage));. React can interrupt this transition if a more urgent update (like user typing) comes in, and restart it later. isPending is true while the transition is in progress — use it to show a loading indicator without hiding the current content. Without useTransition: clicking a tab that triggers a slow render makes the UI feel frozen — the expensive render blocks everything. With useTransition: the tab click is instant, the current page stays visible (with an optional spinner), and the new page renders in the background. useTransition vs useDeferredValue: useTransition gives you control over the update dispatch; useDeferredValue defers a value you receive. Use useTransition for navigations, tab switches, and search filtering. startTransition can also be imported and used outside components.

Intermediate

React Server Components (RSC) are a new paradigm (stable in React 19 / Next.js App Router) where components render exclusively on the server and send only the result (serialized React tree) to the client — not JavaScript. Benefits: (1) Zero bundle cost — server component code is never sent to the client. (2) Direct server access — query databases, read files, call backend services directly without API routes. (3) Streaming — server components stream output progressively, enabling instant loading states. (4) Automatic code splitting — only client-interactive parts are sent as JavaScript. Rules for server components: cannot use browser APIs, cannot use hooks, cannot use event listeners, cannot maintain interactive state. Client components (marked with "use client") work like traditional React components — they have state, hooks, and event handlers. Composition: server components can render client components, but client components cannot directly render server components (they can receive them as children). RSC is a fundamental shift in how React apps handle data fetching and rendering.

Intermediate

Both manage state in function components, but they serve different use cases. useState is simpler — best for independent pieces of state: booleans, strings, numbers, simple objects. Each call manages one value. useReducer centralizes state updates into a pure reducer function — best when: (1) State is a complex object with multiple sub-values. (2) State updates depend on each other. (3) There are many different ways to update state (many action types). (4) The next state depends on the previous state in complex ways. (5) You want to extract and test state logic separately. Mental model: useState = simple variable; useReducer = Redux-lite (state + actions + reducer). When to migrate: if you find yourself writing many related useState calls that update together, or complex conditional update logic, refactor to useReducer. Performance: useReducer can be marginally more efficient in some cases because the dispatch function is stable (never recreated). useReducer + useContext: combine for lightweight global state management that avoids Redux for medium-sized apps.

Intermediate

Batching is React's optimization of grouping multiple state updates into a single re-render instead of rendering after each update. React 17 and earlier: batched updates only inside React event handlers. Updates inside setTimeout, Promises, native event handlers, etc. were NOT batched — each setState caused a separate render. React 18: Automatic Batching. All updates are automatically batched, regardless of where they originate — React event handlers, setTimeout, Promise callbacks, native event listeners. Example: setTimeout(() => { setCount(c => c + 1); setFlag(f => !f); }, 1000); — with React 18, this causes one re-render, not two. Opting out: use flushSync() from react-dom to force a synchronous update without batching: flushSync(() => setCount(1)); // renders immediately. Use flushSync when you need the DOM to be updated synchronously before the next line (e.g., reading a DOM measurement after a state change). Batching is a performance optimization — it reduces unnecessary renders and prevents intermediate UI states from being visible.

Intermediate

Concurrent Mode (React 18, enabled by createRoot) is a set of new capabilities that allow React to interrupt rendering work and prioritize more important updates. In legacy mode, rendering was synchronous and uninterruptible — once started, it had to finish. In Concurrent Mode, React can: (1) Interrupt a low-priority render to handle a high-priority update (like user input). (2) Abandon work that is no longer needed (e.g., if the user navigates away). (3) Reuse previously computed work. (4) Prepare multiple UI states simultaneously. Enabling: ReactDOM.createRoot(document.getElementById("root")).render(<App />); — all React 18 apps should use createRoot. New features enabled by Concurrent Mode: useTransition, useDeferredValue, Suspense for data fetching, automatic batching, streaming SSR. Important: components must be pure (no side effects during render) for concurrent rendering to work correctly — the render function may be called multiple times. This is why Strict Mode double-invokes renders — to surface purity violations.

Intermediate

React Portals allow rendering a component's output into a DOM node that is outside the parent component's DOM hierarchy. ReactDOM.createPortal(child, containerDOMNode). Example: a modal dialog that should render at the <body> level to avoid CSS overflow:hidden or z-index issues: function Modal({ children }) { return ReactDOM.createPortal(<div className="modal">{children}</div>, document.body); }. Although the modal's DOM lives outside its parent in the HTML, it still behaves as a React child — events bubble up through the React component tree (not the DOM tree), and context works across portals. Common use cases: modals and dialogs, tooltips and popovers, dropdown menus, notification toasts — any UI that must visually escape its container's stacking context. Accessibility: remember to manage focus when a portal renders an interactive element, trap focus within modals, and handle keyboard events appropriately. Libraries like Radix UI and Headless UI use portals under the hood for these components.

Intermediate

useId (React 18) generates a unique, stable ID that is consistent between the server and client, solving hydration mismatches when IDs are used for accessibility attributes. Problem it solves: using Math.random() or a counter to generate IDs causes SSR hydration errors because the server and client generate different values. const id = useId(); generates something like :r0: — unique within the component, consistent between server and client, and stable across renders. Common use: linking labels and inputs for accessibility: const id = useId(); <label htmlFor={id}>Name:</label><input id={id} />. For a list, call useId once and derive multiple IDs: const id = useId(); const nameId = `\${id}-name`; const emailId = `\${id}-email`;. Do not use useId for keys in lists — keys identify items in a list by data, not by component instance. useId is specifically for HTML attribute connections (id, aria-labelledby, aria-describedby, htmlFor). Multiple calls to useId in the same component return different IDs.

Intermediate

useSyncExternalStore (React 18) is the recommended hook for subscribing to external data stores in a way that is compatible with concurrent rendering. External stores include browser APIs (window.innerWidth, navigator.online), third-party state libraries (Zustand, Redux), and any mutable external value. Signature: const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?);. subscribe: a function that registers a listener and returns an unsubscribe function. getSnapshot: a function that returns the current value (must return the same value if nothing changed). getServerSnapshot: for SSR, returns the server-side snapshot. Example: const isOnline = useSyncExternalStore(window.addEventListener.bind(window, "online"), () => navigator.onLine);. Why not useState + useEffect for subscriptions: in concurrent mode, useEffect fires asynchronously and can miss updates that happen between render and the effect. useSyncExternalStore provides "tear-free" reads — the snapshot is always consistent with what was rendered. Redux Toolkit and Zustand use this hook internally.

Intermediate

React.forwardRef creates a component that forwards the ref it receives to a child DOM element or another component. By default, refs do not pass through function components — <MyInput ref={ref} /> does not give you access to the input DOM node inside MyInput. With forwardRef: const MyInput = React.forwardRef((props, ref) => { return <input ref={ref} {...props} />; });. Now a parent can do: const inputRef = useRef(); <MyInput ref={inputRef} /> and call inputRef.current.focus(). When to use: (1) Building a reusable input/button/select component library where consumers need ref access. (2) Integrating with animation libraries that need DOM references. (3) Focus management in accessible component systems. Combine with useImperativeHandle: to expose a custom API instead of the raw DOM node. In React 19, ref is passed as a regular prop — forwardRef is no longer needed and is being deprecated. Until then, forwardRef is required for any reusable component that needs to accept a ref.

Intermediate

React.createElement(type, props, ...children) creates a new React element from scratch — it is what JSX compiles to. <Button color="blue">Click</Button> becomes React.createElement(Button, { color: "blue" }, "Click"). You rarely call it directly. React.cloneElement(element, newProps, ...newChildren) creates a new element based on an existing React element, merging new props with the existing ones. Useful when a parent wants to add or override props on children it receives: function Toolbar({ children }) { return React.Children.map(children, child => React.cloneElement(child, { className: "toolbar-item" })); }. This adds a className to every child without the child having to know about it. Common use cases for cloneElement: adding event handlers to children (RadioGroup adding onChange to Radio children), injecting context-based props, building compound components. Caution: cloneElement is fragile — it couples the parent to the children's props interface. Modern alternatives using Context or render props are often more maintainable. React 19 may deprecate cloneElement patterns in favor of Context.

Intermediate

Compound components are a pattern where multiple components work together to form a complete UI element, sharing implicit state via React Context. Think of HTML's <select> and <option> — they work together through an implicit relationship. React example: <Tabs><Tabs.Tab id="1" label="First" /><Tabs.Content id="1">Content...</Tabs.Content></Tabs>. The Tabs parent manages the active tab state and shares it via Context; Tabs.Tab and Tabs.Content read from that Context. Implementation: Tabs creates a context with the active ID and setter; the sub-components are attached as static properties (Tabs.Tab = TabComponent). Benefits: flexible API — consumers can reorder, omit, or add sub-components; implicit state sharing without prop drilling; great for UI component libraries (modals, tabs, accordions, dropdowns). Vs render props: compound components are more declarative and composable; render props can be more flexible for injecting dynamic content. Libraries like Radix UI heavily use compound components.

Intermediate

React.Children provides utility functions for working with props.children, which can be a single element, array, string, or undefined — making it tricky to work with directly. React.Children.map(children, fn): like Array.map but handles all child types (including null/undefined); returns a new array of React elements. React.Children.forEach(children, fn): same but returns nothing (for side effects). React.Children.count(children): returns the number of children. React.Children.only(children): returns the single child or throws if not exactly one child — useful for enforcing that a component accepts exactly one child. React.Children.toArray(children): flattens and converts to a real array with stable keys. Limitations: React.Children is deprecated in favor of modern patterns (direct array methods on children, or React 19's improvements). Issues include that cloning elements via React.Children.map can cause key warnings and is fragile. Modern alternative: if children is always an array (e.g., enforced by TypeScript), you can use Array.isArray checks and work directly.

Intermediate

shouldComponentUpdate(nextProps, nextState) is a lifecycle method in class components that lets you control whether a component should re-render. It receives the next props and state, and returns true (re-render) or false (skip render). By default, React always re-renders after setState() or when a parent re-renders — shouldComponentUpdate lets you skip unnecessary renders for performance. Example: shouldComponentUpdate(nextProps, nextState) { return nextProps.id !== this.props.id; } — only re-render when the id prop changes. React.PureComponent implements shouldComponentUpdate with a shallow comparison automatically, so you rarely need to write it manually. Function component equivalent: React.memo (with an optional comparison function). Pitfalls: returning false in shouldComponentUpdate prevents all re-renders, including when children need to update — be careful. Mutating objects directly means the shallow comparison sees no change and skips a needed re-render. Incorrect shouldComponentUpdate implementations cause subtle bugs. Prefer React.PureComponent or React.memo over manual shouldComponentUpdate.

Intermediate

Server-Side Rendering (SSR) renders React components on the server and sends the resulting HTML to the browser, instead of sending an empty HTML shell and rendering everything client-side. How it works: (1) A request comes in. (2) The server runs React to produce an HTML string. (3) The browser receives and displays the HTML immediately (fast First Contentful Paint). (4) React hydrates the HTML on the client — attaches event listeners and takes over interactivity. Benefits: faster initial page load (users see content before JavaScript executes); better SEO (search engines see the content); faster Time to First Byte. Challenges: server must be capable of running Node.js; window/document are not available during server render; hydration must match server output exactly or mismatches occur. Hydration mismatches: if server and client render different content, React warns and re-renders client-side — defeats the purpose. Frameworks: Next.js, Remix, and Gatsby automate SSR configuration. React 18 introduces streaming SSR — the server can stream HTML progressively rather than waiting for the entire page.

Intermediate

Hydration is the process of attaching React event handlers to the server-rendered HTML on the client, making the static HTML interactive. After SSR sends the HTML, the browser displays it immediately (content is visible without waiting for JavaScript). Then React downloads, parses, and runs the JavaScript bundle. React "hydrates" by traversing the existing DOM, comparing it to the virtual DOM, and attaching event handlers — without replacing the DOM (which would cause a flash). Hydration must match: the client render must produce the exact same output as the server — if any component renders differently (due to Date.now(), Math.random(), window checks), React logs a hydration mismatch warning and falls back to full client-side rendering for that subtree. Selective hydration (React 18): components inside Suspense boundaries hydrate independently — React can prioritize hydrating the parts the user interacts with first. Progressive hydration: defer hydration of below-the-fold components to reduce Time to Interactive. Islands architecture: frameworks like Astro hydrate only specific "islands" of interactivity, leaving the rest as static HTML.

Intermediate

Code splitting divides your JavaScript bundle into smaller chunks that are loaded on demand, reducing the initial download size and improving load time. Without code splitting, all your code is bundled into one large file that must be downloaded before the app starts. React.lazy + dynamic import: the primary way to split code in React: const Dashboard = React.lazy(() => import("./Dashboard")); — creates a separate chunk for Dashboard that only loads when it is rendered. Route-based splitting: the most impactful approach — each page/route loads its own code: in React Router, wrap lazy routes with Suspense. Component-based splitting: split large, rarely-used components (modals, charts, editors). Bundler support: webpack, Vite, and Rollup automatically create separate chunks for dynamic imports. The bundler analyzes the import graph and creates efficient chunks. Magic comments: webpack allows naming chunks: import(/* webpackChunkName: "charts" */ "./Charts"). Prefetching: use webpackPrefetch: true or Vite's prefetch to hint the browser to download chunks before they are needed.

Intermediate

Both handle cleanup when a component is unmounted, but useEffect cleanup is more powerful. componentWillUnmount: called once, just before the component is removed from the DOM. Only fires on unmount. useEffect cleanup: runs before the component unmounts AND before the effect re-runs (on every dependency change). This is a key difference: useEffect(() => { const sub = subscribe(id); return () => sub.unsubscribe(); }, [id]); — when id changes, the cleanup runs first (unsubscribes the old subscription), then the effect runs again (subscribes with the new id). With componentWillUnmount, you would need componentDidUpdate to handle the subscription change. useEffect cleanup handles both cases with one mechanism. Common cleanup tasks: cancel fetch requests (AbortController), clear timeouts/intervals, unsubscribe from observables/events, remove DOM event listeners, disconnect ResizeObserver/IntersectionObserver, close WebSocket connections. React 18 Strict Mode: cleanup + re-run happens once in development to verify cleanup is correct — this is intentional and reveals missing cleanup bugs.

Intermediate

Memoization in React is the optimization of caching computed values or component renders to avoid expensive recalculation when inputs have not changed. Three tools: (1) React.memo(Component): memoizes a component — skips re-rendering if props have not changed (shallow comparison). For components that receive the same props often. (2) useMemo(() => computation, [deps]): memoizes a computed value — only recomputes when dependencies change. For expensive calculations within a component. (3) useCallback(fn, [deps]): memoizes a function reference — returns the same function object unless dependencies change. For stable callback references to pass to memoized children or useEffect deps. Memoization is NOT always beneficial: each memoization has overhead (memory, comparison cost). For cheap computations and non-memoized children, the overhead exceeds the benefit. Profile first with React DevTools Profiler to identify actual bottlenecks before adding memoization. Common mistake: memoizing everything — this clutters code and can actually hurt performance. Profile, measure, optimize selectively.

Intermediate

The main performance issue with React Context is that every consumer re-renders when the context value changes, even if the specific piece of data the consumer uses did not change. If you put { user, theme, locale } in one context, changing the user causes the theme and locale consumers to re-render unnecessarily. Solutions: (1) Split contexts: create separate contexts for frequently and infrequently changing data — UserContext, ThemeContext, LocaleContext. Consumers only subscribe to what they need. (2) Memoize the context value: const value = useMemo(() => ({ user, logout }), [user]) — prevents a new object reference every render from triggering all consumers. (3) Context selectors (library solution): libraries like use-context-selector implement selector-based context subscriptions. (4) Colocate context consumption: place context consumers close to where they are needed — smaller subtrees to re-render. (5) Move state down: if only a small subtree needs the state, lift it no higher than necessary. For high-frequency updates (counters, animations), avoid context — use Zustand or Jotai instead.

Intermediate

React Portals render children into a DOM node outside the component's position in the DOM tree. Primary use cases: (1) Modals and Dialogs: must appear above all other content. Rendering inside a parent with overflow: hidden or low z-index would clip the modal. Portaling to document.body avoids this. (2) Tooltips: must escape any ancestor with overflow: hidden and appear at the top of the stacking context. (3) Dropdown menus: same overflow/z-index issues as tooltips. (4) Toast notifications: fixed-position elements that overlay all content. (5) Full-screen overlays. How portals maintain React behavior: although the DOM node is outside the parent, the portal is still part of the React tree — event bubbling follows the React component hierarchy, not the DOM hierarchy. Clicking inside a portal triggers React event handlers in parent components even though the DOM is elsewhere. Context works across portals. Accessibility: portaling a dialog requires proper focus management, ARIA attributes (role="dialog", aria-modal), and focus trapping to be accessible. Libraries like Radix UI primitives handle all accessibility requirements.

Intermediate

The React Profiler provides programmatic access to render performance data. The <Profiler> component: <Profiler id="Navigation" onRender={callback}><Nav /></Profiler>. The onRender callback receives: id (the Profiler id), phase ("mount" or "update"), actualDuration (time spent rendering this commit), baseDuration (estimated time to render without memoization), startTime, commitTime. Use actualDuration to identify slow components — if it is consistently high, that component or its children are expensive. React DevTools Profiler (browser extension): a visual, interactive version of the Profiler API with flamegraph, ranked chart, and timeline views. Record a session, interact with your app, and DevTools shows which components rendered, why, and how long each took. The "why did this component render?" feature identifies the specific prop or hook that triggered the render. Performance workflow: observe the problem → profile to find the component → understand why → apply memoization/restructuring → verify improvement in the profiler.

Intermediate

In server-side routing (traditional), navigating to a URL sends an HTTP request to the server, which responds with a full new HTML page — a full page reload occurs. Each navigation is a round trip to the server. Fast initial load, simple server setup, but slow navigation and full page reloads lose client state. In client-side routing (SPA), navigation is handled by JavaScript in the browser — React Router intercepts link clicks, pushes the URL to the browser's History API, and renders the appropriate component without a page reload. Only data changes (via API calls), not the entire page. Benefits: instantaneous navigation (no network round trip), smooth transitions, preserved state, offline-capable. React Router: uses the HTML5 History API to update the URL and render the matching component. Deep linking still works because the server must serve the same HTML for all routes (configure the server to return index.html for all paths). Next.js/Remix: combine both — initial page load is server-rendered for fast FCP and SEO, subsequent navigations are client-side for speed. This is the modern best practice.

Intermediate

React Testing Library (RTL) is the official recommended testing utility for React. Its philosophy: test components the way users interact with them — not implementation details. RTL renders components in a virtual DOM (via jsdom) and provides queries to find elements like a user would: by text, role, label, placeholder — not by class names or component internals. Key queries: getByRole("button", { name: "Submit" }), getByLabelText("Email"), getByText("Welcome"), findByText("Loaded!") (async). Key actions: userEvent.click(button), userEvent.type(input, "hello"). Assertions: with jest-dom: expect(element).toBeInTheDocument(), toHaveValue("hello"), toBeDisabled(). Philosophy benefits: tests do not break when you refactor internals (change class names, extract components) — they only break when behavior changes. This produces more durable, maintainable tests. Avoid: testing component state directly, implementation details, or internal methods. Instead, test what the user sees and can do.

Intermediate

Keys are central to React's reconciliation algorithm for efficiently updating lists. Without keys, React uses element position — if position n had a ListItem before and still does, React assumes it is the same instance and updates it in place. This is wrong for reordered lists — React might update a component with the wrong data, or preserve state from the wrong previous item. With keys: React associates key-identified elements across renders regardless of position. Algorithm: (1) For each key in the new list, check if a matching key exists in the old list. (2) If yes: update the existing component in place (preserve state, update props). (3) If the position changed: React can move the DOM node efficiently without recreating it. (4) Keys in old list with no match in new list: components are unmounted. (5) Keys in new list with no match in old: components are mounted. Keys must be stable across renders — if you generate keys with Math.random() or Date.now(), React sees every render as brand new and recreates all items. Stable IDs from your data model are ideal. Index as key: acceptable only for static lists that never reorder, add, or delete items.

Advanced

React Fiber is React's internal reconciliation engine, a complete rewrite introduced in React 16. A fiber is a plain JavaScript object representing a unit of work. Each React element corresponds to a fiber in the fiber tree. Each fiber contains: the element's type, the associated DOM node, parent/child/sibling fiber links, the effect list (DOM mutations needed), the hook state for that component, the work in progress alternate (for double buffering), and priority (Lanes in React 18). Double buffering: React maintains two trees — the current tree (what is rendered) and the work-in-progress tree (being computed). When work is complete, React atomically switches the two trees. This allows React to build the new tree without affecting the displayed UI. Work loop: the scheduler calls performUnitOfWork on each fiber, building the work-in-progress tree. After completing the work phase (all fibers processed), React enters the commit phase (applying DOM mutations synchronously — cannot be interrupted). Lanes: React 18's priority system — lanes are bitmasks that classify work priority (SyncLane, DefaultLane, TransitionLane, IdleLane). The scheduler picks the highest-priority lane to work on first.

Advanced

Beyond React.memo, several advanced techniques optimize React rendering. (1) State colocation: keep state as close to where it is used as possible — state at a high level means more components re-render on each update. Moving state down reduces the affected subtree. (2) Component composition with children: instead of rendering children inside a component that changes frequently, pass them as children prop from a stable parent — they do not re-render when the wrapper re-renders. (3) useMemo for expensive computations within render. (4) Virtualization for long lists (react-window, react-virtual, TanStack Virtual) — render only visible items, not the entire list. (5) Splitting providers: separate frequently-changing context from stable context. (6) Lazy initialization of state: useState(() => expensiveComputation()) — only runs once. (7) Transition API (useTransition) to deprioritize expensive renders. (8) useDeferredValue to prevent blocking renders. (9) Web Workers for truly CPU-intensive tasks (via Comlink or similar). (10) Server Components (Next.js App Router) — move static rendering to the server entirely.

Advanced

An undo/redo system requires storing a history of states. Using useReducer with a history stack: type HistoryState<T> = { past: T[]; present: T; future: T[] };. The reducer handles normal actions by pushing the current present to past, setting the new state as present, and clearing future. UNDO: pop from past, push present to future, set popped as present. REDO: pop from future, push present to past, set popped as present. Implement as a custom hook: function useHistory<T>(initialState: T) { const [state, dispatch] = useReducer(historyReducer, { past: [], present: initialState, future: [] }); const undo = () => dispatch({ type: "UNDO" }); const redo = () => dispatch({ type: "REDO" }); return { state: state.present, undo, redo, canUndo: state.past.length > 0, canRedo: state.future.length > 0 }; }. Optimizations: limit history length to prevent memory growth; use Immer for efficient state updates with structural sharing (unchanged parts of the state tree share references); debounce rapid changes (text input) to avoid storing every keystroke. Libraries like Redux Toolkit with redux-undo or Zustand with immer middleware handle this pattern well.

Advanced

React 18's concurrent features allow React to prepare multiple versions of the UI simultaneously, interrupting lower-priority work for higher-priority updates. Transitions mark state updates as non-urgent, allowing React to keep rendering the current UI while computing the transition in the background. startTransition(() => setFilter(newFilter)) — the filter update is "deferred" until React has bandwidth. If a higher-priority update arrives (user types), React abandons the in-progress transition and starts fresh. How it works under the hood: React maintains two rendering trees. The "urgent" tree is what the user sees now. The "background" tree is what React is computing for the transition. When the transition finishes, React atomically switches. If the transition is abandoned, the background tree is discarded — no wasted DOM mutations. Prioritization: React 18 uses a "Lanes" model — discrete inputs (clicks, typing) are SyncLane (highest priority), continuous inputs (scrolling) are InputContinuousLane, transitions are TransitionLane, and data fetching/background work is IdleLane. The scheduler always works on the highest-priority lane first.

Advanced

At scale, React forms face challenges: excessive re-renders (every keystroke re-renders the form), complex validation, nested forms, dynamic fields, and form state that interacts with application state. Strategies: (1) React Hook Form: uses uncontrolled inputs with refs — only re-renders on submit or when validation changes. Provides register, handleSubmit, formState.errors. Performance difference vs controlled: a 100-field form with RHF renders ~3 times on submit vs 100+ times with controlled inputs. (2) Schema validation: integrate Zod or Yup for declarative validation schemas that colocate validation rules with data types. (3) Field-level vs form-level rendering: in Formik/RHF, ensure field components are memoized to prevent full form re-renders on field changes. (4) Nested forms pattern: use React context to share form state across deeply nested custom fields without prop drilling. (5) Dynamic fields: RHF's useFieldArray handles dynamic list fields efficiently. (6) Server validation: use Remix's form actions or React Server Actions (React 19) for forms that interact directly with the server without client-side JavaScript state.

Advanced

The React Compiler (codenamed React Forget, now in beta as of 2024) is a Babel/webpack plugin that automatically adds memoization to React components and hooks — eliminating the need to manually write useMemo, useCallback, and React.memo. The compiler analyzes the component's data flow statically and inserts memoization at the right granularity — often more precisely than humans would. It understands React's semantics (rendering, state, props, hooks) and can determine which values are stable vs. which change on every render. How it works: the compiler transforms your React component into one that automatically caches its computations and skips work when inputs have not changed. Requirements: your code must follow React's rules (pure renders, correct hook usage) — the compiler cannot optimize code that violates rules. Impact: most apps will see significant performance improvements without code changes; the codebase becomes simpler (fewer memo/useCallback/useMemo calls); developers spend less time on manual performance tuning. Meta has shipped the compiler internally with large performance gains. It is progressively being rolled out to the ecosystem.

Advanced

React has evolved through several patterns for sharing component logic, each with different tradeoffs. (1) Custom Hooks (modern, preferred): extract stateful logic into a reusable "use..." function. No component wrapping, no props collision, no render tree pollution. Best for: any sharable stateful logic. (2) Higher-Order Components (HOCs): function that wraps a component, adding behavior. Problems: wrapper hell in DevTools, prop name collisions, unclear origin of props. Legacy pattern — hooks usually replace it. (3) Render Props: component that accepts a function prop to render. More explicit than HOCs but creates callback nesting. (4) Context: shares state/logic down the tree without props. Good for global concerns (auth, theme) but re-render issues for dynamic data. (5) Compound Components: multiple components sharing state via context, composable API. Best for UI component libraries. (6) State Management Libraries (Redux, Zustand, Jotai): globally accessible logic and state. (7) Suspense + use() (React 19): share async data fetching logic declaratively. Decision: custom hooks first; context for shared state; compound components for UI kits; global state libraries when context is too broad.

Advanced

React supports HTML's native accessibility features but requires care since JSX introduces pitfalls. Semantic HTML: use the right element — button for actions, a for links, form with label/input pairs. React preserves semantic meaning. ARIA attributes: use camelCase in JSX: aria-label, aria-describedby, role. Focus management: use refs to programmatically manage focus — when a modal opens, move focus inside; when it closes, return focus to the trigger. inputRef.current.focus(). useId for IDs: generate accessible IDs for label-input pairs that are stable across SSR hydration. Live regions: use aria-live="polite" for dynamic content updates that should be announced. Keyboard navigation: ensure all interactive elements are focusable and operable via keyboard. Avoid click-only interactions. Color contrast: meet WCAG 2.1 AA standards (4.5:1 for text). Escape key: handle Escape to close modals/dropdowns. Testing: axe-core (via jest-axe or React Testing Library's axe integration), Lighthouse, screen readers (NVDA, JAWS, VoiceOver). Libraries: Radix UI primitives handle focus management, keyboard nav, and ARIA patterns for common components.

Advanced

The use() hook (React 19) is a new primitive that reads the value of a resource (a Promise or a Context) during rendering. Unlike all other hooks, use() can be called conditionally and inside loops — it is not bound by the rules of hooks in that regard. With Promises: const data = use(fetchDataPromise); — if the promise is pending, the component suspends (falls back to the nearest Suspense boundary); if resolved, returns the value; if rejected, throws to the nearest Error Boundary. This replaces the pattern of storing async data in state via useEffect. With Context: const theme = use(ThemeContext); — equivalent to useContext but callable conditionally. Server Action integration: in Next.js App Router, use() integrates with server-side data fetching, Suspense streaming, and React Server Components. Important: use() cannot be called in a try-catch or inside event handlers — only in component bodies or other hooks. It represents a fundamental shift in how React handles asynchronous data — from imperative loading state to declarative Suspense-based data fetching.

Advanced

The Activity component (formerly Offscreen, experimental in React) allows rendering components in a hidden state — the component is mounted, its state is preserved, but it is not visible to the user. This enables instant-show UI by pre-computing and preserving component trees before they are needed. Use cases: tab panels (mount all tabs but only show the active one, preserving each tab's scroll position and state), virtual lists (preserve rendered items just off-screen), back-button navigation (preserve previous page state for instant back navigation). <Activity mode="hidden"><ExpensiveTab /></Activity>. In "hidden" mode: effects are not fired; the component is rendered but not painted; state is fully preserved. In "visible" mode: effects fire, content displays. How it differs from CSS display:none: React Activity truly controls when effects run and when the component participates in the React tree; CSS hide/show keeps the component mounted but does not pause effects. Status: still experimental as of React 18/19 — the API is subject to change. Available in experimental React builds.

Advanced

React Server Actions (stable in React 19, via Next.js App Router) are async functions that run on the server and can be called from client components — without needing to write a separate API endpoint. Mark a function with "use server" directive to make it a server action: async function createUser(formData) { "use server"; await db.user.create({ data: { name: formData.get("name") } }); revalidatePath("/users"); }. Invoking from client: pass to a form's action prop: <form action={createUser}><input name="name" /><button>Create</button></form>. The form submission calls the server function without page reload or manual fetch. With useActionState (React 19): const [state, action, isPending] = useActionState(createUser, null); — tracks pending state and the last result. Benefits: no API route boilerplate; progressive enhancement (works without JavaScript for form submissions); mutations are colocated with components; automatic CSRF protection in frameworks; direct database/service access. Security: never trust client-provided data — validate and authorize in the server action body.

Advanced

Micro-frontends apply microservice principles to the frontend — splitting a large frontend application into smaller, independently deployable pieces that can be developed and released by separate teams. Implementation strategies with React: (1) Module Federation (webpack 5): each micro-frontend exposes React components as remote modules; a shell app consumes them at runtime. Teams can deploy updates independently. (2) Single-SPA: a framework for routing between multiple independently deployed SPAs. (3) Iframes: strictest isolation but poor UX and styling limitations. (4) Web Components: wrap React apps in Custom Elements — technology-agnostic integration. Challenges: shared dependencies (multiple React versions is expensive — use Module Federation's shared scope); design consistency (need a shared design system); routing coordination; authentication sharing; performance (multiple React runtimes); communication between micro-frontends (custom events, shared state). When to use: large organizations with multiple independent teams working on the same application; when teams need to deploy independently without coordination. For most apps, a well-organized monorepo is simpler.

Advanced

At scale, React's built-in state tools (useState, useContext) have limitations: context has performance issues for frequently changing state; lifting state creates prop drilling; complex state machines are hard to express. Library comparison: Redux Toolkit: mature ecosystem, predictable state container, excellent DevTools (time-travel debugging), best for large teams and complex state. Boilerplate reduced significantly with RTK. Zustand: minimal API, unopinionated, no providers, excellent performance, growing adoption. Best for medium-complexity apps. Jotai: atomic state model — compose tiny pieces of state (atoms); very fine-grained reactivity (only components using a specific atom re-render). Best for highly dynamic, granular state. Recoil: similar to Jotai, from Meta. MobX: observable-based, automatic reactivity tracking, feels like Vue's reactivity. TanStack Query (React Query): specialized for server state (caching, synchronization, background refetch) — not a general state manager. Combine with Zustand for client state. Decision criteria: team size, complexity, DevTools needs, bundle size, and whether server state is the primary concern (use React Query) vs. complex client state (Redux Toolkit or Zustand).

Advanced
Back to All Topics 81 questions total