Advanced React Interview Questions and Answers

๐Ÿ“‹ Table of Contents โ–พ
  1. Questions & Answers
  2. 📝 Knowledge Check

⚛ Advanced React Interview Questions

This lesson targets mid-level to senior roles. Questions cover the Context API, performance optimization, advanced hooks, routing, state management patterns, portals, code splitting, and SSR. Mastering these topics shows interviewers you understand React deeply, not just superficially.

Questions & Answers

01 What is the Context API? When should you use it vs Redux?

Context The Context API provides a way to share values through the component tree without explicit prop drilling. It’s built into React and is ideal for global, infrequently changing data.

// 1. Create context
const ThemeContext = React.createContext('light');

// 2. Provide it
<ThemeContext.Provider value="dark">
  <App />
</ThemeContext.Provider>

// 3. Consume it
function Button() {
  const theme = useContext(ThemeContext);
  return <button className={theme}>Click</button>;
}

Context vs Redux:

  • Use Context for: current user/auth, theme, locale, rarely-changing global values
  • Use Redux for: complex state with many transitions, shared state across many components, need for devtools/time-travel debugging, large team projects
  • Context caveat: Every component consuming a context re-renders when the context value changes โ€” can cause performance issues if used for frequently-updating values
02 What is useReducer and when should you use it over useState?

Hooks useReducer is an alternative to useState for managing complex state logic. It follows the same pattern as Redux: you dispatch actions, and a reducer function calculates the next state.

const initialState = { count: 0, step: 1 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment': return { ...state, count: state.count + state.step };
    case 'decrement': return { ...state, count: state.count - state.step };
    case 'setStep':   return { ...state, step: action.payload };
    default: throw new Error('Unknown action: ' + action.type);
  }
}

const [state, dispatch] = useReducer(reducer, initialState);
dispatch({ type: 'increment' });

Prefer useReducer when:

  • State has multiple sub-values that change together
  • Next state depends on the previous state in complex ways
  • State transitions have clear names (actions) that should be logged or traced
  • You want to colocate state logic away from the render tree (testable pure function)
03 What is useMemo? When should you (and shouldn’t you) use it?

Performance useMemo memoizes the result of an expensive computation, recalculating only when its dependencies change. It prevents expensive recalculations on every render.

const filteredList = useMemo(() => {
  // expensive filter on thousands of items
  return items.filter(item => item.category === category);
}, [items, category]); // only re-runs when items or category changes

When TO use:

  • Genuinely expensive computations (complex sorting, filtering large datasets, heavy math)
  • When the computed value is used as a prop for a React.memo-wrapped child to prevent re-renders

When NOT to use:

  • Simple calculations โ€” the memoization overhead can cost more than the computation
  • Premature optimization โ€” profile first, optimize second
  • When dependencies change on every render anyway (memoization provides no benefit)
04 What is useCallback and how does it differ from useMemo?

Performance useCallback memoizes a function reference โ€” it returns the same function instance across renders unless its dependencies change.

// Without useCallback โ€” new function reference every render
const handleClick = () => doSomething(id);

// With useCallback โ€” same reference unless id changes
const handleClick = useCallback(() => {
  doSomething(id);
}, [id]);

Key difference from useMemo:

  • useMemo(() => computeValue(), [deps]) โ€” memoizes the return value
  • useCallback(() => fn(), [deps]) โ€” memoizes the function itself
  • They are equivalent: useCallback(fn, deps) === useMemo(() => fn, deps)

Primary use case: Pass callbacks to child components wrapped with React.memo. Without useCallback, the child re-renders on every parent render (new function reference = changed prop).

05 What is React.memo? How does it prevent re-renders?

Performance React.memo is a Higher Order Component that wraps a functional component and memoizes its render output. It performs a shallow comparison of props before re-rendering.

const ExpensiveList = React.memo(function({ items, onItemClick }) {
  console.log('rendering ExpensiveList');
  return <ul>{items.map(i => <li key={i.id} onClick={() => onItemClick(i)}>{i.name}</li>)}</ul>;
});

// Custom comparison function (optional)
const Optimized = React.memo(Component, (prevProps, nextProps) => {
  return prevProps.id === nextProps.id; // return true to SKIP re-render
});

The optimization triangle: For React.memo to be effective you usually need:

  • React.memo on the child component
  • useCallback on any function props passed to it
  • useMemo on any object/array props passed to it

Without all three, the shallow comparison may still detect changes and re-render.

06 What are custom hooks? How do you create one?

Hooks Custom hooks are JavaScript functions that start with use and can call other React hooks. They let you extract and share stateful logic between components without HOCs or render props.

// Custom hook: useFetch
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    fetch(url)
      .then(res => res.json())
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [url]);

  return { data, loading, error };
}

// Usage in any component
function UserProfile({ userId }) {
  const { data, loading, error } = useFetch(`/api/users/${userId}`);
  if (loading) return <Spinner />;
  if (error) return <Error message={error.message} />;
  return <Profile user={data} />;
}

Each call to a custom hook gets its own isolated state. Custom hooks don’t share state between components โ€” they share the logic only.

07 What are Error Boundaries? What errors do they NOT catch?

Error Handling Error boundaries are class components that catch JavaScript errors in their child tree during rendering, lifecycle methods, and constructors โ€” displaying a fallback UI instead of crashing the app.

class ErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError(error) {
    return { hasError: true }; // update state to show fallback
  }

  componentDidCatch(error, info) {
    logErrorToService(error, info.componentStack);
  }

  render() {
    if (this.state.hasError) return <h2>Something went wrong.</h2>;
    return this.props.children;
  }
}

// Usage
<ErrorBoundary>
  <MyRiskyComponent />
</ErrorBoundary>

Error boundaries do NOT catch:

  • Errors in event handlers (use try/catch there)
  • Errors in asynchronous code (setTimeout, fetch)
  • Errors in the error boundary itself
  • Errors during server-side rendering

The react-error-boundary npm package provides a modern functional wrapper with reset capabilities.

08 What is code splitting and how do you implement it in React?

Performance Code splitting breaks your JavaScript bundle into smaller chunks loaded on demand. This improves initial load time because users only download the code they need for the current page/feature.

// Static import โ€” loads in the main bundle (bad for large/rarely-used components)
import HeavyChart from './HeavyChart';

// Dynamic import โ€” creates a separate chunk (loaded on demand)
import React, { lazy, Suspense } from 'react';

const HeavyChart = lazy(() => import('./HeavyChart'));

function Dashboard() {
  return (
    <Suspense fallback={<div>Loading chart...</div>}>
      <HeavyChart />
    </Suspense>
  );
}

Route-based splitting is the most impactful strategy โ€” each route becomes a separate chunk:

const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Settings = lazy(() => import('./pages/Settings'));

Bundlers like Webpack and Vite automatically create separate chunks for each dynamic import.

09 What are React Portals? When would you use them?

DOM Portals render children into a DOM node that lives outside the parent component’s DOM hierarchy, while still participating in React’s tree for event bubbling and context.

import ReactDOM from 'react-dom';

function Modal({ children }) {
  return ReactDOM.createPortal(
    <div className="modal-overlay">{children}</div>,
    document.getElementById('modal-root') // outside React's root
  );
}

When to use portals:

  • Modals and dialogs โ€” must escape overflow: hidden or z-index stacking context of parent
  • Tooltips and popovers โ€” need to appear above all other content
  • Notification toasts โ€” rendered at the body level regardless of where they’re triggered
  • Dropdowns โ€” avoid being clipped by parent containers

Even though a portal renders outside the React tree in the DOM, React events still bubble up through the React component hierarchy โ€” not the DOM hierarchy.

10 What is React Router v6? Explain its key concepts.

Routing React Router is the standard client-side routing library. v6 was a major rewrite with significant API changes.

import { BrowserRouter, Routes, Route, Link, useNavigate, useParams } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <nav><Link to="/users">Users</Link></nav>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/users" element={<Users />} />
        <Route path="/users/:id" element={<UserDetail />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
}

// Reading URL params
function UserDetail() {
  const { id } = useParams();
  const navigate = useNavigate();
  return <button onClick={() => navigate('/')}>User {id}</button>;
}

v6 key changes from v5: Switch replaced by Routes, Redirect replaced by Navigate, exact prop removed (exact matching is default), useHistory replaced by useNavigate, nested routes use Outlet.

11 What is the reconciliation algorithm? How does diffing work?

Internals Reconciliation is React’s process of comparing the new Virtual DOM tree with the previous one and computing the minimum DOM changes needed.

React’s diffing heuristics (O(n) algorithm):

  • Different element types: If the root element type changes (e.g., <div> to <span>), React tears down the old tree completely and builds a new one from scratch (including destroying all child component instances and state).
  • Same element type: React keeps the DOM node and only updates changed attributes/props. Child state is preserved.
  • Same component type: React updates the props on the existing component instance. State is preserved.
  • Lists: React compares children by key. Without keys, it assumes position; with keys, it can correctly reorder/add/remove.

Full tree comparison of two trees is O(nยณ) โ€” React’s heuristics make it O(n) by assuming most UI changes happen at the same level of the tree.

12 What is Higher-Order Components (HOC)? Give a real-world example.

Patterns A Higher-Order Component is a function that takes a component and returns a new enhanced component. It’s used to share cross-cutting concerns like authentication, logging, or feature flags.

// HOC: withAuth โ€” redirects if not logged in
function withAuth(WrappedComponent) {
  return function AuthenticatedComponent(props) {
    const { isLoggedIn } = useAuth();
    if (!isLoggedIn) return <Navigate to="/login" />;
    return <WrappedComponent {...props} />;
  };
}

// Usage
const ProtectedDashboard = withAuth(Dashboard);
<ProtectedDashboard user={user} />

HOC caveats:

  • Wrapper hell โ€” nesting multiple HOCs creates deeply nested component trees in DevTools
  • Prop collision โ€” HOC and wrapped component may use the same prop name
  • HOCs are largely replaced by custom hooks in modern React, which are simpler and more composable
13 What are render props? How do they compare to hooks?

Patterns The render prop pattern is a technique where a component accepts a function as a prop that returns React elements. The component calls this function to render its output, sharing its internal state with the consumer.

// Render prop component
function MouseTracker({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  return (
    <div onMouseMove={e => setPosition({ x: e.clientX, y: e.clientY })}>
      {render(position)}
    </div>
  );
}

// Usage
<MouseTracker render={({ x, y }) => <p>Mouse at {x}, {y}</p>} />

The same logic as a custom hook:

function useMouse() {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  // attach listener via useEffect...
  return pos;
}
// Much simpler to use: const { x, y } = useMouse();

Render props and HOCs both predate hooks. In modern React, custom hooks are the preferred abstraction for sharing logic between components.

14 What is useLayoutEffect? When does it differ from useEffect?

Hooks useLayoutEffect fires synchronously after all DOM mutations but before the browser paints. This is unlike useEffect which fires asynchronously after the paint.

// useEffect  โ€” fires after paint (async, non-blocking)
// useLayoutEffect โ€” fires before paint (sync, blocking)

When to use useLayoutEffect:

  • When you need to read a DOM measurement (scroll position, element dimensions) and immediately update state/style to avoid visual flicker
  • Tooltip positioning โ€” read where the target is, calculate popup position, apply โ€” all before the browser shows anything
  • Animations that depend on DOM layout measurements

Prefer useEffect in almost all other cases. useLayoutEffect blocks the browser from painting until it completes, which can hurt perceived performance if overused. It also cannot run on the server (SSR) โ€” use conditionals or move the logic to useEffect in universal code.

15 What is Server-Side Rendering in Next.js? Explain getServerSideProps vs getStaticProps.

SSR Next.js offers multiple data-fetching strategies:

// getServerSideProps โ€” runs on every request (SSR)
export async function getServerSideProps(context) {
  const user = await fetchUser(context.params.id);
  return { props: { user } };
}
// Use when: data changes frequently or is user-specific (dashboards, profiles)

// getStaticProps โ€” runs at build time (SSG)
export async function getStaticProps() {
  const posts = await fetchAllPosts();
  return { props: { posts }, revalidate: 60 }; // ISR: rebuild every 60s
}
// Use when: data is shared and relatively stable (blogs, docs, marketing pages)

Incremental Static Regeneration (ISR): The revalidate option in getStaticProps lets Next.js rebuild individual pages in the background after a time interval โ€” best of both worlds: static speed with near-real-time data.

In the App Router (Next.js 13+), these are replaced by async Server Components with fetch(url, { cache: 'no-store' }) (SSR) or fetch(url, { next: { revalidate: 60 } }) (ISR).

16 What is Redux? Explain the core principles and the Redux Toolkit.

State Management Redux is a predictable state container based on three principles:

  • Single source of truth โ€” entire app state in one store object
  • State is read-only โ€” the only way to change state is to dispatch an action
  • Changes via pure reducers โ€” reducers take (state, action) and return new state

Redux Toolkit (RTK) is the modern way to use Redux โ€” it eliminates boilerplate:

import { createSlice, configureStore } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: state => { state.value += 1 }, // uses Immer โ€” safe mutation syntax
    decrement: state => { state.value -= 1 },
  }
});

export const { increment, decrement } = counterSlice.actions;
export const store = configureStore({ reducer: { counter: counterSlice.reducer } });

In components: const count = useSelector(state => state.counter.value) and dispatch(increment()). RTK Query (built into RTK) handles API fetching with caching, similar to React Query.

17 What is React Strict Mode and what does it do?

Dev Tools <React.StrictMode> is a development-only tool that activates extra checks and warnings to help you write better code. It renders nothing visible in the UI.

// Wrap your app or part of it
<React.StrictMode>
  <App />
</React.StrictMode>

What Strict Mode does in development:

  • Double invokes render โ€” components render twice to detect side effects in render (render should be pure)
  • Double invokes effects โ€” effects run twice (mount โ†’ unmount โ†’ mount) to ensure cleanup works correctly
  • Warns about deprecated APIs โ€” string refs, legacy context, findDOMNode, etc.
  • Warns about state update on unmounted components

The double-render behavior in React 18+ helps identify components that are not pure. If your component breaks when rendered twice, it has a bug. Strict Mode helps surface these issues before they hit production.

18 How do you optimize a React app that is re-rendering too often?

Performance A systematic approach to diagnosing and fixing re-render issues:

Step 1 โ€” Profile first: Use React DevTools Profiler to identify slow renders. Never optimize based on guesswork.

Step 2 โ€” Identify the cause:

  • Parent re-render causes child re-render (default behavior)
  • New object/array/function reference passed as prop on every render
  • Context value changes too frequently

Step 3 โ€” Apply the right fix:

  • React.memo โ€” skip child re-render when props haven’t changed
  • useCallback โ€” stable function references to pass as props
  • useMemo โ€” stable object/array references to pass as props
  • Split context โ€” separate frequently-changing from rarely-changing context values
  • State colocation โ€” move state closer to where it’s used to reduce the scope of re-renders
  • Virtualization โ€” react-window / react-virtual for long lists
19 What is the difference between controlled and uncontrolled forms in React?

Forms

// CONTROLLED โ€” React state is the single source of truth
function ControlledForm() {
  const [name, setName] = useState('');
  const handleSubmit = (e) => { e.preventDefault(); console.log(name); };
  return (
    <form onSubmit={handleSubmit}>
      <input value={name} onChange={e => setName(e.target.value)} />
    </form>
  );
}

// UNCONTROLLED โ€” DOM is the source of truth; read via ref on submit
function UncontrolledForm() {
  const nameRef = useRef(null);
  const handleSubmit = (e) => { e.preventDefault(); console.log(nameRef.current.value); };
  return (
    <form onSubmit={handleSubmit}>
      <input ref={nameRef} defaultValue="" />
    </form>
  );
}

Use controlled for: real-time validation, conditional disabling of the submit button, dynamic input formatting, syncing multiple inputs. Use uncontrolled (or React Hook Form) for: simple forms, file inputs, integrating with non-React code, or when form libraries handle the heavy lifting.

20 What is forwardRef and when do you need it?

Refs React.forwardRef allows a parent component to pass a ref down to a DOM element (or child component) that it doesn’t own directly.

// Without forwardRef โ€” the parent cannot access the <input> inside CustomInput
// With forwardRef โ€” the parent gets a ref to the underlying <input>
const CustomInput = React.forwardRef(function(props, ref) {
  return <input ref={ref} className="styled-input" {...props} />;
});

// Parent usage
function SearchBar() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus(); // works because of forwardRef
  }, []);

  return <CustomInput ref={inputRef} placeholder="Search..." />;
}

Refs are not regular props โ€” React does not pass them automatically. forwardRef is how you “opt in” to exposing a ref from inside your component. It is widely used in UI component libraries (input fields, modals, scroll containers) so consumers can programmatically focus, scroll, or measure elements.

21 What is lazy state initialisation in useState?

Hooks Passing a function to useState enables lazy initialisation โ€” the function runs only on the first render, not on every re-render. This is essential when the initial value is expensive to compute.

// โŒ Expensive function called on EVERY render (wasteful)
const [data, setData] = useState(parseHeavyData(rawInput));

// โœ… Lazy init โ€” function called ONLY on the first render
const [data, setData] = useState(() => parseHeavyData(rawInput));

// Another common use: reading from localStorage once
const [theme, setTheme] = useState(() => {
  return localStorage.getItem('theme') ?? 'light';
});

The difference is subtle but important: without the function wrapper, parseHeavyData(rawInput) evaluates on every render even though its return value is only used once (for initialisation). The function wrapper defers evaluation until it’s actually needed.

📝 Knowledge Check

Test your understanding of advanced React patterns and optimisation techniques.

🧠 Quiz Question 1 of 5

Which hook should you use to memoize a function reference so a child component does not re-render unnecessarily?





🧠 Quiz Question 2 of 5

What is the purpose of React.memo()?





🧠 Quiz Question 3 of 5

Error boundaries can catch errors thrown in which of the following locations?





🧠 Quiz Question 4 of 5

What does the dependency array in useEffect control?





🧠 Quiz Question 5 of 5

Which React API allows rendering a component’s children into a DOM node outside the parent hierarchy?