React JS
Interview Questions with Answers

1. How does React’s one-way data flow differ from two-way binding frameworks?

Answer:
React flows data one way: parents pass data down via props, and children send changes up by calling callback props. State lives in a single, clear owner. In two-way binding frameworks, a UI change can directly update the model and the model can immediately update the UI, which can hide where a change originated.
Why it matters: One-way flow makes apps easier to reason about, debug, and optimize.
Example (controlled input with explicit “flow up”):

function NameForm() {
const [name, setName] = React.useState("");
return (
   <input value={name} onChange={e => setName(e.target.value)} />
);
}

 

Crack your next React interview with confidence

2. Why is immutability important in React state updates?

Answer:
React relies on shallow comparisons to know what changed. If you mutate state in place, React may not see a new reference and can skip necessary updates. Immutable updates create new references, making change detection fast and reliable. It also prevents accidental side effects and enables optimizations like React.memo and time-travel debugging.
Example (don’t mutate, create new):

// BAD: mutates in place
setTodos(t => { t.push({ id: 3, text: "Study" }); return t; });
// GOOD: new array reference
setTodos(t => [...t, { id: 3, text: "Study" }]);

3. How does React batch state updates, and when might it not batch them?

Answer:
React batches multiple setState/setX calls that happen in the same turn of the event loop to produce a single render. Since React 18, automatic batching applies to updates inside React events and most async contexts (like setTimeout, promises, and async handlers).
It may not batch when you explicitly opt out using flushSync, or when updates occur in separate event loop turns.
Example (batched into one render):

function Counter() {
const [a, setA] = React.useState(0.;
const [b, setB] = React.useState(0.;
function incrementTogether() {
   setA(x => x + 1.;
   setB(x => x + 1.; // both batched -> one render
}
return <button onClick={incrementTogether}>{a + b}</button>;
}

Opt-out example (force immediate render each time):

import { flushSync } from 'react-dom';
function click() {
flushSync(() => setA(x => x + 1.);
flushSync(() => setB(x => x + 1.); // two renders
}

4. What is the difference between reconciliation and diffing in React?

Answer:
Diffing is the algorithm that compares the previous element tree with the next one and figures out the minimal set of changes.
Reconciliation is the whole process of updating the UI: run render functions, diff the trees, decide which components to reuse or remount, and then commit changes to the DOM (and run effects).
So, diffing is a part of reconciliation; reconciliation is the end-to-end update.
Example (key change forces remount during reconciliation):

// Changing key tells the reconciler: treat as a new component.
<TodoList key={filter} filter={filter} />

5. How does React decide whether to re-render a component?

Answer:
A component re-renders when its state/props/context change or when its parent re-renders and passes new references/values. React then runs the component function again to produce the next UI. You can avoid unnecessary re-renders with React.memo (for components) and by passing stable props (e.g., memoized callbacks/values). Note: useMemo/useCallback don’t prevent renders by themselves; they help keep prop references stable for memoized children.
Example (prevent a child’s unnecessary re-renders):

const Child = React.memo(function Child({ onSave }) {
return <button onClick={onSave}>Save</button>;
});
function Parent() {
const onSave = React.useCallback(() => { /* ... */ }, []);
return <Child onSave={onSave} />;
}

6. Why JSX expressions must return a single root element?

Answer:
A component must return one tree root because JSX compiles to a single React.createElement call at the top level. When you need siblings, wrap them in a fragment so React still receives a single root node.
Example (using a fragment):

function Card() {
return (
   <>
     <h3>Title</h3>
     <p>Body</p>
   </>
);
}

7. Can you write React components without JSX? If yes, how?

Answer:
Yes. JSX is syntax sugar for React.createElement. You can call it directly. This is useful in environments without a build step or for very dynamic UIs.
Example (no JSX):

function Hello() {
return React.createElement('h1', { className: 'greet' }, 'Hello');
}

8. How does React handle event delegation under the hood?

Answer:
React uses event delegation: it sets up a small number of listeners on the root container and dispatches events down the component tree using a synthetic event system. This reduces memory usage and ensures consistent behavior across browsers. It also means events work for elements added later without reattaching handlers.
Example (one handler works for many items):

function List({ items, onSelect }) {
return (
   <ul onClick={e => {
     const li = e.target.closest('li');
     if (li) onSelect(li.dataset.id);
   }}>
     {items.map(it => <li key={it.id} data-id={it.id}>{it.label}</li>)}
   </ul>
);
}

9. Why should you avoid using array index as a key in lists?

Answer:
Keys give React a stable identity to match items between renders. Using the index breaks when the list is reordered, filtered, or items are inserted/removed: React can mismatch items, causing wrong DOM updates or state “jumping” between rows. Use a stable unique id instead.
Example (bad vs good):

// BAD: key={i}
items.map((it, i) => <Row key={i} item={it} />);
// GOOD: key={it.id}
items.map(it => <Row key={it.id} item={it} />);

10. How does React handle attribute differences between HTML and JSX?

Answer:
JSX names map to DOM properties and use camelCase for most attributes and events:
class → className
for (label) → htmlFor
event handlers use camelCase: onclick → onClick
style is an object with camelCased CSS properties
Booleans can be written as props (<input disabled />)
Some attributes are slightly different (tabIndex, readOnly, autoFocus, defaultValue)
Example:

<label htmlFor="email" className="lbl">Email</label>
<input id="email" defaultValue="user@site.com" autoFocus />
<div style={{ backgroundColor: 'lavender', paddingTop: 8 }}>
Styled box
</div>
<button onClick={() => alert('Hi')}>Click</button>

11. What is the order of lifecycle methods in class components during mounting?

Answer:
When a class component mounts (first time added to the DOM), React calls methods in this order:

  1. constructor() → good for initializing state and binding methods.

  2. static getDerivedStateFromProps() (rarely used) → syncs state with props before render.

  3. render() → returns JSX.

  4. componentDidMount() → runs after the component is inserted into the DOM, useful for API calls or subscriptions.
    Example:

class Demo extends React.Component {
constructor(props) {
   super(props);
   console.log("constructor");
   this.state = { count: 0 };
}
static getDerivedStateFromProps() {
   console.log("getDerivedStateFromProps");
   return null;
}
render() {
   console.log("render");
   return <h1>{this.state.count}</h1>;
}
componentDidMount() {
   console.log("componentDidMount");
}
}

Console order: constructor → getDerivedStateFromProps → render → componentDidMount

12. How can you implement shouldComponentUpdate to prevent re-renders?

Answer:
shouldComponentUpdate(nextProps, nextState) lets you decide if a component should re-render. Returning false skips the re-render, which boosts performance.
You compare current props/state with upcoming ones and allow rendering only when necessary.
Example:

class Profile extends React.Component {
shouldComponentUpdate(nextProps) {
   // Re-render only if name actually changes
   return nextProps.name !== this.props.name;
}
render() {
   console.log("rendering Profile");
   return <h2>{this.props.name}</h2>;
}
}

This avoids unnecessary re-renders when name stays the same.

13. What is the difference between componentDidMount and useEffect with []?

Answer:
componentDidMount runs once after the initial render of a class component.
useEffect(() => { … }, []) runs once after the initial render of a functional component.
The difference:
useEffect can optionally return a cleanup function (like componentWillUnmount).
useEffect is more flexible because you can run it with dependencies, not just once.
In concurrent React, effects are deferred, while componentDidMount ran immediately after mounting.
Example:

// Class
componentDidMount() {
console.log("Mounted");
}
// Hooks
useEffect(() => {
console.log("Mounted");
return () => console.log("Unmounted");
}, []);

14. What problems arise if you call setState inside render()?

Answer:
Calling setState inside render() creates an infinite loop:
render() → calls setState → triggers re-render → again calls setState.
This will freeze the app or crash with “Too many re-renders.”
Correct usage: Only update state in lifecycle methods (componentDidMount, event handlers, or effects).
Example (wrong):

render() {
this.setState({ count: this.state.count + 1 }); // infinite loop
return <p>{this.state.count}</p>;
}

15. Why was componentWillMount deprecated?

Answer:
componentWillMount was deprecated because:
It often caused side effects before mounting, which could run multiple times in async rendering.
Code like data fetching or subscriptions inside it led to bugs and inconsistent behavior.
Safer alternatives exist: constructor (for initialization) and componentDidMount (for side effects).
Example (old code):

componentWillMount() {
// Deprecated: may run multiple times
fetch("/api/data").then(...);
}

Better: Move data fetching to componentDidMount.

16. How does React handle closure traps inside hooks?

Answer:
A closure trap happens when a hook captures stale values of variables. React handles this by requiring dependencies arrays in hooks like useEffect, useCallback, and useMemo. The dependencies tell React when to recreate the closure with fresh values.
Example (trap):

function Timer() {
const [count, setCount] = React.useState(0.;
// closure trap: always logs 0
React.useEffect(() => {
   setInterval(() => console.log(count), 1000.;
}, []);
}

Fix: include count in dependencies.

React.useEffect(() => {
const id = setInterval(() => console.log(count), 1000.;
return () => clearInterval(id);
}, [count]);

17. Why is it unsafe to put hooks inside conditions or loops?

Answer:
Hooks must run in the same order on every render. If you put them inside conditions or loops, the order may change, and React can’t match up state between renders. This breaks the hook system.
Correct: Always call hooks at the top level of the component.
Example (wrong):

if (loggedIn) {
const [user, setUser] = useState(null); // breaks rules
}

Example (correct):

const [user, setUser] = useState(null);
if (!loggedIn) return <Login />;

18. What are potential pitfalls of using useEffect without dependencies?

Answer:
When you omit the dependencies array:
The effect runs after every render, which may cause:
Performance issues (expensive operations run too often).
Infinite loops if the effect updates state without conditions.
You lose control over when the effect should re-run.
Example (problem):

useEffect(() => {
setCount(c => c + 1.; // infinite loop
}); // runs on every render

Solution: Add correct dependencies or use [] for run-once behavior.

19. Why is useImperativeHandle used with forwardRef?

Answer:
Normally, refs give access to DOM nodes. useImperativeHandle lets you customize what a parent component gets when it uses a ref on a child component. This is useful when exposing only specific methods instead of the full child instance.
Example:

const CustomInput = React.forwardRef((props, ref) => {
const inputRef = React.useRef();
React.useImperativeHandle(ref, () => ({
   focus: () => inputRef.current.focus()
}));
return <input ref={inputRef} />;
});
function Parent() {
const ref = React.useRef();
return (
   <>
     <CustomInput ref={ref} />
     <button onClick={() => ref.current.focus()}>Focus Input</button>
   </>
);
}

20. How does useSyncExternalStore improve subscription handling?

Answer:
useSyncExternalStore is a hook designed for subscribing to external stores (like Redux or custom stores) in a safe way. It ensures:
React gets a consistent snapshot of the store during concurrent rendering.
Avoids tearing (where UI shows mixed old/new state).
Provides a standard API for libraries to integrate smoothly with React 18+.
Example (basic counter store):

function useCounterStore() {
return React.useSyncExternalStore(
   store.subscribe,     // subscribe to changes
   store.getSnapshot    // get current value
);
}
function Counter() {
const count = useCounterStore();
return <p>Count: {count}</p>;
}

This way, React always renders with the latest consistent state.

Stop scrolling, start preparing

21. How do you validate forms without external libraries in React?

Answer:
You can handle validation manually by:

Tracking input values in state.

Running validation logic (like regex or conditions) when the user types or submits.

Showing error messages conditionally.

Example (basic email validation):

function SignupForm() {
const [email, setEmail] = React.useState("");
const [error, setError] = React.useState("");
function handleSubmit(e) {
   e.preventDefault();
   if (!/\S+@\S+\.\S+/.test(email)) {
     setError("Invalid email address");
   } else {
     setError("");
     console.log("Submitted:", email);
   }
}
return (
   <form onSubmit={handleSubmit}>
     <input value={email} onChange={e => setEmail(e.target.value)} />
     {error && <p style={{color:"red"}}>{error}</p>}
     <button type="submit">Submit</button>
   </form>
);
}

22. How do you implement debounced input handling in React?

Answer:
Debouncing delays a function call until the user stops typing for a given time. You can implement it with setTimeout and clearTimeout.

Example:

function SearchBox() {
const [query, setQuery] = React.useState("");
React.useEffect(() => {
   const handler = setTimeout(() => {
     console.log("Searching for:", query);
   }, 500.; // 500ms delay
   return () => clearTimeout(handler);
}, [query]);
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
This prevents API calls on every keystroke and only fires after typing stops.

23. What are the differences between synthetic events and native events in React?

Answer:

React wraps browser events in SyntheticEvent for cross-browser consistency.

Synthetic events are pooled (reused for performance), meaning their properties reset after the event callback unless you call event.persist().

Native events are the raw browser events.

React manages synthetic events through delegation at the root, reducing memory use.

Example:

function Button() {
function handleClick(e) {
   console.log(e.nativeEvent);   // Native DOM event
   console.log(e.type);          // SyntheticEvent: "click"
}
return <button onClick={handleClick}>Click</button>;
}

24. How do you handle file uploads in React?

Answer:
Use an <input type=”file”>, read the file from event.target.files, and send it using FormData with fetch or Axios.

Example:

function FileUpload() {
function handleUpload(e) {
   const file = e.target.files[0];
   const formData = new FormData();
   formData.append("file", file);
   fetch("/upload", { method: "POST", body: formData });
}
return <input type="file" onChange={handleUpload} />;
}

25. How do you make a form dynamically render new input fields in React?

Answer:
Keep an array in state and map over it to render inputs. Add new fields by updating the array.

Example:

function DynamicForm() {
const [fields, setFields] = React.useState([""]);
function addField() {
   setFields([...fields, ""]);
}
return (
   <form>
     {fields.map((val, i) => (
       <input key={i} value={val}
         onChange={e => {
           const copy = [...fields];
           copy[i] = e.target.value;
           setFields(copy);
         }} />
     ))}
     <button type="button" onClick={addField}>Add Field</button>
   </form>
);
}

26. How do error boundaries differ from try/catch in React?

Answer:

try/catch works for synchronous code inside functions but doesn’t catch rendering errors.

Error boundaries are React components (componentDidCatch or getDerivedStateFromError) that catch errors in rendering, lifecycle methods, and child components.

They let you display a fallback UI instead of breaking the whole app.

Example:

class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
   return { hasError: true };
}
componentDidCatch(error, info) {
   console.error(error, info);
}
render() {
   return this.state.hasError ? <h2>Something went wrong</h2> : this.props.children;
}
}

27. What limitations exist for error boundaries (what can’t they catch)?

Answer:
Error boundaries cannot catch:

Errors inside event handlers (use try/catch there).

Errors in asynchronous code like setTimeout or Promises.

Errors during server-side rendering (SSR).

Errors in ErrorBoundary itself.

28. How do you gracefully handle API errors in React UI?

Answer:
Wrap API calls in try/catch, store error state, and display user-friendly messages instead of crashing.

Example:

function UserList() {
const [users, setUsers] = React.useState([]);
const [error, setError] = React.useState("");
React.useEffect(() => {
   async function fetchUsers() {
     try {
       const res = await fetch("/api/users");
       if (!res.ok) throw new Error("Failed to fetch users");
       setUsers(await res.json());
     } catch (err) {
       setError(err.message);
     }
   }
   fetchUsers();
}, []);
if (error) return <p>Error: {error}</p>;
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

29. What strategies exist for fallback UIs in React applications?

Answer:

Error Boundaries → show a generic error message when rendering fails.

Loading Spinners/Skeletons → show placeholders while waiting for data.

Retry Buttons → let users retry failed API calls.

Graceful Defaults → render cached data or empty states.

Route-level Fallbacks → show “Not Found” or “Offline” pages.

Example (fallback while loading):

{loading ? <p>Loading...</p> : <DataComponent />}

30. How would you debug an infinite re-render loop in React?

Answer:
Steps:

Check if you’re calling setState or useState setters inside render → move them to effects or handlers.

Look for useEffect without dependencies but setting state → add correct dependency array.

Ensure props passed to child components are memoized if they cause re-renders (useCallback, useMemo).

Use console logs to trace which state/prop keeps changing.

Example (bug):

useEffect(() => {
setCount(count + 1.; // runs after every render → infinite loop
});

Fix:

useEffect(() => {
setCount(c => c + 1.;
}, []); // run once

31. What problems does prop drilling cause in large React apps?

Answer:
Prop drilling is when you pass props through multiple “middle” components that don’t use them—just to reach a deeply nested child. This creates noisy component APIs, tight coupling, and brittle code: every intermediate layer must keep forwarding props, refactors become risky, and a small change at the top can force edits across many files. It also increases unnecessary re-renders because those middle components re-render even though they don’t use the data.

Example:

// App -> Layout -> Sidebar -> Profile needs `user`
function App(){ return <Layout user={user}/> }
function Layout({ user }){ return <Sidebar user={user}/> }
function Sidebar({ user }){ return <Profile user={user}/> } // drilling

32. Why should context be used sparingly in React?

Answer:
Context is great for global, stable values (theme, auth, locale). But frequent or large updates in context cause all consumers to re-render, even if they only use a small part. Context also hides dependencies (a component looks “prop-free” but secretly relies on a provider), which reduces reusability and makes testing/mocking trickier. Prefer props for local data flow; use context for truly cross-cutting concerns and keep its values stable.

Example tip: Put non-changing functions/objects in context; keep rapidly changing state local or in a dedicated store.

33. How does React context differ from dependency injection in Angular?

Answer:
React context is a value broadcast down a React tree. Components read a value with useContext—there’s no object construction or lifecycle managed by context itself.
Angular DI is a provider/injector system: tokens map to instances constructed and managed by the framework, with hierarchical injectors, scoping, and lifecycles. In short: React context shares values; Angular DI creates and supplies dependencies.

Example (React):

const ThemeCtx = React.createContext('light');
function Button(){ const theme = React.useContext(ThemeCtx); /* use theme */ }

34. How do you optimize context updates to avoid unnecessary re-renders?

Answer:

Split contexts so each provider carries a minimal concern (e.g., AuthContext and ThemeContext instead of one mega-context).

Memoize provider values so reference changes only when necessary:

const value = React.useMemo(() => ({ user, login }), [user, login]);
<AuthContext.Provider value={value}>{children}</AuthContext.Provider>

Keep rapidly changing state out of context; pass it down via props or colocate it.

Selector pattern / external store: expose derived slices or use a library (use-context-selector) or useSyncExternalStore to subscribe to only what’s needed.

Avoid recreating objects/functions inside providers—stabilize with useMemo/useCallback.

35. What is the difference between multiple contexts vs nested providers?

Answer:
They go together. “Multiple contexts” means separate concerns; “nested providers” is how you supply them. Using multiple small contexts (even if nested) limits the blast radius of updates—only consumers of that context re-render. The downside is visual nesting in JSX, which you can mitigate by a simple AppProviders wrapper.

Example:

function AppProviders({ children }) {
return (
   <ThemeProvider>
     <AuthProvider>
       <I18nProvider>{children}</I18nProvider>
     </AuthProvider>
   </ThemeProvider>
);
}

36. How does React handle asynchronous rendering with Fiber?

Answer:
Fiber breaks rendering into small units of work that React can pause, resume, and even abandon. In the render phase, React can yield to the browser (to keep the UI responsive), prioritize urgent updates (like typing), and continue later. The commit phase (applying changes to the DOM) remains synchronous and fast. Features like startTransition, Suspense, and deferred values sit on top of Fiber to schedule non-urgent work without blocking interaction.

Example (prioritize typing over list rendering):

import { startTransition } from "react";
function Search() {
 const [text, setText] = React.useState("");
const [results, setResults] = React.useState([]);
function onChange(e) {
   const next = e.target.value;
   setText(next);                // urgent (keystroke)
   startTransition(() => {       // non-urgent (filtering)
     setResults(expensiveFilter(next));
   });
}
return <input value={text} onChange={onChange} />;
}

37. How do you profile React app performance using React DevTools?

Answer:
Open the Profiler tab in React DevTools, click Start profiling, use your app, then Stop. Inspect each commit to see which components rendered and how long they took.

Use the Flamegraph to spot hotspots, the Ranked view to sort by cost, and toggle “Highlight updates” to visualize re-renders.

From insights, apply React.memo, stabilize props with useCallback/useMemo, split contexts, or virtualize lists. Re-profile to confirm improvements.

Optional inline profiler:

import { Profiler } from "react";
<Profiler id="Products" onRender={(...m)=>console.log(m)}>
<Products />
</Profiler>

38. What is React’s rendering priority model (lanes in React Fiber)?

Answer:
React assigns updates to lanes—priority buckets. Urgent user input (like typing/click) goes to high-priority lanes and renders quickly; non-urgent work (like transitions or background data) goes to lower-priority lanes that can be interrupted and resumed.

APIs like startTransition mark updates as transition lanes, while default state updates often go to default lanes. This model lets React keep the UI snappy by interleaving work based on importance.

Rule of thumb: urgent = normal setState; non-urgent = wrap in startTransition.

39. How do you implement infinite scrolling in React efficiently?

Answer:

Paginate on the server (or API) and fetch by page/cursor.

Use IntersectionObserver to detect when the user nears the end.

Debounce/throttle fetch triggers.

Virtualize long lists with react-window/react-virtualized to render only visible rows.

Cache loaded pages and avoid duplicate requests.

Example (IntersectionObserver + pagination):

function Feed() {
const [page, setPage] = React.useState(1.;
const [items, setItems] = React.useState([]);
const [hasMore, setHasMore] = React.useState(true);
const loaderRef = React.useRef(null);
React.useEffect(() => {
   let ignore = false;
   (async () => {
     const res = await fetch(`/api/posts?page=${page}`);
     const data = await res.json();
     if (!ignore) {
       setItems(prev => [...prev, ...data.items]);
       setHasMore(data.hasMore);
     }
   })();
   return () => { ignore = true };
}, [page]);
React.useEffect(() => {
   if (!loaderRef.current) return;
   const io = new IntersectionObserver((entries) => {
     if (entries[0].isIntersecting && hasMore) setPage(p => p + 1.;
   });
   io.observe(loaderRef.current);
   return () => io.disconnect();
}, [hasMore]);
return (
   <>
     {items.map(it => <Post key={it.id} data={it} />)}
     <div ref={loaderRef} style={{ height: 1 }} />
   </>
);
}

40. What are common memory leaks in React, and how do you prevent them?

Answer:
Common leaks:

Uncleared timers/intervals (setInterval, setTimeout).

Subscriptions/listeners (WebSocket, EventSource, window.addEventListener) not removed.

In-flight async updates that call setState after unmount.

Large references held in closures or global caches that never get released.

Prevention:

Always clean up in useEffect’s return:

useEffect(() => {
const id = setInterval(tick, 1000.;
return () => clearInterval(id); // cleanup
}, []);
For fetches, use AbortController or an “ignore flag”:
useEffect(() => {
const ctrl = new AbortController();
fetch(url, { signal: ctrl.signal }).catch(()=>{});
return () => ctrl.abort();
}, [url]);
Remove listeners on unmount:
useEffect(() => {
const handler = () => {};
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []);

Avoid unnecessary globals; store mutable data in useRef and clear it when done.

Hit the Button below to grab the guide and accelerate your front-end journey today.

41. How do you handle scroll restoration in React Router?

Answer:
React Router doesn’t restore scroll for you. Implement it yourself by listening to location changes, saving positions, and restoring on back/forward. For simple cases, scroll to top on route change; for SPA-like back/forward, cache positions by pathname or key.
Example (basic “scroll to top” on navigation):

import { useEffect } from "react";
import { useLocation } from "react-router-dom";
export function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => { window.scrollTo({ top: 0, left: 0, behavior: "instant" }); }, [pathname]);
return null;
}

Example (restore on back/forward):

import { useEffect, useRef } from "react";
import { useLocation } from "react-router-dom";
const positions = new Map(); // key -> {x,y}
export function ScrollRestoration() {
const loc = useLocation();
const keyRef = useRef(loc.key);
useEffect(() => {
const onScroll = () => positions.set(keyRef.current, { x: window.scrollX, y: window.scrollY });
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
useEffect(() => {
keyRef.current = loc.key;
const pos = positions.get(loc.key) || { x: 0, y: 0 };
window.scrollTo(pos.x, pos.y);
}, [loc.key]);
return null;
}

42. What is the difference between relative and absolute paths in nested routes?

Answer:
Absolute paths start with / and resolve from the app root.
Relative paths don’t start with / and resolve from the parent route’s path. Relative paths keep nested route trees portable and easier to move.
Example:

// Parent path: /settings
<Route path="settings" element={<Settings />}>
{/* child resolves to /settings/profile (relative) */}
<Route path="profile" element={<Profile />} />
{/* absolute (always /admin), ignores parent */}
<Route path="/admin" element={<Admin />} />
</Route>

43. How do you prefetch data before navigation in React Router?

Answer:
With data routers (v6.4+), run a route’s loader without navigating using useFetcher(). You can call fetcher.load(“/target”) on hover or when an element enters the viewport to warm the cache; when you navigate, the data is already there.
Example (prefetch on hover):

import { useFetcher, Link } from "react-router-dom";
function PrefetchLink() {
const fetcher = useFetcher();
return (
<Link
to="/products"
onMouseEnter={() => fetcher.load("/products")}
>
Products
</Link>
);
}

44. How do route loaders and actions work in React Router v6.4+?

Answer:
A loader runs before render on navigation. It fetches data for a route and exposes it via useLoaderData().
An action handles mutations for that route (form submits or programmatic submissions). Use <Form method="post"> or fetcher.submit(); the action runs, then React Router revalidates loaders to refresh data.
Example (route config & usage):

// routes
{
path: "/todos",
loader: async () => fetch("/api/todos").then(r => r.json()),
action: async ({ request }) => {
const formData = await request.formData();
return fetch("/api/todos", { method: "POST", body: formData });
},
element: <TodosPage/>
}
// component
import { useLoaderData, Form } from "react-router-dom";
function TodosPage() {
const todos = useLoaderData();
return (
<>
<ul>{todos.map(t => <li key={t.id}>{t.title}</li>)}</ul>
<Form method="post">
<input name="title" />
<button type="submit">Add</button>
</Form>
</>
);
}

45. How do you persist query parameters across navigations in React Router?

Answer:
Read current search params with useSearchParams, then merge them into the next link or navigation so they survive route changes.
Example (preserve current filters while changing page):

import { useLocation, Link } from "react-router-dom";
function PreserveSearchLink({ to, children }) {
const { search } = useLocation();
return <Link to={`${to}${search}`}>{children}</Link>;
}

Programmatic:

import { useSearchParams, useNavigate } from "react-router-dom";
function NextPageButton() {
const [sp] = useSearchParams();
const navigate = useNavigate();
return <button onClick={() => navigate({ pathname: "/list", search: sp.toString() })}>Next</button>;
}

46. How does hydration mismatch occur in SSR React apps?

Answer:
A hydration mismatch happens when the HTML rendered on the server doesn’t match what the client renders for the same initial render. Causes include time-dependent values (Date.now()), random IDs, conditionals based on window/navigator, or fetching different data on client vs server. React warns and may discard server markup.
How to avoid:
Render the same initial data on both sides (pass it via script tag or framework data APIs).
Gate client-only logic inside useEffect (runs after hydration).
Use stable IDs (e.g., useId()), or suppressHydrationWarning for truly client-only spans.
Example (client-only logic moved to effect):

import Image from "next/image";
export default function Hero() {
return (
<Image
src="/hero.jpg"
alt="Hero"
width={1600}
height={900}
priority
/>
);
}

You also allow remote domains in next.config.js for external images.

49. What is ISR (Incremental Static Regeneration) in Next.js?

Answer:
ISR lets you update static pages after deployment. You build pages once with getStaticProps, then Next.js regenerates a page in the background when a request arrives after the revalidate interval. Users see the old page until the new one is ready; subsequent requests get the fresh version.
Example:

export async function getStaticProps() {
const products = await fetch("https://api/products").then(r => r.json());
return {
props: { products },
revalidate: 60
};
}

50. How do you secure API routes in Next.js applications?

Answer:
Combine authentication, authorization, validation, and defense in depth:

  1. Authenticate: Verify sessions/JWTs. In the App Router, check auth inside Route Handlers (app/api/…/route.ts) or use Middleware to protect paths.

  2. Authorize: Enforce role/permission checks per request.

  3. Validate input: Parse and validate body/query with Zod/Yup.

  4. CSRF protection: For cookie-based sessions, include CSRF tokens on mutations.

  5. Rate limit & CORS: Throttle abusive clients; restrict origins.

  6. Avoid secrets on client: Read secrets via server-only code.
    Example (App Router — simple JWT check in a route):

// app/api/admin/route.ts
import { NextResponse } from "next/server";
import { verifyJwt } from "@/lib/auth";
export async function GET(req: Request) {
const auth = req.headers.get("authorization") || "";
const token = auth.replace(/^Bearer\s+/i, "");
const payload = await verifyJwt(token);
if (!payload || payload.role !== "admin") return NextResponse.json({ error: "Forbidden" }, { status: 403 });
return NextResponse.json({ secret: "ok" });
}

Example (Middleware to guard /api/private):

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(req: NextRequest) {
if (req.nextUrl.pathname.startsWith("/api/private")) {
const token = req.headers.get("authorization")?.replace(/^Bearer\s+/i, "");
if (!token) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
return NextResponse.next();
}
export const config = { matcher: ["/api/private/:path*"] };

51. What are compound components, and when should you use them?

Answer:
Compound components are a group of components that work together under a single parent and share implicit state through context. The parent manages the logic; children declare structure. Users compose the UI by arranging the children—no prop drilling.
When to use: Toggles, tabs, accordions, dropdowns, menus—any UI where multiple parts must coordinate but you want a clean, declarative API.
Example (Toggle with shared state):

const ToggleContext = React.createContext();
function Toggle({ children }) {
const [on, setOn] = React.useState(false);
const value = React.useMemo(() => ({ on, setOn }), [on]);
return <ToggleContext.Provider value={value}>{children}</ToggleContext.Provider>;
}
function ToggleButton() {
const { on, setOn } = React.useContext(ToggleContext);
return <button onClick={() => setOn(o => !o)}>{on ? "ON" : "OFF"}</button>;
}
function ToggleOn({ children }) {
const { on } = React.useContext(ToggleContext);
return on ? children : null;
}
function ToggleOff({ children }) {
const { on } = React.useContext(ToggleContext);
return on ? null : children;
}
// Usage
<Toggle>
<ToggleOn>Lights are on</ToggleOn>
<ToggleOff>Lights are off</ToggleOff>
<ToggleButton />
</Toggle>

52. How does the state reducer pattern improve flexibility in React components?

Answer:
The state reducer pattern lets consumers control or modify a component’s internal state transitions. The component calls a reducer you provide with (state, action) and uses the returned state. This makes the component highly customizable without forking its code.
Example (counter with pluggable reducer):

function useCounter({ initial = 0, stateReducer } = {}) {
const [state, setState] = React.useState({ count: initial });
const dispatch = (action) => {
setState((prev) => {
const changes = baseReducer(prev, action);
return stateReducer ? stateReducer(prev, changes) : changes;
});
};
return {
count: state.count,
inc: () => dispatch({ type: "inc" }),
dec: () => dispatch({ type: "dec" }),
reset: () => dispatch({ type: "reset" })
};
}
function baseReducer(state, action) {
switch (action.type) {
case "inc": return { count: state.count + 1 };
case "dec": return { count: state.count - 1 };
case "reset": return { count: 0 };
default: return state;
}
}
// Consumer customizes behavior
const reducer = (state, changes) =>
changes.count < 0 ? { count: 0 } : changes;
const c = useCounter({ stateReducer: reducer });

53. How do you implement the provider pattern in React?

Answer:
Create a context, wrap children in a Provider, and read values with useContext. The provider pattern is the backbone of sharing state/config across a subtree.
Example (AuthProvider):

const AuthContext = React.createContext();
export function AuthProvider({ children }) {
const [user, setUser] = React.useState(null);
const login = (u) => setUser(u);
const logout = () => setUser(null);
const value = React.useMemo(() => ({ user, login, logout }), [user]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth() {
const ctx = React.useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be inside AuthProvider");
return ctx;
}
// Usage
<AuthProvider>
<App />
</AuthProvider>

54. How does the hooks factory pattern work?

Answer:
A hooks factory is a function that returns a configured hook. It lets you parameterize dependencies once, then produce multiple specialized hooks.
Example (factory to create data hooks per endpoint):

function createDataHook(baseUrl) {
return function useData(resource) {
const [data, setData] = React.useState(null);
React.useEffect(() => {
let ignore = false;
(async () => {
const res = await fetch(`${baseUrl}/${resource}`);
const json = await res.json();
if (!ignore) setData(json);
})();
return () => { ignore = true; };
}, [baseUrl, resource]);
return data;
};
}
const useApiData = createDataHook("https://api.example.com");
const users = useApiData("users");

55. What is the difference between renderless components and HOCs?

Answer:
Renderless components don’t render UI themselves; they manage logic and expose it via children-as-a-function or context. You decide the markup.
HOCs wrap a component and inject props or behavior by returning a new component. They can compose logic but may add wrapper trees.
Example (renderless with function-as-children):

function Mouse({ children }) {
const [pos, setPos] = React.useState({ x: 0, y: 0 });
React.useEffect(() => {
const h = e => setPos({ x: e.clientX, y: e.clientY });
window.addEventListener("mousemove", h);
return () => window.removeEventListener("mousemove", h);
}, []);
return children(pos);
}
// Usage
<Mouse>{({ x, y }) => <div>({x}, {y})</div>}</Mouse>

Example (HOC):

const withLogger = (Comp) => (props) => {
console.log("render", Comp.name);
return <Comp {...props} />;
};

56. How do you test custom hooks in isolation?

Answer:
Use renderHook to run a hook in a test environment and assert on its returned values and effects. If the hook needs context, wrap it with a provider using the wrapper option.
Example (Testing Library + Jest):

import { renderHook, act } from "@testing-library/react";
function useCounter() {
const [n, setN] = React.useState(0);
return { n, inc: () => setN(v => v + 1) };
}
test("useCounter increments", () => {
const { result } = renderHook(() => useCounter());
act(() => result.current.inc());
expect(result.current.n).toBe(1);
});

With context:

const Ctx = React.createContext(0);
function useFromCtx(){ return React.useContext(Ctx); }
test("hook reads context", () => {
const wrapper = ({ children }) => <Ctx.Provider value={42}>{children}</Ctx.Provider>;
const { result } = renderHook(() => useFromCtx(), { wrapper });
expect(result.current).toBe(42);
});

57. How do you mock API calls in Jest for React components?

Answer:
Mock fetch (or your API client) and control resolved data/errors. For component tests, render the component and assert the UI after promises resolve.
Example (mock fetch):

test("shows users", async () => {
global.fetch = jest.fn().mockResolvedValue({
ok: true,
json: async () => [{ id: 1, name: "Alice" }]
});
render(<UsersList />);
expect(await screen.findByText("Alice")).toBeInTheDocument();
});

Example (mock Axios):

jest.mock("axios");
import axios from "axios";
axios.get.mockResolvedValue({ data: { ok: true } });

58. What’s the difference between shallow rendering and full DOM rendering in testing?

Answer:
Shallow rendering renders a component without its children. It’s fast and isolates the unit, but you may miss integration bugs.
Full DOM rendering renders the whole tree (JSDOM). It’s closer to real behavior, lets you test interactions and effects, but is slower and more coupled to implementation.
Guideline: Prefer React Testing Library with full rendering and test user-visible behavior. Use shallow only when you truly need isolation.

59. How do you test React portals?

Answer:
Render with a real portal target in the test DOM, then assert on content inside that target. Most libraries mount a root div for portals (modals, tooltips). Create it in beforeEach, clean up in afterEach.

Example:

// Component
function Modal({ open, children }) {
const el = document.getElementById("modal-root");
return open ? ReactDOM.createPortal(children, el) : null;
}
// Test
beforeEach(() => {
const root = document.createElement("div");
root.setAttribute("id", "modal-root");
document.body.appendChild(root);
});
afterEach(() => {
document.getElementById("modal-root").remove();
});
test("renders inside portal", () => {
render(<Modal open>Hi</Modal>);
expect(document.getElementById("modal-root")).toHaveTextContent("Hi");
});

60. How do you perform snapshot testing in React, and what are its pros/cons?

Answer:
Use Jest snapshots to capture a component’s rendered output and compare future runs to detect unintended changes.

Example:

import renderer from "react-test-renderer";
test("Button snapshot", () => {
const tree = renderer.create(<Button primary>Save</Button>).toJSON();
expect(tree).toMatchSnapshot();
});

Pros:

  • Quick safety net for accidental UI changes.
  • Good for stable, presentational components.

Cons:

  • Can be brittle and lead to “update snapshot” culture if overused.
  • Large snapshots hide real issues.
  • Doesn’t validate behavior—only structure.

Best practice: Keep snapshots small and focused, pair with behavior tests (clicks, API states), and avoid snapshotting huge, dynamic trees.

Note: The interview questions and answers provided on this page have been thoughtfully compiled by our academic team. However, as the content is manually created, there may be occasional errors or omissions. If you have any questions or identify any inaccuracies, please contact us at team@learn2earnlabs.com. We appreciate your feedback and strive for continuous improvement.