Skip to content
Plurafy

React Interview Questions

React is an open-source frontend JavaScript library for building web interface/components.


A Virtual DOM is an in-memory representation of actual DOM. Its used in React to efficiently compare the DOM changes and applies the diff to the actual DOM. It basically improves the performance of rendering process as directly manipulating actual DOM would be slower and expensive.

JSX stands for JavaScript XML. Its a syntax extension for JavaScript that lets you write HTML like code directly inside your JavaScript files.

Tools like Babel or SWC convert JSX into standard JavaScript function calls (React.createElement()) before the code runs in a browser.

// For an example:
<div>Hello World!</div>;
// is converted to
React.createElement("div", null, "Hello World!");

3. Difference between class component and functional component?

Section titled “3. Difference between class component and functional component?”

Class Components are es6 classes that extends React.Component and requires a render() function to return JSX. It rely on life cycle methods like componentDidMount(), componentDidUpdate() etc and manage state with this.state.

Functional Components are just general functions that takes props as input and return JSX. They rely on hooks (useEffect, useState, etc.) to handle life cycle events and manage state.

Functional component are preferred over class components as they are more cleaner and easier to understand. Class components are outdated but useful for error handling and can be used to create a ErrorBoundary component.

Stateless Components are often called as Presentational Components. They don’t manage state on their own. Instead they receive data via props and use them while rendering the component.

const FormInput = ({ type = "text", label, name, value, onChange }) => {
return (
<label>
{label}
<input type={type} name={name} value={value} onChange={onChange} />
</label>
);
};

On the other hand A Stateful Component manages their state internally. They can update their state based on some user actions or events.

function UserForm() {
const [formData, setFormData] = useState({
username: "",
email: "",
});
// Dynamically update the state whenever an input changes
const handleChange = (event) => {
const { name, value } = event.target;
setFormData((prevData) => ({
...prevData,
[name]: value,
}));
};
const handleSubmit = (event) => {
event.preventDefault();
console.log("Submitted State Data:", formData);
// Send 'formData' to your backend API here
};
return (
<form onSubmit={handleSubmit}>
<FormInput
label="Username:"
name="username"
value={formData.username}
onChange={handleChange}
/>
<FormInput
type="email"
label="Email:"
name="email"
value={formData.email}
onChange={handleChange}
/>
<button type="submit">Submit</button>
</form>
);
}

5. How to implement type checking in React?

Section titled “5. How to implement type checking in React?”

We can implement type checking at two different stages. At compile time using TypeScript and at run time using PropTypes(mostly with JavaScript).

But as of React 19, PropTypes are deprecated and its recommended to use TypeScript as standard.

interface UserDataProps {
name: string;
age: number;
email: `${string}@${string}.${string}`
}
function UserData({ name, age, email }) {
return (
<ul>
<li>Name: {name}</li>
<li>Age: {age}</li>
<li>Email: {email}</li>
</ul>
)
}

The primary purpose of the key prop in React is to give each element or component instance a stable, unique identity across renders. This identifier tells React’s reconciliation engine exactly which item in the list have been changed/added/removed, allowing React to optimize performance and preserve component state accurately.

Without key, React defaults to matching items by their array index. If you add a new item at the top of the list, React thinks all the items in the list have been changed, causing it to unnecessarily re-render the entire list.

By using an unique key, React compares old key with the new key and realizes that the existing items simply moved down and only mounts the single new item. This significantly improves the rendering performance.

We should not use index as keys as indexes will change when length of the list changes. This will result similar to using components without key.

function UserList({ users }) {
return (
<ul>
{
users.map((user, index) => <li key={index}>{user.name}</li>)
users.map((user) => <li key={user.id}>{user.name}</li>)
}
</ul>
)
}

In controlled component form data is managed by component state but in Uncontrolled component, form data is managed by the browser DOM.

Feature Controlled Components Uncontrolled Components
Source of Truth React State (useState) The Browser DOM
Data Retrieval Accessed instantly via the state variable Pulled on demand using React Refs (useRef) or FormData
Value Tracking Updates continuously via an onChange handler Tracks internally; uses defaultValue for initial seeding
Re-renders Re-renders the component on every keystroke Does not trigger re-renders as the user types
Use Case Complex validation, dynamic layouts, and instant UI feedback Simple forms, non-React library integrations, and file uploads (<input type="file">)
// Controlled
function ControlledInput() {
const [value, setValue] = useState();
return (
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}
// Uncontrolled
function UncontrolledInput() {
const ref = useRef();
return <input type="text" ref={ref} />;
}

8. How to share data between React components?

Section titled “8. How to share data between React components?”

In React, sharing data between components depends on the relationship between those components. The three primary approaches are passing data via Props, Lifting State Up and using the Context API.

  1. Parent to Child(Using Props)

    The most straightforward way to pass data is downward(unidirectional data flow) from a parent component to a child component using props.

    // Parent Component
    function Parent() {
    return <Child message="Hello from Parent!" />;
    }
    // Child Component
    function Child({ message }) {
    // Destructuring the prop
    return <h2>{message}</h2>;
    }
  2. Child to Parent(Using Callback Functions)

    To send data up from a child to a parent, you still use props, but you pass a callback function from the parent to the child.

    // Parent Component
    function Parent() {
    const handleChildData = (data) => {
    console.log("Data from child:", data);
    };
    return <Child onSendData={handleChildData} />;
    }
    // Child Component
    function Child({ onSendData }) {
    return (
    <button onClick={() => onSendData("Hello Parent!")}>
    Send to Parent
    </button>
    );
    }
  3. Between Sibling Components(Lifting State Up)

    When two sibling components need to share the same changing data, you must lift the state up to their closest common parent component.

    // Common Parent Component
    function Parent() {
    const [sharedText, setSharedText] = useState("");
    return (
    <>
    <InputSibling onChangeText={setSharedText} />
    <DisplaySibling text={sharedText} />
    </>
    );
    }

In React, the state change cycle describes the exact sequence of events that occurs from the moment a state variable is modified to the moment the updated user interface (UI) is painted on the screen.

Modern React operates this cycle via a internal framework paradigm called Render and Commit, which divides the cycle into three main phases: Triggering, Rendering, and Committing.

Diagram
  1. Triggering a Render The cycle begins when a component’s state needs to change. This is usually sparked by user interactions (like clicking a button), network responses, or timers and a state updater function (like setCount from the useState hook) or this.setState() is called. Calling this function signals to React that the component’s UI is now out-of-date and needs to be recalculated. React queues this update.

  2. Rendering the Component Rendering means React is calling your component function to figure out what the UI should look like. React executes the component function from top to bottom using the new state values that returns a new tree of JSX elements(the Virtual DOM). For a state update, React compares this new JSX tree with the old tree from the previous render. It calculates exactly what changed.

  3. Committing to the DOM Once React knows exactly what changed, it enters the commit phase and updates the actual browser DOM. Because of the diffing process in the rendering phase, React only changes the specific DOM nodes that actually need updating. If your state update didn’t change what the JSX outputs, React won’t touch the DOM at all.

Once the DOM is updated, React releases control back to the browser. The browser repaints the screen, and the user finally sees the updated state reflected visually.

Props mirroring in React is an anti-pattern where a component copies an incoming prop directly into its own local state via useState or a constructor, leading to synchronized state issues.

Problem:

When you copy a prop straight into state upon initial render, the local state does not automatically update if the parent component changes that prop later on. This breaks the single source of truth principle in React.

// ANTI-PATTERN: Props Mirroring
function ChildComponent({ initialCount }) {
// The prop is only used once to seed the state
const [count, setCount] = useState(initialCount);
// If initialCount changes in the parent, 'count' here won't change!
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

Exception:

Sometimes developers want to initialize state from a prop. If you truly only need the initial value and will never sync it again, it is better to name the prop clearly (like initialValue or defaultCount) to signal that subsequent updates are ignored.

If you do need the component to stay synchronized with the prop, you should avoid storing it in state entirely and just use the prop directly inside your render logic, or compute it on the fly.

11. What are hooks in React and how to use them?

Section titled “11. What are hooks in React and how to use them?”

Hooks are built-in functions in React that allow you use state and different other React features inside functional components. Before Hooks, developers had to write class components to manage a component’s lifecycle or local state. Hooks let you “hook into” these capabilities using simpler, cleaner JavaScript functions.

You can either use built-in hook or combine them to build your custom hook.

Some of the built-in hooks are useState, useEffect, useMemo, useCallback etc.

Simple custom hook using react

function useToggle(initialValue = false) {
// 2. Use built-in React hooks inside
const [value, setValue] = useState(initialValue);
// 3. Define the logic to modify the state
const toggle = () => {
setValue((prevValue) => !prevValue);
};
// 4. Return the data and functions your component will need
return [value, toggle];
}

Rules of Hooks

To ensure React can correctly track your state and lifecycle triggers, all Hooks must follow two main rules:

  1. Call Hooks only at the top level: Do not call Hooks inside loops, conditions, or nested functions.
  2. Call Hooks only from React functions: You can call them from React functional components or your own custom Hooks, but never from regular JavaScript functions or old class components.

The useRef hook is a built-in React hook that allows you to reference a value that is not needed for rendering. It returns a mutable object with a single property called .current.

Unlike state useState, mutating the .current property does not trigger a component re-render. The value you store inside a ref persists across your component’s entire lifecycle.

import { useRef } from "react";
const myRef = useRef(initialValue);
// Returns: { current: initialValue }

Core Use Cases

  1. Manipulating the DOM Directly

    The most common use case for useRef is to interact with HTML elements directly (similar to using document.querySelector in vanilla JavaScript). This is ideal for managing text input focus, scrolling to a specific element, or measuring element dimensions.

    import { useRef, useEffect } from "react";
    function FocusInput() {
    const inputRef = useRef(null); // 1. Create the ref
    useEffect(() => {
    // 3. Access and focus the DOM element via .current
    inputRef.current.focus();
    }, []);
    return (
    <>
    // 2. Attach the ref to the JSX node
    <input ref={inputRef} type="text" />
    </>
    );
    }
  2. Storing Information That Doesn’t Change the UI

    If a value needs to be remembered between renders but changing it shouldn’t cause the page to redraw, use a ref. Common examples include storing interval/timeout IDs, tracking a render counter, or caching previous state values.

    import { useRef, useEffect } from "react";
    function Timer() {
    const timerIdRef = useRef(null);
    useEffect(() => {
    // Store the interval ID so we can clear it later
    timerIdRef.current = setInterval(() => {
    console.log("Tick...");
    }, 1000);
    return () => clearInterval(timerIdRef.current);
    }, []);
    const stopTimer = () => {
    // Clears the interval without causing a re-render
    clearInterval(timerIdRef.current);
    };
    return <button onClick={stopTimer}>Stop Log</button>;
    }

13. What is the useId hook and when should it be used?

Section titled “13. What is the useId hook and when should it be used?”

The useId hook is a built-in React hook introduced in React 18 that generates unique, stable ID strings.

Its primary purpose is to create component-scoped identifiers that remain perfectly consistent across both the server and the client. This consistency solves a major problem in modern web apps: when using server-side rendering (SSR), functions like Math.random() or simple global counters will generate different IDs on the server than they do on the client, which completely breaks hydration. useId guarantees the exact same IDs are generated during both phases.

When to use useId?

You should use useId when you need to link DOM elements together for HTML accessibility attributes (ARIA). The most common use cases include:

  • Form Labels: Linking a
  • ARIA Attributes: Connecting descriptive elements with attributes like aria-describedby, aria-labelledby, or aria-controls.

When you build a reusable component that might be rendered multiple times on the same page, useId ensures that each instance gets its own unique ID pairs, preventing browser focus or screen reader clashes

import { useId } from "react";
function ProtectedField() {
// Generates a unique, stable string ID (e.g., ":r0:")
const hintId = useId();
return (
<div>
<label htmlFor={hintId}>Password:</label>
<input type="password" aria-describedby={hintId} id={hintId} />
<p id={hintId}>Must contain at least 8 characters.</p>
</div>
);
}

When not to use useId?

Developers often mistake useId as a general-purpose unique string generator. There are strict scenarios where you must avoid it.

  1. Do not use it to generate keys in a list: React key props must be generated from your actual data (like a database ID). Using useId in a loop will break React’s reconciliation engine during re-renders.
  2. Do not use it as a data cache key: It should not be used to generate keys for caching systems or the use() hook.
  3. Do not call it conditionally or inside loops: Like all React hooks, useId must only be called at the very top level of your functional component.

14. How do you implement routing in React?

Section titled “14. How do you implement routing in React?”

We can implement routing in React using the React Router(react-router-dom) library. The modern, recommended approach (React Router v6.4+) uses a Data Router configuration, which separates route definitions from the JSX tree to enable advanced data-fetching APIs. Alternatively, you can use the traditional JSX component-based declaration.

  1. Approach A: The Modern Data Router

    This approach defines your routes as a JavaScript configuration array and provides them to the app using RouterProvider.

    import React from "react";
    import ReactDOM from "react-dom/client";
    import { createBrowserRouter, RouterProvider } from "react-router-dom";
    import Home from "./pages/Home";
    import About from "./pages/About";
    import NotFound from "./pages/NotFound";
    // Configure the paths and their corresponding components
    const router = createBrowserRouter([
    {
    path: "/",
    element: <Home />,
    errorElement: <NotFound />, // Handles 404s/errors
    },
    {
    path: "/about",
    element: <About />,
    },
    ]);
    ReactDOM.createRoot(document.getElementById("root")).render(
    <React.StrictMode>
    <RouterProvider router={router} />
    </React.StrictMode>,
    );
  2. Approach B: Traditional JSX Component Routing

    If you prefer configuring your routes directly inside your JSX component layout (e.g., inside App.jsx), wrap your layout with BrowserRouter.

    import { BrowserRouter, Routes, Route } from "react-router-dom";
    import Home from "./pages/Home";
    import About from "./pages/About";
    import NotFound from "./pages/NotFound";
    function App() {
    return (
    <BrowserRouter>
    <Routes>
    <Route path="/" element={<Home />} />
    <Route path="/about" element={<About />} />
    {/* The "*" path catches any URL that doesn't match above */}
    <Route path="*" element={<NotFound />} />
    </Routes>
    </BrowserRouter>
    );
    }
    export default App;

15. How do you programatically navigate between routes using React Router and share data between routes?

Section titled “15. How do you programatically navigate between routes using React Router and share data between routes?”

We can use the useNavigate hook inside the functional components to trigger navigation inside a function or event handler.

import { useNavigate } from "react-router-dom";
function LoginButton() {
const navigate = useNavigate();
const handleLogin = () => {
// Perform authentication logic here...
navigate("/dashboard");
};
return <button onClick={handleLogin}>Log In</button>;
}

How to Share Data Between Routes?

Depending on the type of data and how it needs to be accessed, there are four primary patterns for sharing data between routes:

  1. Route State (Best for transient data) We can pass an arbitrary object inside the state property during navigation using the navigate() function. This data isn’t visible in the URL bar and disappears if the user copies and pastes the link in a new tab.

    Sending

    // Programmatically:
    navigate("/profile", { state: { userId: 123, username: "Alex" } });
    // Declaratively:
    <Link to="/profile" state={{ userId: 123, username: "Alex" }}>
    View Profile
    </Link>;

    Receiving

    import { useLocation } from "react-router-dom";
    function Profile() {
    const location = useLocation();
    const { userId, username } = location.state || {}; // Fallback in case state is undefined
    return <h1>Welcome, {username}!</h1>;
    }
  2. URL Parameters (Best for unique IDs & resource queries)

    URL parameters embed data directly into the path string (e.g., /users/123). This is ideal for shareable, bookmarkable links.

    Sending

    navigate(`/users/${userId}`);

    Receiving

    import { useParams } from "react-router-dom";
    function UserProfile() {
    const { id } = useParams(); // Retrieves '123'
    return <div>User ID: {id}</div>;
    }
  3. Query Strings / Search Parameters (Best for filtering & sorting)

    Query strings append data to the end of the URL after a question mark (e.g., /shop?category=shoes&sort=price)

    Sending

    navigate("/shop?category=shoes");

    Receiving

    import { useSearchParams } from "react-router-dom";
    function Shop() {
    const [searchParams] = useSearchParams();
    const category = searchParams.get("category"); // Retrieves 'shoes'
    return <div>Displaying items for: {category}</div>;
    }
  4. Global State Management (Best for persistent application state)

    If data needs to be accessed by many different routes and persist throughout the entire session (like user authentication status or a shopping cart), passing data through the router becomes cumbersome. Instead, use global state:

    • React Context API: Native to React and wraps your router structure to distribute global variables.
    • State Libraries: External systems like Redux Toolkit or Zustand manage a central data store accessible from any view regardless of active route conditions.

16. What is the difference between useEffect and useLayoutEffect in React?

Section titled “16. What is the difference between useEffect and useLayoutEffect in React?”

The primary difference between useEffect and useLayoutEffect is when they execute in relation to the browser painting the screen. While both hooks share the exact same syntax and accept the same dependency arrays, they serve different performance and visual purposes.

  • useEffect runs asynchronously after the browser paints the screen. It non-blocking and won’t delay screen rendering. Primarily used for data fetching, event listeners and analytics.
  • useLayoutEffect runs synchronously before the browser paints the screen. It blocks painting until the code finishes executing. Primarily used for measuring DOM nodes, custom animations, fixing UI flickering.

Lifecycle Flow

Diagram
import { useState, useEffect, useLayoutEffect, useRef } from "react";
const defaultStyles = {
background: "lightblue",
margin: "10px auto",
padding: "10px",
};
export default function BasicExample() {
const [count, setCount] = useState(0);
const boxRef = useRef(null);
// 1. Synchronous (Runs BEFORE the screen updates)
useLayoutEffect(() => {
if (boxRef.current) {
// Safely measure or tweak DOM elements directly before the user sees them
console.log(
"1. useLayoutEffect: Box width is",
boxRef.current.offsetWidth,
);
}
}, [count]); // Fires every time count changes
// 2. Asynchronous (Runs AFTER the screen updates)
useEffect(() => {
// Perfect for tasks that don't affect the visual layout
console.log("2. useEffect: Count updated to", count);
document.title = `Count: ${count}`;
}, [count]); // Fires every time count changes
return (
<div style={{ padding: "20px", textAlign: "center" }}>
<div
ref={boxRef}
style={{ width: `${100 + count * 20}px`, ...defaultStyles }}
>
Box Width changes with count
</div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}

A pure component in React is a component that renders the same output for the same state and props, helping optimize performance by skipping unnecessary re-renders.

They compare primitive values (like strings and numbers) by value, and complex values (like arrays and objects) by memory reference. If the shallow comparison finds no changes in props or state, React bypasses the render process entirely for that component. You can implement pure components depending on whether you are writing class-based or modern functional code.

import React from "react";
// Extending PureComponent automatically implements a shallow prop/state check
class UserCard extends React.PureComponent {
render() {
// UserCard rendered only when props or state change!
return <div>User: {this.props.name}</div>;
}
}
// React.memo makes this a pure component.
// Without React.memo it will re-render every single time the parent component re-renders.
const UserCard = React.memo(({ name }) => {
// "UserCard rendered only when 'name' changes!"
return <div>User: {name}</div>;
});

Common Pitfalls:

  • Because pure components only do a shallow check, mutating a nested object or array directly in memory will fail to trigger a re-render. You must create a new copy of the object or array instead.
  • Passing objects created inline on each render breaks purity because a new reference is generated every time.
    // ❌ Breaks memoization because { id: 1 } is a new object on every render
    <MyComponent config={{ id: 1 }} />
  • Inline Functions: Passing inline functions creates a new function instance on every render. Use the useCallback hook to preserve function references across renders.
    // ❌ Creates a new function reference every render
    <MyComponent onClick={() => doSomething()} />
  • The render phase must remain free of side effects and be completely predictable for identical inputs. The component must not use any guest/global variables that may cause unpredictable results.

useCallback is a React Hook that caches (memoizes) a function definition between re-renders.

When a React component re-renders, every function defined inside it is recreated from scratch. In most cases, this creation is fast and negligible. However, useCallback allows you to preserve the same function instance across renders unless its dependencies change.

const cachedFn = useCallback(fn, dependencies);

On the initial render, useCallback returns the function you passed. On subsequent renders, if the dependencies haven’t changed, React returns the exact same function instance from the previous render.

When Should You Use It?

  1. Passing Callbacks to Optimized Child Components(Pure Components)

    If a child component is wrapped in React.memo to skip re-rendering when props don’t change, passing an inline function as a prop breaks that optimization—because a new function instance is created on every parent render.

    import { useState, useCallback, memo } from "react";
    // Child component skips re-rendering if its props haven't changed
    const ExpensiveChild = memo(({ onClick }) => {
    return (
    <>
    ...
    <button onClick={onClick}>Click Me</button>
    </>
    );
    });
    function Parent() {
    const [count, setCount] = useState(0);
    const [otherState, setOtherState] = useState(0);
    // Without useCallback, handleClick would be a new reference on every render,
    // forcing ExpensiveChild to re-render even if count didn't change.
    const handleClick = useCallback(() => {
    console.log("Count:", count);
    }, [count]);
    return (
    <div>
    <button onClick={() => setOtherState(otherState + 1)}>
    Update Unrelated State ({otherState})
    </button>
    <ExpensiveChild onClick={handleClick} />
    </div>
    );
    }
  2. The Function is a Dependency in Another Hook If a function defined inside your component is listed in the dependency array of a useEffect, wrapping it in useCallback prevents the effect from firing on every single render.

    function SearchResults({ query }) {
    const fetchData = useCallback(async () => {
    const response = await fetch(`/api/search?q=${query}`);
    return response.json();
    }, [query]); // Only recreated if 'query' changes
    useEffect(() => {
    fetchData().then((data) => {
    // Handle data...
    });
    }, [fetchData]); // Effect only runs when fetchData reference changes
    }
  3. Custom Hooks

    When building custom hooks, any functions returned by the hook should generally be wrapped in useCallback so consumers can safely use them inside their own useEffect dependency arrays or optimized components.

19. What is the use of useMemo hook in React?

Section titled “19. What is the use of useMemo hook in React?”

useMemo is a React Hook that caches (memoizes) the calculated result of a function between re-renders, preventing expensive, resource-heavy calculations from running on every render.

  1. Skipping Expensive Recalculations

    React re-runs the entire component body whenever state or props change. If your component contains a heavy calculation, that calculation will execute again on every single render—even if the inputs to that calculation haven’t changed. useMemo wraps your calculation and only re-executes it when one of its specified dependencies changes.

    import { useMemo } from "react";
    function ProductList({ products, filterText }) {
    // Only recalculates when `products` or `filterText` changes
    const filteredProducts = useMemo(() => {
    return products.filter((p) => p.name.includes(filterText));
    }, [products, filterText]);
    return <div>{/* Render filteredProducts */}</div>;
    }
  2. Preserving Referential Equality for Child Components

    In JavaScript, object and array literals ({}, []) are created with a new memory reference on every render. If you pass an unmemoized object or array as a prop to a child component wrapped in React.memo, the child will re-render every time because its prop reference changed.

    function Parent({ userId }) {
    // Without useMemo, 'options' gets a new memory reference every render
    const options = useMemo(
    () => ({
    userId,
    theme: "dark",
    }),
    [userId],
    );
    return <MemoizedChild options={options} />;
    }
  3. Stabilizing Dependencies for Other Hooks

    If an object, array, or computed value is included in the dependency array of another hook (like useEffect or useCallback), wrapping that value in useMemo prevents the dependent hook from firing unnecessarily on every render.

20. What is the use of useReducer hook in React?

Section titled “20. What is the use of useReducer hook in React?”

The useReducer hook in React is an alternative to useState designed for managing complex or multi-step state logic within a component. It follows the Redux-style state management pattern, taking a state and an action, and returning a new state.

const [state, dispatch] = useReducer(reducer, initialState);
  1. reducer(state, action): A pure function that takes the current state and an action object, and returns the next state based on the action type.
  2. initialState: The initial value of your state.
  3. dispatch(action): A function used to trigger state updates by passing an action object (typically containing a type string and optional payload).
import { useReducer } from "react";
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
case "reset":
return { count: 0 };
default:
throw new Error(`Unhandled action type: ${action.type}`);
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "reset" })}>Reset</button>
</div>
);
}

Ideal Use Cases for useReducer

  • Complex Forms: Managing forms with multiple inputs, validation states, touched/dirty flags, and submission states.
  • Dependent State Updates: When the next state relies heavily on values in previous states or multiple fields need to update simultaneously (e.g., setting isLoading: true, error: null, and data: null on a fetch request).
  • State Machines: Modeling finite states (e.g., idle -> loading -> success / error) to prevent illegal state combinations.
  • Prop Drilling Mitigation with Context: Passing down a single static dispatch function via React Context to deep child components instead of creating and passing down multiple separate callback functions.

In React, forwardRef() is a utility function used to pass a ref through a component to one of its child components or to an underlying DOM element.

Key Use Cases

  • Direct DOM Access: Giving a parent component direct access to a child’s DOM node (e.g., to focus an input, scroll an element into view, or measure dimensions).
  • Higher-Order Components (HOCs): Forwarding refs through wrapper or container components so the consumer can access the wrapped component’s underlying DOM element or instance.
  • Component Libraries: Creating reusable, foundational UI elements (like buttons, inputs, or modals) that allow consumers to attach standard DOM refs.
import React, { useRef, forwardRef } from "react";
// 1. Wrap the child component with forwardRef
const CustomInput = forwardRef((props, ref) => {
return <input ref={ref} className="input-styled" {...props} />;
});
// 2. Parent component creates and passes the ref
function ParentComponent() {
const inputRef = useRef(null);
const handleFocus = () => {
// Directly focus the underlying HTML <input>
inputRef.current.focus();
};
return (
<div>
<CustomInput ref={inputRef} placeholder="Type here..." />
<button onClick={handleFocus}>Focus Input</button>
</div>
);
}

In React 19, ref is a regular prop on function components and forwardRef is deprecated.

React Strict Mode (<React.StrictMode>) is a feature tool that wraps components to perform runtime checks and surface potential bugs during local development. It renders no visible UI and has zero impact on the production build.

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>,
);

Key Benefits:

  1. Detecting Impure Renders (Double Rendering)

    React expects component rendering to be a pure calculation with no side effects. In development mode, Strict Mode intentionally calls component functions and state updaters twice to surface unexpected side effects.

    If a component mutates an external variable during rendering, rendering it twice produces inconsistent results. Strict Mode exposes these side effects early before they cause subtle bugs in production.

  2. Validating Effect Cleanup (Double Effect Execution)

    In React 18+, Strict Mode mounts, unmounts, and remounts components on initial render in development.

    If a useEffect sets up a subscription, timer, or EventListener without returning a cleanup function, remounting leads to memory leaks or duplicate listeners. It immediately exposes missing cleanup logic (e.g., duplicate WebSocket connections or double API calls in dev mode).

  3. Detecting Deprecated or Unsafe APIs

    Strict Mode warns in the browser console when your app or third-party dependencies use outdated React patterns, including:

    • Unsafe Lifecycle Methods: componentWillMount, componentWillReceiveProps, componentWillUpdate
    • String Ref API: Using this.refs.myInput instead of useRef() or createRef().
    • Legacy Context API: Older context patterns replaced by React.createContext
    • Deprecated findDOMNode: An old escape hatch that breaks abstraction boundaries.

Render props is a technique in React for sharing code between components using a prop whose value is a function.

Instead of hardcoding what a component renders, the component accepts a function that returns a React element and calls that function inside its render logic.

// 1. Component that encapsulates state/logic
function MouseTracker({ render }) {
const [position, setPosition] = React.useState({ x: 0, y: 0 });
const handleMouseMove = (event) => {
setPosition({ x: event.clientX, y: event.clientY });
};
return (
<div style={{ height: "100vh" }} onMouseMove={handleMouseMove}>
{/* Pass state to the render prop function */}
{render(position)}
</div>
);
}
// 2. Parent component deciding how to present that state
function App() {
return (
<MouseTracker
render={({ x, y }) => (
<h1>
The mouse position is ({x}, {y})
</h1>
)}
/>
);
}

Key Benefits

  • Code Reuse: Share complex behavior (like tracking scroll, handling form state, or fetching data) across multiple components without duplicating code.
  • Inversion of Control: The consumer determines how the UI looks based on the provided state, while the provider component handles how the state updates.
  • Dynamic Rendering: You can pass dynamic data into the callback function to render completely different UI structures in different contexts.

While render props were the standard solution for sharing stateful logic prior to React 16.8, Custom React Hooks have largely replaced them for most use cases because Hooks avoid deep JSX nesting (“wrapper hell”).

24. How do you prevent http request race conditions in React?

Section titled “24. How do you prevent http request race conditions in React?”

In React, we can use AbortController to cancel ongoing asynchronous tasks, most commonly fetch requests, when a component unmounts or when a user triggers a new request before the previous one finishes. This prevents memory leaks, race conditions, and unnecessary network bandwidth consumption.

import { useState, useEffect } from "react";
function SearchResults({ query }) {
const [results, setResults] = useState({});
useEffect(() => {
const controller = new AbortController();
let active = true;
setResults({ status: "loading" });
const fetchResults = async () => {
try {
const response = await fetch(`/api/search?q=${query}`, {
signal: controller.signal,
});
const data = await response.json();
if (active) {
setResults({ status: "success", data });
}
} catch (e) {
if (active) {
setResults({ status: "error" });
}
}
};
fetchResults();
return () => {
controller.abort(); // Cancels the in-flight request
active = false;
};
}, [query]);
return <div>{/* Render results */}</div>;
}

Reconciliation is React’s process for determining what changed in your UI after a component re-renders, then applying only the necessary updates to the actual DOM. React compares the newly returned element tree with the previous one during the render phase, and applies the calculated changes during the commit phase.

For example:

function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}

When count changes:

  • React calls the component again and creates a new UI description.
  • React compares it with the previous description.
  • It notices that only the text inside <h1> changed.
  • React updates that text in the DOM instead of rebuilding the entire page.

What React compares?

React mainly uses these factors to decide whether to preserve or replace an element:

  • Element type: <div> and <span> are different types, so React replaces one with the other.
  • Position: Elements in the same position are generally compared with each other.
  • Props: If the type is unchanged, React updates changed props while reusing the existing DOM node.
  • Keys: In lists, keys help React identify which item was added, removed, or moved. Matching keys allow React to preserve the item’s state.

Example:

{
items.map((item) => <li key={item.id}>{item.name}</li>);
}

Use a stable, unique identifier such as item.id, rather than an array index when list items can be reordered or removed.

Code splitting in React is the practice of dividing your application’s JavaScript bundle into smaller chunks that are downloaded only when needed. Instead of loading the entire app at startup, the browser loads the code required for the current page and fetches other features later. This can reduce initial load time and improve responsiveness.

React commonly uses JavaScript’s dynamic import() together with React.lazy and Suspense.

Example:

import { Suspense, lazy } from "react";
const SettingsPage = lazy(() => import("./SettingsPage"));
function App() {
return (
<Suspense fallback={<p>Loading...</p>}>
<SettingsPage />
</Suspense>
);
}

Here:

  • SettingsPage is placed in a separate JavaScript chunk by the bundler.
  • That chunk is downloaded when React needs to render the component.
  • Suspense displays a fallback while the chunk loads.

Benefits and trade-offs

Benefits:

  • Smaller initial JavaScript download.
  • Faster startup, especially for large applications.
  • Less JavaScript to parse and execute initially.
  • Heavy features or rarely used pages can load on demand.

Trade-off: each additional chunk may require a network request, so splitting extremely small components can add unnecessary overhead. In practice, split at route boundaries or around large, optional features.

React 19, released as stable on December 5, 2024, focuses on simplifying async mutations, forms, server rendering, and resource loading. The biggest changes are Actions, the new use API, improved Server Components support, and more capable React DOM primitives.

Major additions:

  1. Actions for async mutations

    React can now manage common mutation states—pending, errors, optimistic updates, and form submission, without as much manual useState code.

    New capabilities include:

    • Async functions inside transitions.
    • Automatic pending-state tracking.
    • Error handling through Error Boundaries.
    • Automatic optimistic-update rollback.
    • Functions passed directly to <form action> and formAction.
    • Automatic resetting of uncontrolled forms after successful submission.

    For Example:

    <form action={submitAction}>
    <input name="name" />
    <button disabled={isPending}>
    {isPending ? "Saving..." : "Save"}
    </button>
    </form>
  2. New hooks

    React 19 adds several APIs built around Actions:

    API Purpose
    useActionState Runs an Action and exposes its latest result plus pending state
    useOptimistic Shows an immediate temporary UI while a request is pending
    useFormStatus Lets nested form components read the parent form’s pending status
    use Reads promises or context during render

    use is particularly notable because it can read a promise and suspend until it resolves:

    function Comments({ commentsPromise }) {
    const comments = use(commentsPromise);
    return comments.map(comment => (
    <p key={comment.id}>{comment.text}</p>
    ));
    }

    Unlike ordinary hooks, use may be called conditionally, although promises should generally come from a Suspense-compatible framework or cache rather than being created during render.

  3. Server Components and Server Actions

    React 19 includes the Server Components features previously available through React’s Canary releases. Server Components can render at build time or on a server before client bundling, while Server Actions let client components call async server-side functions through the "use server" directive.

  4. Metadata directly in components

    You can now render document metadata wherever it makes sense in the component tree:

    function BlogPost({ title }) {
    return (
    <article>
    <title>{title}</title>
    <meta name="description" content="A blog post" />
    <h1>{title}</h1>
    </article>
    );
    }

    React hoists supported <title>, <meta>, and <link> elements into the document <head>, including during streaming SSR and Server Component rendering.

  5. Built-in resource handling

    React 19 adds support for:

    • Stylesheets with precedence management.
    • Async scripts rendered within components.
    • Resource hints such as preload, preinit, preconnect, and prefetchDNS.
    • Static pre-rendering APIs: prerender and prerenderToNodeStream.
    • These features help React coordinate resource loading with concurrent and streaming rendering.

React Fiber is React’s internal reconciliation architecture system that React uses to determine what changed in your component tree and how to update the UI efficiently and incrementally. It was introduced in React 16 as a rewrite of the older stack-based reconciler.

How it works

React represents each component or rendered element with a Fiber node. A Fiber stores information such as:

  • The component or DOM element type.
  • Its props and state.
  • Its position in the component tree.
  • Its relationship to parent, child, and sibling Fibers.
  • Work that React needs to perform for that node.

React breaks rendering into smaller units of work instead of processing the entire tree in one uninterrupted call stack. This allows it to:

  • Pause and resume rendering.
  • Split large updates into chunks.
  • Prioritize urgent updates.
  • Reuse completed work.
  • Abandon work that is no longer needed.

Render and commit phases

Fiber separates an update into two broad phases:

  • Render phase: React calculates what should change. This work can be interrupted or restarted.
  • Commit phase: React applies the completed changes to the DOM. This phase is kept consistent and is not partially applied.

For example, while React is rendering a large list, a more urgent input update can receive priority so typing remains responsive.

29. How would you investigate a React page that feels slow?

Section titled “29. How would you investigate a React page that feels slow?”

Investigating performance issues in a React application requires a systematic approach to isolate whether the bottleneck is caused by rendering overhead, unnecessary re-renders, network/data fetching latency, or main-thread blocking JavaScript.

Here is a structured framework for diagnosing and fixing a slow React page.

  1. Initial Profiling & Measurement
    Before writing or changing any code, establish baseline metrics to pinpoint where time is being spent.

    A. Chrome DevTools (Performance Tab)

    1. Open DevTools in an Incognito Window (to disable extensions that distort performance).
    2. Record a Performance Profile while interacting with the slow feature.
    3. Check the Main Thread Flamechart:
      • Long Tasks (Red Flags): Tasks taking longer than 50ms block user input.
      • Call Stack: Look for heavy JavaScript execution, excessive layout thrashing, or expensive synchronous calculations.
    4. Check Core Web Vitals:
      • LCP (Largest Contentful Paint): Indicates slow initial loading or unoptimized images/assets.
      • INP (Interaction to Next Paint): Indicates slow response to clicks, taps, or keypresses.
      • CLS (Cumulative Layout Shift): Highlights layout instability during load or state updates.

    B. React Developer Tools Profiler

    1. Install the React DevTools extension.
    2. Open the Profiler tab and check “Record why each component rendered during profiling” in settings.
    3. Start recording, perform the slow interaction, and stop.
    4. Analyze the Flamegraph and Ranked Chart:
      • Yellow/Orange bars: Components that took a long time to commit.
      • Render Reasons: Hover over components to see why they rendered (e.g., “Props changed”, “Parent component rendered”, “Hook 3 changed”).
  2. Identify the Root Cause
    Once you have gathered profiler data, categorize the issue into one of four primary buckets:

    • Excessive/Un-necessary Render
      • Cascading renders
      • Inline reference churn
      • Unsplit Context state
    • Heavy Component Tree & DOM
      • Large lists (>100 items)
      • Heavy sync calculations
      • Heavy layout / CSS recalculations
    • Bundle Size & Loading Bottlenecks
      • Large resources like images, fonts etc.
      • Large bundle instead of multiple small bundles.
      • Unoptimized bundles
      • Heavy library imports
    • Network & Data Fetching
      • Waterfall API calls
      • Uncached requests
  3. Targeted Fix Strategies

    • Bucket A: Excessive or Unnecessary Re-Renders

      • Context Splitting: If using React Context, ensure frequently changing state is separated from static context. A single context change forces every consumer component to re-render.
      • State Colocation: Push state down as close as possible to where it is actually consumed rather than keeping it high in the component tree.
      • Stable References:
      • Wrap expensive sub-trees or pure presentational components in React.memo.
      • Pass stable callbacks using useCallback and memoize derived prop values using useMemo when passing them down to memoized children.
      • Render Diagnostics: Use tools like why-did-you-render in development environments to log unnecessary re-renders automatically to the console.
    • Bucket B: Heavy Main Thread & DOM Operations

      • List Virtualization: For rendering large datasets (hundreds or thousands of DOM nodes), implement windowing/virtualization using libraries like react-window or @tanstack/react-virtual.
      • Defer Non-Critical Updates:
      • Use useTransition to mark non-urgent state transitions (e.g., filtering a long list while typing in a search bar), keeping input typing responsive.
      • Use useDeferredValue for values that update too frequently.
      • Offload Heavy Calculations: Move complex synchronous computations (data transformations, sorting) out of the render loop into Web Workers using comlink or memoize them with useMemo.
    • Bucket C: Bundle Size & Loading Bottlenecks

      • Route & Component Code Splitting: Use React.lazy() and Suspense or framework-level dynamic imports (e.g., Next.js dynamic()) to split code by route or non-critical modal/tab component.
      • Analyze Bundle Dependencies: Use source-map-explorer or webpack-bundle-analyzer to identify large packages (e.g., full lodash imports, heavy date/charting libraries) and swap them for lighter alternatives or tree-shakeable imports.
      • Asset Optimization: Ensure images are lazily loaded, modern formats (.webp, .avif) are served, and fonts are preloaded.
    • Bucket D: Data Fetching Latency

      • Eliminate API Waterfalls: Shift sequential useEffect fetches into parallel requests or move data fetching to the server level (Server Components / SSR loaders).
      • Client Caching & Stale-While-Revalidate: Use structured data-fetching solutions like @tanstack/react-query or SWR to handle caching, background updating, and optimistic UI updates.
  4. Verification & Prevention\

    1. Re-profile: Compare production-build performance traces before and after changes to verify measurable improvement (aim for lower Total Blocking Time and improved INP).
    2. Automate with Lighthouse / CI: Add Lighthouse CI checks to pull requests to prevent performance regressions on critical user journeys.
    3. Field Data Monitoring: Integrate Real User Monitoring (RUM) tools like Sentry, Datadog, or Vercel Analytics to track production Core Web Vitals (INP, LCP, CLS) across diverse device types and network conditions.

30. How would load large list as react component?

Section titled “30. How would load large list as react component?”

To load a large list efficiently in a React component, you should avoid rendering all items at once. Instead, use virtualization (windowing) or pagination/infinite scroll so only a small subset of items is in the DOM at any time.

Core techniques

  1. Virtualization / Windowing (best for very large lists)
    Virtualization renders only the items currently visible in the viewport plus a small buffer, keeping the DOM size constant even for tens of thousands of rows.

    Popular libraries:

    • react-window – lightweight, focused on lists and grids.
    • react-virtualized – older, more feature-rich but heavier.
    • tanstack/virtual – framework-agnostic virtualization that works well with React.

    Basic pattern with react-window:

    import { FixedSizeList as List } from 'react-window';
    function LargeList({ items }) {
    const Row = ({ index, style }) => (
    <div style={style}>
    {items[index].label}
    </div>
    );
    return (
    <List
    height={600} // visible height in px
    itemCount={items.length} // total items
    itemSize={35} // height of each row in px
    width="100%"
    >
    {Row}
    </List>
    );
    }

    This way, even if items has 100,000 entries, only ~20–30 DOM nodes exist at once.

  2. Pagination
    Load and render data in pages (e.g., 20–50 items per page). This reduces initial render cost and network load if data comes from an API.

    Simple client-side pagination example:

    function PaginatedList({ items, pageSize = 20 }) {
    const [page, setPage] = React.useState(0);
    const totalPages = Math.ceil(items.length / pageSize);
    const start = page * pageSize;
    const pageItems = items.slice(start, start + pageSize);
    return (
    <>
    <ul>
    {pageItems.map(item => (
    <li key={item.id}>{item.label}</li>
    ))}
    </ul>
    <div>
    <button disabled={page === 0} onClick={() => setPage(p => p - 1)}>
    Prev
    </button>
    <span> Page {page + 1} of {totalPages} </span>
    <button disabled={page >= totalPages - 1} onClick={() => setPage(p => p + 1)}>
    Next
    </button>
    </div>
    </>
    );
    }

    For server-side data, you’d fetch ?page=${page}&size=${pageSize} instead of slicing a local array.

  3. Infinite scroll
    Similar to pagination but automatically loads more items as the user scrolls near the end. This keeps the initial render small while giving a seamless experience.

    Typical approaches:

    • Use a library like react-infinite-scroll-component.
    • Implement your own using IntersectionObserver on a sentinel element at the bottom of the list.

    Conceptually:

    1. Render first N items.
    2. When the sentinel becomes visible, append the next batch.
    3. Repeat until all data is loaded.

    This avoids rendering everything at once but can still grow the DOM over time, so combine with virtualization if the list can become very large.

Practical recommendations

  • < 500 items, simple UI: plain .map() in a component is usually fine.
  • 500–5,000 items: consider pagination or infinite scroll, virtualization if scrolling performance matters.
  • > 5,000 items or frequent updates: use virtualization (react-window / tanstack/virtual) as the default approach.
  • Always give each list item a stable key (e.g., item.id) to help React reconcile efficiently.

31. What is hydration and hydration mismatch in React?

Section titled “31. What is hydration and hydration mismatch in React?”

What is hydration in React?

Hydration is the process by which React converts server-rendered (or statically pre-rendered) HTML into a fully interactive client side application by attaching event handlers and initializing state without recreating the existing DOM nodes.

In a typical Server-Side Rendering (SSR) setup:

  • The server renders your React components to HTML and sends that HTML to the browser.
  • The browser displays this HTML immediately, so the user sees content quickly.
  • Once the JavaScript bundle loads, React “hydrates” that HTML. It walks the existing DOM tree, matches it against your component tree, and attaches event listeners and state.

Think of the server HTML as a “dry” skeleton, hydration is React pouring in the “water” of interactivity (events, state, effects) so the page becomes alive.

Hydration Mismatch

A hydration mismatch (or hydration error) happens when the HTML that React generates on the client during its first render does not exactly match the HTML that was rendered on the server.

Because hydration assumes the two trees are identical, any difference means React cannot safely reuse the server DOM as-is. When a mismatch is detected, React:

  • Logs a warning/error showing the difference.
  • Discards the mismatched part of the server-rendered tree (from the nearest Suspense boundary downward).
  • Re-renders that part from scratch on the client.

This can cause visible issues like:

  • Content flickering or shifting after load.
  • Event handlers not working until after re-render.
  • Inconsistent UI between initial paint and interactive state.

Common causes of hydration mismatches

Typical reasons the server and client renders differ include:

  • Using browser-only APIs during render
    Accessing window, localStorage, document, or similar in code that runs on the server (e.g., in Next.js Server Components or during SSR) will produce different output on server vs client.

  • Non-deterministic rendering
    Using Math.random(), Date.now(), or other values that change between server and client renders leads to different HTML.

  • Conditional rendering based on environment
    Rendering different content depending on whether code is running on the server or client (e.g., typeof window === 'undefined' checks that change the rendered JSX).

  • HTML structure differences
    Invalid HTML nesting (e.g., <div> inside <p>) that the browser auto-corrects differently than React expects can also trigger mismatches.

How to avoid or fix hydration mismatches

Common strategies:

  • Move browser-only logic to effects or client components
    In frameworks like Next.js, put code that uses window, localStorage, etc. inside useEffect or mark the component with "use client" so it only runs in the browser.

  • Ensure render output is deterministic
    Avoid random values or timestamps in the rendered markup unless they’re generated in a way that’s consistent between server and client (or deferred to after hydration).

  • Use suppressHydrationWarning carefully
    For known, safe one-off differences (like a timestamp that only affects text content), React allows suppressHydrationWarning on specific elements, but this should be used sparingly and only when you understand the risk.

  • Check component boundaries
    Make sure Server Components and Client Components are separated correctly and that anything needing browser APIs is not rendered during the server pass.

32. How would you diagnose a hydration mismatch?

Section titled “32. How would you diagnose a hydration mismatch?”

Diagnosing a React hydration mismatch comes down to pinpointing what rendered differently on the server versus the client during the initial render pass.

When a hydration error occurs, React compares the pre-rendered HTML sent from the server with the initial Virtual DOM tree generated in the browser. If there is a discrepancy, React throws a warning (or error in production/newer versions) and falls back to client-side rendering for that section.

  1. Analyze the Developer Console Output
    In React 18 and Next.js/SSR environments, React prints detailed warning logs in development. Look for messages like:

    Warning: Text content did not match. Server: "Hello Server" Client: "Hello Client"
    or
    Hydration failed because the initial UI does not match what was rendered on the server.

    The browser console will show the exact component tree where the mismatch started. Inspect the highlighted element and look for the dynamic props, text nodes, or conditionals inside it.

  2. Identify Common Hydration Mismatch Culprits
    Most hydration errors stem from a few predictable patterns:

    • Non-Deterministic Data (Dates, Timestamps, Random Values)
      Using functions like Date.now(), new Date().toLocaleTimeString(), or Math.random() during render causes the server and client values to differ.

      Move non-deterministic calculations into useEffect or state that updates only on the client

    • Browser-Only Globals (window, localStorage, navigator)
      Accessing window.innerWidth, localStorage, or user-agent detection directly in the render body means the server renders one branch (or undefined/fallback) while the client renders another.

      Access browser globals inside a useEffect hook or wrap the component in a client-only dynamic import.

    • Invalid HTML Nesting (SSR Engine Auto-Correction) Browsers automatically fix invalid HTML tags when parsing raw server HTML. For instance, placing a <div> inside a <p> tag causes the browser to automatically split the <p> tag into two. When React attempts to hydrate, the DOM structure is already altered.

      <p><div>Content</div></p>
      <table><div>Content</div></table>
    • Server/Client Feature Flags & Auth States Rendering different components based on cookies or auth tokens that are missing or different between the SSR request and client state.

  3. Debugging Techniques
    A. Isolate with the React Developer Tools

    • Open React DevTools in your browser.
    • Search for the component identified in the console stack trace.
    • Inspect the props and state during initial render versus after hydration.

    B. Temporary Suppress Warnings vs. Clean Fix
    If a component must render client-only dynamic content and you accept a brief text shift, suppress the warning on specific elements:

    <span suppressHydrationWarning>{new Date().getFullYear()}</span>

    C. Create a useIsMounted or useMounted Hook
    For components dependent on client-side state (like dark mode or localStorage), use a mounted hook to delay client rendering until after initial hydration complete:

    import { useEffect, useState } from 'react';
    export function useIsMounted() {
    const [isMounted, setIsMounted] = useState(false);
    useEffect(() => {
    setIsMounted(true);
    }, []);
    return isMounted;
    }
    // Usage inside component:
    const isMounted = useIsMounted();
    if (!isMounted) return <Skeleton />;
    return <ClientOnlyContent />;

33. Explain different types of rendering mechanism in React and their use?

Section titled “33. Explain different types of rendering mechanism in React and their use?”

React applications support several rendering strategies, each suited to different performance, SEO, and interactive needs.

Mechanism Description Render Location Ideal Use Cases
CSR (Client-Side Rendering) HTML skeleton sent; browser downloads JS bundle to render content. Browser Internal dashboards, gated SaaS platforms, highly dynamic interactive apps.
SSR (Server-Side Rendering) HTML generated on the server per user request with dynamic data. Server (on request) E-commerce product pages, news feeds, content requiring personalized SEO.
SSG (Static Site Generation) HTML generated at build time and cached globally on a CDN. Server (at build time) Documentation sites, blogs, marketing landing pages, help centers.
ISR (Incremental Static Regeneration) Rebuilds individual static pages in the background without a full rebuild. Server (on background revalidation) Large-scale content platforms, broad e-commerce catalogs, news portals.
RSC (React Server Components) Components execute on the server and stream UI primitives without shipping JS. Server (streaming to client) Modern full-stack React applications (e.g., Next.js App Router).

Deep Dive into Each Strategy

  1. Client-Side Rendering (CSR)

    In traditional CSR apps (like classic Create React App or Vite React setups), the server serves a bare HTML shell containing a script tag. The browser downloads the Javascript, executes React, pulls data via API calls, and updates the DOM.

    • Pros: Smooth page transitions after initial load, reduced server execution burden (handled by the client).
    • Cons: Slower initial page load (First Contentful Paint), poor SEO default performance without workarounds.
    • Best Used For: Private dashboards, account settings, or apps where SEO is unnecessary and user interaction dominates.
  2. Server-Side Rendering (SSR)

    With SSR (supported by frameworks like Next.js and Remix), every incoming HTTP request causes the server to execute React, fetch fresh data, and compile the full HTML markup before sending it to the browser. The browser then hydrates the HTML to attach event listeners.

    • Pros: Excellent for real-time SEO, fast First Contentful Paint (FCP), guaranteed fresh data on every request.
    • Cons: Higher server load/costs, potential delay in Time to First Byte (TTFB) while waiting for server data fetching.
    • Best Used For: Highly dynamic public pages like live inventory views, personalized user feeds, or personalized dashboards needing SEO.
  3. Static Site Generation (SSG)

    SSG builds every single route into flat HTML and JSON files during the continuous integration / build step. These pre-built files are deployed directly to a Content Delivery Network (CDN) edge.

    • Pros: Blazing fast load times, minimal server costs, maximum reliability under high traffic spikes.
    • Cons: Long build times for large sites; content remains stale until a new build and deployment occur.
    • Best Used For: Marketing sites, blogs, portfolios, documentation hubs, and privacy policies.
  4. Incremental Static Regeneration (ISR)

    Popularized by Next.js, ISR combines SSG with dynamic cache revalidation. Pages are generated statically at build time, but when a revalidation interval expires (e.g., revalidate: 60), the next incoming request triggers a background regeneration without blocking the user.

    • Pros: Fast CDN speeds like SSG, but with scalable updates without redeploying the entire website.
    • Cons: First visitor after cache expiration gets stale content while the background rebuild executes; non-trivial state consistency across distributed deployments.
    • Best Used For: E-commerce sites with thousands of products, news archives, and large multi-author publication platforms.

    Read more

  5. Streaming SSR and Partial Hydration (advanced)

    • Streaming SSR:
      The server streams HTML chunks to the client as they’re ready, instead of waiting for the entire page to render. This improves perceived load time, especially for large pages.

    • Partial hydration / Islands architecture:
      Only certain interactive “islands” of the page are hydrated with JavaScript; the rest remains static HTML. This reduces JS cost and improves performance.

  6. React Server Components (RSC)

    RSC splits component rendering at the architecture level. Server Components run exclusively on the server can directly query databases or access secrets and send serialized UI data to the browser with zero added JavaScript bundle size. Client Components handle traditional interactive state (useState, useEffect).

    • Pros: Significantly smaller JavaScript bundles, fast rendering pipeline, direct access to backend resources directly inside the component tree.
    • Cons: Requires specialized framework routing (e.g., Next.js App Router, Waku), stricter mental model around server/client boundaries.
    • Best Used For: Modern full-stack React projects seeking optimal performance alongside server-side resource fetching.

34. Difference between Server and Client components?

Section titled “34. Difference between Server and Client components?”

Server Components and Client Components (popularized by React 18+ and Next.js App Router) split modern UI rendering based on where the code executes and what resources it accesses.

Core Comparison

Feature Server Components (RSC) Client Components
Execution Environment Runs strictly on the server during build time or request time. Prerendered on server, then hydrated and executed in the browser.
Bundle Size Impact 0 KB JavaScript sent to browser. Code stays on the server. Adds to the JavaScript bundle sent to the client.
Direct Backend Access Direct access to databases, file systems, microservices, and API keys. Must fetch data via network requests (APIs/Server Actions).
Interactivity & State None. Cannot use useState, useEffect, or event listeners (onClick). Full interactivity (useState, useEffect, onClick, custom hooks).
Browser APIs No access to window, document, localStorage, or DOM APIs. Full access to browser APIs (window, localStorage, navigator, etc.).
Opt-in Directive Default in modern frameworks like Next.js App Router. Explicitly declared using 'use client' at the top of the file.

Key Concepts

  1. Server Components

    Server Components allow you to write UI that fetches data directly on the server without sending the rendering logic or heavy dependencies to the browser.

    • Best used for: Data fetching, accessing backend resources, static UI sections (layouts, sidebars, text content), and keeping sensitive credentials (API tokens) off the client.
    • Example:
    // Runs entirely on the server
    import db from '@/lib/db';
    export default async function UserProfile({ userId }: { userId: string }) {
    const user = await db.user.findUnique({ where: { id: userId } });
    return (
    <div>
    <h1>{user.name}</h1>
    <p>{user.email}</p>
    </div>
    );
    }
  2. Client Components

    Client Components enable traditional client-side interactivity. While they are prerendered into static HTML on the server during the initial page load, their client JS executes in the browser to enable interactivity (hydration).

    • Best used for: Event listeners (onClick, onChange), state management (useState, useReducer), lifecycle hooks (useEffect), and browser-only APIs.
    • Example:
    'use client'; // Opts this component and its imports into client execution
    import { useState } from 'react';
    export default function LikeButton() {
    const [likes, setLikes] = useState(0);
    return (
    <button onClick={() => setLikes(likes + 1)}>
    Likes: {likes}
    </button>
    );
    }

How They Work Together

  • Interleaving: Server Components can import and render Client Components directly.
  • Passing Data: Server Components pass data down to Client Components as standard props (props must be JSON-serializable).
  • Children Pattern: To render a Server Component inside a Client Component, pass the Server Component as a children prop rather than importing it directly into the client file.

35. How would you solve data fetching waterfall across nested components?

Section titled “35. How would you solve data fetching waterfall across nested components?”

A data fetching waterfall occurs when nested components trigger network requests sequentially rather than in parallel. Meaning Child B cannot start fetching its data until Parent A finishes fetching its data and renders.

Key Strategies to Eliminate Waterfalls

  1. Parallelize at the Route / Server Level (Server Components / Framework Loaders)

    If you are using modern React (Next.js App Router, Remix, or React Server Components), move data fetching to the server level or route level.

    • React Server Components (RSC): Fetch data in parallel on the server using Promise.all().
    • Route Loaders (Remix / React Router 6.4+): Execute route-level loaders concurrently before components start rendering.
    // Fetch parallel data at the top/server layer rather than nesting hooks in children
    async function ProfilePage({ userId }: { userId: string }) {
    const [user, posts] = await Promise.all([
    getUser(userId),
    getUserPosts(userId)
    ]);
    return <Profile user={user} posts={posts} />;
    }
  2. Component-Level Preloading (Data Preloading Patterns)

    If data fetching must stay on the client (e.g., standard Client-Side Rendering with useEffect or React Query), trigger the network request before or as soon as the parent starts loading, rather than waiting for child components to mount.

    // Define preloading function
    function preloadUserPosts(userId: string) {
    // Initiates request and caches promise/result immediately
    queryClient.prefetchQuery({ queryKey: ['posts', userId], queryFn: () => fetchPosts(userId) });
    }
    function ParentComponent({ userId }: { userId: string }) {
    // Preload child data immediately when parent renders/fetches
    preloadUserPosts(userId);
    const { data: user } = useQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId) });
    return user ? <ChildComponent userId={userId} /> : <Loading />;
    }
  3. Render-as-You-Fetch with React Suspense

    Instead of Fetch-on-Render (fetching in useEffect after mount), adopt Render-as-You-Fetch. Fire requests immediately upon interaction or route change, then pass the pending promise to <Suspense>.

    • Suspense boundaries allow sibling or nested trees to render up to the boundary without blocking the whole UI.
    • React starts fetching data and rendering the component tree concurrently.
  4. GraphQL / Relay Fragment Colocation

    If using GraphQL, components define their data requirements via fragments, but the root route compiles these fragments into a single GraphQL query.

    # Parent query fetches data for all nested children in 1 roundtrip
    query UserProfileQuery($id: ID!) {
    user(id: $id) {
    name
    ...UserHeader_user
    ...UserFeed_user
    }
    }

Summary

Strategy Ideal Use Case Pros
Server Components / Route Loaders Next.js, Remix, SSR apps Solves waterfalls completely with 0 client JS overhead.
Promise.all / Parallel Fetching Single component needing multiple resources Simple, straightforward JavaScript.
Prefetching / React Query Single-Page Apps (CSR) Keeps component-level encapsulation without layout blocking.
GraphQL Fragments (Relay) Complex, deeply nested component trees Solves waterfall while preserving component-data colocation.

36. What are the cons of using React context?

Section titled “36. What are the cons of using React context?”

While React Context is a great built-in solution for passing global data through a component tree without manually passing props at every level, it introduces several trade-offs when overused or applied to the wrong use cases.

Core Disadvantages & Drawbacks

  • Performance & Re-render Issues Every component consuming a context will automatically re-render whenever the context value object changes—even if that component only uses a small property of the state that didn’t change. Unlike dedicated state management libraries (like Zustand or Redux Toolkit), Context lacks fine-grained selectors out of the box.
  • Architectural & Component Coupling Components that rely directly on context become tightly coupled to that specific provider hierarchy. This makes them significantly harder to reuse elsewhere in your application, isolate in component libraries, or render independently in unit tests without wrapping them in context mock providers.
  • Provider Nesting (“Provider Hell”) As an application grows, storing different slices of state (theme, auth, user settings, notifications, feature flags) in separate contexts can result in deeply nested component trees in your root component (<AuthProvider><ThemeProvider><SettingsProvider>...).
  • Lack of DevTools & Debugging Support Unlike Redux, React Context doesn’t offer built-in action logging, state travel/time travel, or clear state diffs. Debugging where a state change originated or tracking complex state updates across multiple providers can quickly become difficult.
  • Poor Suitability for High-Frequency State Context is designed for low-frequency updates (like theme changes, current user data, or UI locale). It is inefficient for fast, high-frequency updates—such as mouse position tracking, complex form inputs, or rapid real-time updates—due to the cascade of re-renders.

When to Use Context vs. Alternatives

Feature / Use Case React Context External State Management (Zustand, Redux)
Best For Low-frequency, app-wide values (Theme, Auth, Locale) Complex, high-frequency, or deeply nested state
Re-render Optimization Coarse (Re-renders all consumers) Fine-grained (Selector-based subscriptions)
Setup Complexity Very Low (Native to React) Low to Moderate
Testing Isolation Requires Provider wrappers Easy to mock store state directly

37. How would you design a re-usable component in React supporting both controlled and un-controlled behavior?

Section titled “37. How would you design a re-usable component in React supporting both controlled and un-controlled behavior?”

To design a React component that seamlessly supports both controlled and uncontrolled states, follow the pattern used by standard HTML form elements (like <input>) and UI libraries such as Radix UI or React Spectrum.

The key pattern is internal state fallback: managing an internal state when the component is uncontrolled, while deferring entirely to the value prop when controlled.

  1. Custom Hook (useControllableState)

    Creating a dedicated hook keeps the state synchronization logic clean and reusable across multiple components.

    import { useState, useCallback, useRef } from 'react';
    interface UseControllableStateProps<T> {
    value?: T;
    defaultValue?: T;
    onChange?: (value: T) => void;
    }
    export function useControllableState<T>({
    value: valueProp,
    defaultValue,
    onChange,
    }: UseControllableStateProps<T>): [T, (nextValue: T | ((prev: T) => T)) => void] {
    // Determine if the component is in controlled mode
    const isControlled = valueProp !== undefined;
    // Track the internal state for uncontrolled mode
    const [internalValue, setInternalValue] = useState<T>(defaultValue as T);
    // Store onChange in a ref to avoid stale closures in callbacks
    const onChangeRef = useRef(onChange);
    onChangeRef.current = onChange;
    // Return controlled value if provided, otherwise internal state
    const value = isControlled ? valueProp : internalValue;
    const setValue = useCallback(
    (nextValue: T | ((prev: T) => T)) => {
    // Calculate next value if a updater function was passed
    const setter = (prev: T) => {
    const resolved = typeof nextValue === 'function'
    ? (nextValue as (prev: T) => T)(prev)
    : nextValue;
    // Notify parent callback in both controlled and uncontrolled modes
    onChangeRef.current?.(resolved);
    return resolved;
    };
    if (!isControlled) {
    setInternalValue(setter);
    } else {
    // In controlled mode, trigger callback using current valueProp
    const resolved = typeof nextValue === 'function'
    ? (nextValue as (prev: T) => T)(valueProp)
    : nextValue;
    onChangeRef.current?.(resolved);
    }
    },
    [isControlled, valueProp]
    );
    return [value, setValue];
    }
  2. Implementing the Component

    Here is a reusable ToggleSwitch built with the hook:

    import React from 'react';
    export interface ToggleProps {
    value?: boolean; // Controlled prop
    defaultValue?: boolean; // Uncontrolled prop
    onChange?: (checked: boolean) => void;
    label?: string;
    }
    export const Toggle: React.FC<ToggleProps> = ({
    value,
    defaultValue = false,
    onChange,
    label = 'Toggle',
    }) => {
    const [isChecked, setIsChecked] = useControllableState({
    value,
    defaultValue,
    onChange,
    });
    const handleToggle = () => {
    setIsChecked(!isChecked);
    };
    return (
    <button
    type="button"
    role="switch"
    aria-checked={isChecked}
    onClick={handleToggle}
    className={`toggle-btn ${isChecked ? 'is-active' : ''}`}
    >
    {label}: {isChecked ? 'ON' : 'OFF'}
    </button>
    );
    };
  3. Usage Examples

    Uncontrolled Usage

    The parent does not pass value. State is managed internally, optional default set via defaultValue.

    // Self-contained component
    <Toggle defaultValue={true} onChange={(val) => console.log('Toggled:', val)} />

    Controlled Usage

    The parent passes value and controls updates via onChange.

    const Parent = () => {
    const [isOn, setIsOn] = useState(false);
    return (
    <Toggle
    value={isOn}
    onChange={(nextVal) => setIsOn(nextVal)}
    />
    );
    };

Key Design Principles to Maintain

  1. Undefined Check (value !== undefined): A component is controlled if and only if value is explicitly passed (not null or undefined).
  2. Warn Against Switching Modes: Switching a component from controlled (value={str}) to uncontrolled (value={undefined}) during its lifecycle causes bugs. In development, throw a warning if isControlled changes dynamic state.
  3. Always Trigger onChange: Fire the onChange event callback regardless of whether the component is controlled or uncontrolled so consumers can subscribe to state changes in both modes.

38. How do you manage Cache in React application?

Section titled “38. How do you manage Cache in React application?”

Implementing Cache-Control for a React application depends on whether you are managing static assets (HTML, JavaScript, CSS, images) or dynamic API requests.

  1. Caching Static Assets (Build Files)

    For single-page applications (SPAs) built with React (Vite, Create React App, Webpack), static assets require two distinct caching strategies:

    A. The Entrypoint (index.html) — Never Cache

    The index.html file must never be aggressively cached. It references bundled JS and CSS assets using content hashes (e.g., main.a8f92c.js). If a browser caches index.html, users won’t receive app updates until their local cache expires.

    Header for index.html:

    Cache-Control: no-cache, no-store, must-revalidate
    Pragma: no-cache
    Expires: 0
    • no-cache: Tells the browser to validate with the server before using a cached copy.
    • no-store: Prevents storing the HTML file anywhere (optional, but ensures complete cache bypass).

    B. Hashed Assets (/static/*, .js, .css, media) — Cache Forever

    Modern bundlers append unique hash strings to filenames whenever code changes. Since a filename change guarantees fresh content, these files can be cached safely for up to a year (the maximum allowed by HTTP specs).

    Header for fingerprinted assets:

    Cache-Control: public, max-age=31536000, immutable
    • max-age=31536000: Caches the file for 1 year ($365 \times 24 \times 60 \times 60$ seconds).
    • immutable: Tells the browser the file content will never change, preventing conditional revalidation requests (304 Not Modified) on page reloads.
  2. Web Server Configurations

    Set these headers at your web server or CDN level rather than inside React.

    Nginx Configuration

    server {
    listen 80;
    server_name example.com;
    root /var/www/react-app/dist;
    # HTML entry point - revalidate always
    location / {
    try_files $uri $uri/ /index.html;
    add_header Cache-Control "no-cache, no-store, must-revalidate";
    }
    # Hashed static assets - cache immutably for 1 year
    location ~* \.(?:css|js)$ {
    expires 1y;
    add_header Cache-Control "public, max-age=31536000, immutable";
    }
    # Media files (images, fonts)
    location ~* \.(?:jpg|jpeg|gif|png|ico|svg|woff2)$ {
    expires 1M;
    add_header Cache-Control "public, max-age=2592000";
    }
    }

    You can implement similar configuration for other server like Vercel, Netlify etc.

  3. Caching API Responses inside React

    To cache dynamic data fetches inside your React application (avoiding duplicate network requests across components), use dedicated data-fetching libraries like TanStack Query (React Query) or RTK Query.

    Example with TanStack Query

    import { useQuery } from '@tanstack/react-query';
    function UserProfile({ userId }: { userId: string }) {
    const { data, isLoading } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json()),
    staleTime: 1000 * 60 * 5, // Data remains fresh for 5 minutes (no background refetch)
    gcTime: 1000 * 60 * 30, // Unused data remains in memory for 30 minutes
    });
    if (isLoading) return <div>Loading...</div>;
    return <div>{data.name}</div>;
    }

    Native fetch with Next.js (App Router / SSR React)

    If using React in a server context (e.g., Next.js), control headers via fetch options:

    // Force-cache response (default)
    const staticData = await fetch('https://api.example.com/data', {
    cache: 'force-cache',
    });
    // Revalidate every 60 seconds (ISR)
    const revalidatedData = await fetch('https://api.example.com/data', {
    next: { revalidate: 60 },
    });

Summary

Asset Type Header Value Purpose
index.html no-cache, no-store, must-revalidate Ensures users instantly receive new application builds
Bundled JS/CSS (/assets/*.js) public, max-age=31536000, immutable Maximizes performance via long-term CDN/browser cache
API Requests Use staleTime via TanStack Query Minimizes redundant network requests on client re-renders