← Back to DevBytes

Top 50 React Interview Questions for Senior Developers

Top 50 React Interview Questions for Senior Developers

Senior React developer interviews go far beyond basic component syntax. They probe your understanding of rendering behavior, reconciliation, performance trade-offs, architectural patterns, and the subtle edge cases that separate experienced engineers from beginners. This tutorial walks through 50 carefully selected questions, grouped by theme, with practical code examples and the reasoning interviewers want to hear.

Why These Questions Matter

Senior developers are expected to make decisions that affect entire teams: choosing state management strategies, optimizing rendering for large applications, designing reusable component APIs, and debugging complex issues. The questions below mirror real interview scenarios at companies hiring for staff and senior frontend roles. Mastering them means you can articulate not just how React works, but why certain approaches are superior in specific contexts.

Section 1: Core React Concepts (Questions 1–10)

1. What is the difference between JSX and HTML?

JSX is syntactic sugar over React.createElement(). Key differences include using className instead of class, htmlFor instead of for, camelCase event handlers (onClick), self-closing tags for all elements, and the ability to embed JavaScript expressions inside curly braces. JSX also prevents XSS by escaping values by default.

2. Explain the React reconciliation algorithm.

Reconciliation is the process by which React compares the new virtual DOM tree with the previous one to determine the minimal set of DOM mutations. React uses a heuristic O(n) algorithm with two assumptions: elements of different types produce different trees, and keys identify which children have changed. The diffing happens in three phases: type comparison, prop comparison, and children reconciliation.

3. What are keys, and why are they important?

Keys help React identify which items in a list have changed, been added, or removed. They must be stable, unique, and predictable. Using array indices as keys can cause subtle bugs when items reorder, because React reuses component instances and state based on key identity.

// Bad: index as key
{items.map((item, index) => (
  
))}

// Good: stable unique id
{items.map(item => (
  
))}

4. What is the difference between controlled and uncontrolled components?

Controlled components derive their value from React state and notify changes via callbacks. Uncontrolled components store their own state in the DOM, accessed via refs. Controlled components are preferred for most forms because they provide a single source of truth and enable validation.

// Controlled
function ControlledInput() {
  const [value, setValue] = useState('');
  return  setValue(e.target.value)} />;
}

// Uncontrolled
function UncontrolledInput() {
  const inputRef = useRef(null);
  const handleSubmit = () => console.log(inputRef.current.value);
  return ;
}

5. What is a higher-order component (HOC)?

An HOC is a function that takes a component and returns a new component with enhanced behavior. HOCs are used for cross-cutting concerns like authentication, logging, or data fetching. They should be pure, pass through props they don't use, and maximize composability.

function withAuth(WrappedComponent) {
  return function AuthenticatedComponent(props) {
    const { user } = useAuth();
    if (!user) return ;
    return ;
  };
}

const Dashboard = withAuth(DashboardBase);

6. What is the difference between props and state?

Props are read-only inputs passed from a parent component. State is internal, mutable data managed by the component itself. Props are immutable within the receiving component; state changes trigger re-renders. A key senior insight: state can be lifted up or pushed down, and the decision shapes your component architecture.

7. What are render props?

Render props is a pattern where a component takes a function as a prop that returns a React element, allowing the parent to control what gets rendered inside. This enables logic reuse without HOCs.

function MouseTracker({ render }) {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  return (
    
setPos({ x: e.clientX, y: e.clientY })}> {render(pos)}
); } // Usage

{pos.x}, {pos.y}

} />

8. What is the virtual DOM?

The virtual DOM is an in-memory representation of the actual DOM. React updates the virtual DOM first, diffs it against the previous version, and applies only the necessary changes to the real DOM. This batching minimizes expensive browser reflows and repaints.

9. What is the difference between createElement and cloneElement?

createElement creates a new React element from scratch. cloneElement copies an existing element and optionally overrides or adds props. cloneElement is useful when you want to inject props into children passed into your component.

10. What is a fragment, and when would you use it?

Fragments let you group multiple elements without adding extra DOM nodes. Use the shorthand <> syntax or the explicit <React.Fragment> when you need a key prop.

function DefinitionList({ items }) {
  return (
    
{items.map(item => (
{item.term}
{item.description}
))}
); }

Section 2: Hooks Deep Dive (Questions 11–20)

11. Why does React enforce the Rules of Hooks?

Hooks rely on call order. React internally maintains a linked list of hooks per component, and each hook call must correspond to the same slot on every render. Calling hooks conditionally, in loops, or inside nested functions breaks this ordering and corrupts state. Lint rules enforce this invariant.

12. What is the difference between useState and useReducer?

useState is ideal for independent primitive state. useReducer shines when state transitions are complex, interdependent, or when the next state depends on the previous one. It centralizes transition logic and makes state changes predictable and testable.

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: return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  return (
    <>
      
      {state.count}
      
    
  );
}

13. How does useEffect differ from useLayoutEffect?

useEffect runs asynchronously after the browser paints, making it suitable for most side effects. useLayoutEffect runs synchronously after DOM mutations but before paint, useful when you need to measure or modify the DOM before the user sees it (e.g., tooltips, animations). useLayoutEffect can block painting, so prefer useEffect unless you see visual flicker.

14. What is the dependency array in useEffect, and what are common pitfalls?

The dependency array tells React when to re-run the effect. An empty array runs once on mount. Omitting it runs after every render. Common pitfalls include stale closures (omitting a used dependency), infinite loops (setting state without a dependency array), and over-fetching (including unstable references).

// Stale closure bug
function Timer() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1); // count is always 0 here
    }, 1000);
    return () => clearInterval(id);
  }, []); // missing count

  // Fix: use functional update
  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);
}

15. What is useMemo, and when should you avoid it?

useMemo caches a computed value between renders unless dependencies change. Use it for expensive calculations or to preserve referential equality of objects passed to memoized children. Avoid it for trivial computations, because the memoization itself has overhead. Overusing useMemo can hurt performance more than it helps.

16. What is useCallback, and how does it relate to useMemo?

useCallback memoizes a function reference. It is essentially useMemo(() => fn, deps). Use it when passing callbacks to optimized child components that rely on referential equality to skip re-renders.

const handleSubmit = useCallback((data) => {
  saveData(data);
}, [saveData]);

return ;

17. What is useRef, and what are its common use cases?

useRef returns a mutable object whose .current property persists across renders without triggering re-renders. Common uses: accessing DOM elements, storing mutable values that don't affect rendering, and holding interval/timeout IDs.

function Stopwatch() {
  const [seconds, setSeconds] = useState(0);
  const intervalRef = useRef(null);

  const start = () => {
    intervalRef.current = setInterval(() => setSeconds(s => s + 1), 1000);
  };
  const stop = () => clearInterval(intervalRef.current);

  useEffect(() => () => clearInterval(intervalRef.current), []);
  return 
{seconds}
; }

18. What is useImperativeHandle?

It customizes the instance value exposed to parent components when using ref. Instead of exposing the entire DOM node, you expose only specific methods, creating a controlled imperative API.

const FancyInput = forwardRef((props, ref) => {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
    clear: () => { inputRef.current.value = ''; }
  }));
  return ;
});

19. What is useTransition, and when would you use it?

Introduced in React 18, useTransition marks certain state updates as non-urgent, allowing React to interrupt them to keep the UI responsive. Use it for expensive renders triggered by user input, like filtering a large list.

function Search() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  const handleChange = (e) => {
    setQuery(e.target.value); // urgent
    startTransition(() => {
      setResults(filterHugeList(e.target.value)); // non-urgent
    });
  };

  return (
    <>
      
      {isPending ?  : }
    
  );
}

20. What is useDeferredValue?

useDeferredValue lets you defer updating a less critical part of the UI. It returns a deferred copy of a value that lags behind during urgent updates. It is useful when you cannot control the source of the state update (e.g., receiving props from a parent).

function FilteredList({ items, filter }) {
  const deferredFilter = useDeferredValue(filter);
  const filtered = useMemo(() => items.filter(i => i.includes(deferredFilter)),
    [items, deferredFilter]);
  return ;
}

Section 3: Performance Optimization (Questions 21–28)

21. What is React.memo, and when should you use it?

React.memo is a higher-order component that memoizes a component, re-rendering only when props change by shallow comparison. Use it for pure components that render often with the same props. Avoid it for components that always receive new object or function props unless you also memoize those props.

const ExpensiveItem = React.memo(function Item({ title, onClick }) {
  return 
  • {title}
  • ; });

    22. What is the difference between shallow and deep comparison?

    Shallow comparison checks reference equality for primitives and referential equality for objects/arrays. Deep comparison recursively compares values. React's default optimizations use shallow comparison because deep comparison is expensive and can negate performance gains.

    23. How do you prevent unnecessary re-renders?

    24. What is code splitting, and how do you implement it in React?

    Code splitting breaks your bundle into smaller chunks loaded on demand. Use React.lazy with Suspense for route-level or component-level splitting.

    const AdminPanel = React.lazy(() => import('./AdminPanel'));
    
    function App() {
      return (
        }>
          
        
      );
    }
    

    25. What is the difference between lazy loading and prefetching?

    Lazy loading fetches resources only when needed, reducing initial load. Prefetching loads resources in advance during idle time, anticipating future navigation. Combining both gives optimal perceived performance: prefetch the next likely route while lazy loading rarely visited ones.

    26. How do you optimize large lists in React?

    Use virtualization libraries like react-window or react-virtualized to render only visible items. Also consider pagination, infinite scroll, and memoizing row components.

    import { FixedSizeList } from 'react-window';
    
    const Row = ({ index, style }) => (
      
    Item {index}
    ); function BigList({ count }) { return ( {Row} ); }

    27. What is the React Profiler, and how do you use it?

    The Profiler measures render times and helps identify performance bottlenecks. Use the React DevTools Profiler tab to record interactions and inspect component render durations. You can also use the <Profiler> component programmatically.

    function onRender(id, phase, actualDuration) {
      console.log(`${id} ${phase} took ${actualDuration}ms`);
    }
    
    
      
    
    

    28. What is windowing, and why is it effective?

    Windowing (virtualization) renders only the subset of items visible in the viewport, replacing the rest with empty space. For a list of 10,000 items, only ~20 DOM nodes exist at any time, dramatically reducing memory and rendering cost.

    Section 4: State Management (Questions 29–36)

    29. When should you lift state up?

    Lift state up when two sibling components need to share or synchronize data. The common parent becomes the source of truth. Avoid lifting too high, however, because it causes widespread re-renders. Balance cohesion with locality.

    30. What is prop drilling, and how do you solve it?

    Prop drilling occurs when props are passed through multiple intermediate components that don't use them. Solutions include Context API, composition (passing components as children or render props), and state management libraries like Redux, Zustand, or Jotai.

    31. How does the Context API work, and what are its limitations?

    Context provides a way to pass data through the component tree without prop drilling. When context value changes, every consumer re-renders, regardless of whether the consumed slice changed. This makes Context inefficient for high-frequency updates. Split contexts by update frequency to mitigate this.

    const ThemeContext = createContext();
    
    function App() {
      const [theme, setTheme] = useState('dark');
      return (
        
          
        
      );
    }
    

    32. What is the Flux architecture, and how does Redux implement it?

    Flux is a unidirectional data flow pattern: actions dispatch to a store, which updates state and notifies views. Redux implements Flux with a single store, pure reducer functions, and immutable state. This makes state changes predictable, debuggable via time-travel, and testable.

    33. What is Redux Toolkit, and why is it recommended over classic Redux?

    Redux Toolkit (RTK) reduces boilerplate with createSlice, configures sensible defaults (Immer for immutability, Redux Thunk for async), and includes createAsyncThunk for async logic. It is now the official recommended way to write Redux.

    const usersSlice = createSlice({
      name: 'users',
      initialState: { list: [], status: 'idle' },
      reducers: {
        addUser: (state, action) => { state.list.push(action.payload); }
      },
      extraReducers: (builder) => {
        builder
          .addCase(fetchUsers.pending, (state) => { state.status = 'loading'; })
          .addCase(fetchUsers.fulfilled, (state, action) => {
            state.list = action.payload;
            state.status = 'succeeded';
          });
      }
    });
    

    34. What are alternatives to Redux, and when would you choose them?

    Alternatives include Zustand (minimal, hook-based), Jotai (atomic state), Recoil (atomic, derived), MobX (observable, fine-grained), and React Query (server state). Choose based on needs: Zustand for simplicity, Jotai for fine-grained reactivity, React Query for server cache. Many apps now separate server state (React Query) from client state (Zustand/Context).

    35. What is the difference between server state and client state?

    Server state is data fetched from a server: it is asynchronous, can be stale, and may be owned by multiple clients. Client state is UI-specific data like form inputs, theme, or modal visibility. Tools like React Query, SWR, and RTK Query specialize in server state, while Redux/Zustand handle client state.

    36. How do you persist state across page reloads?

    Use localStorage, sessionStorage, or IndexedDB. Libraries like redux-persist or zustand/middleware/persist automate this. Always consider serialization cost and security: never store sensitive data in localStorage.

    const useStore = create(
      persist(
        (set) => ({ user: null, setUser: (user) => set({ user }) }),
        { name: 'app-storage' }
      )
    );
    

    Section 5: Patterns and Architecture (Questions 37–42)

    37. What are compound components?

    Compound components let related components work together through implicit state sharing, typically via Context. This pattern produces expressive, declarative APIs like <Select>, <Select.Option>.

    const SelectContext = createContext();
    
    function Select({ children, value, onChange }) {
      return (
        
          
    {children}
    ); } Select.Option = function Option({ value, children }) { const { value: selected, onChange } = useContext(SelectContext); return ( ); }; // Usage

    38. What is the container/presentational pattern?

    Container components handle data fetching and state; presentational components focus on UI. This separation improves reusability and testability. With hooks, this pattern is less rigid—custom hooks often replace containers—but the conceptual separation remains valuable.

    39. What are custom hooks, and what makes a good one?

    Custom hooks extract reusable stateful logic into functions prefixed with use. A good custom hook has a single responsibility, returns a stable API, handles cleanup, and is composable with other hooks.

    function useDebounce(value, delay) {
      const [debounced, setDebounced] = useState(value);
      useEffect(() => {
        const id = setTimeout(() => setDebounced(value), delay);
        return () => clearTimeout(id);
      }, [value, delay]);
      return debounced;
    }
    

    40. What is the provider pattern?

    The provider pattern wraps the app (or a subtree) in a context provider to make global data available. Examples include theme providers, auth providers, and feature-flag providers. Keep providers close to where they're needed to avoid unnecessary re-renders.

    41. How do you handle error boundaries in React?

    Error boundaries are class components (or those using react-error-boundary) that catch errors in their child tree during rendering, lifecycle methods, and constructors. They prevent a single error from unmounting the entire app.

    class ErrorBoundary extends React.Component {
      constructor(props) {
        super(props);
        this.state = { hasError: false };
      }
      static getDerivedStateFromError() {
        return { hasError: true };
      }
      componentDidCatch(error, info) {
        logErrorToService(error, info);
      }
      render() {
        if (this.state.hasError) return ;
        return this.props.children;
      }
    }
    

    42. What is the difference between controlled and uncontrolled flows in form architecture?

    Controlled flows keep all form state in React, enabling validation, conditional rendering, and dynamic defaults. Uncontrolled flows let the DOM manage state, useful for large forms where re-rendering on every keystroke is expensive. Hybrid approaches use controlled fields for critical inputs and uncontrolled for the rest. Libraries like react-hook-form bridge both worlds efficiently.

    Section 6: React 18+ and Concurrent Features (Questions 43–46)

    43. What is concurrent rendering in React 18?

    Concurrent rendering lets React prepare multiple versions of the UI simultaneously, interrupting, pausing, or abandoning renders. This enables features like useTransition, useDeferredValue, and automatic batching. It improves responsiveness by keeping the main thread available for user interactions.

    44. What is automatic batching?

    React 18 batches state updates inside all event handlers, promises, timeouts, and native handlers—not just React event handlers as in React 17. This reduces unnecessary re-renders.

    // React 18: both updates batched into one re-render
    function handleClick() {
      fetch('/api').then(() => {
        setCount(c => c + 1);
        setFlag(f => !f);
      });
    }
    

    45. What is Suspense for data fetching?

    Suspense lets components declaratively wait for async resources. Instead of managing loading state manually, components suspend, and the nearest <Suspense> boundary shows a fallback. This works with React.lazy, Relay, and frameworks like Next.js. The RSC (React Server Components) model relies heavily on Suspense.

    }>
      
    
    

    46. What are React Server Components?

    Server Components render exclusively on the server and never ship their code to the client. They can access databases and filesystems directly, reducing bundle size. They cannot use state or lifecycle hooks. Combined with Client Components, they enable a hybrid model where interactivity lives on the client and data-heavy rendering lives on the server.

    Section 7: Testing and Best Practices (Questions 47–50)

    47. What is the difference between unit, integration, and end-to-end tests in React?

    Unit tests isolate individual functions or components. Integration tests verify multiple components working together. End-to-end (E2E) tests simulate real user journeys through the browser. A healthy test pyramid has many unit tests, fewer integration tests, and a small number of E2E tests.

    48. How do you test components with React Testing Library?

    React Testing Library encourages testing behavior over implementation. Query elements as users would (by role, label, text), interact with them, and assert on the output. Avoid testing internal state or component instance methods.

    import { render, screen, fireEvent } from '@testing-library/react';
    
    test('submits form with valid input', async () => {
      const mockSubmit = jest.fn();
      render();
    
      fireEvent.change(screen.getByLabelText(/name/i), { target: { value: 'Alice' } });
      fireEvent.click(screen.getByRole('button', { name: /submit/i }));
    
      await waitFor(() => expect(mockSubmit).toHaveBeenCalledWith({ name: 'Alice' }));
    });
    

    49. How do you mock API calls in tests?

    Use MSW (Mock Service Worker) to intercept network requests at the service worker level. This keeps tests close to real behavior and works across React Testing Library, Jest, and Playwright. Avoid mocking fetch directly when possible, because it couples tests to implementation details.

    import { setupServer } from 'msw/node';
    import { http, HttpResponse } from 'msw';
    
    const server = setupServer(
      http.get('/api/users', () => HttpResponse.json([{ id: 1, name: 'Alice' }]))
    );
    
    beforeAll(() => server.listen());
    afterEach(() => server.resetHandlers());
    afterAll(() => server.close());
    

    50. What are the most important best practices for senior React developers?

    Conclusion

    Senior React interviews test not only your knowledge of APIs but also your judgment about trade-offs. The best answers acknowledge context: when to memoize and when not to, when to reach for Redux versus Zustand, when server components make sense and when they don't. Practice articulating these decisions out loud, write small experiments to verify your mental model, and review real codebases to see how patterns play out at scale. With these 50 questions as your foundation, you'll be equipped to demonstrate both depth and pragmatism—the qualities that define a senior React engineer.

    — Ad —

    Google AdSense will appear here after approval

    ← Back to all articles