⚛️
Intermediate
14 min read

React Interview Questions

React hooks, components, lifecycle, and best practices

Essential React Interview Questions

Master React concepts that are frequently asked in interviews. This guide covers hooks, lifecycle, virtual DOM, reconciliation, and performance optimization.

1. Explain Virtual DOM and Reconciliation

The Virtual DOM is a lightweight JavaScript representation of the actual DOM. React uses it to optimize DOM updates.

How it works:
  1. Component state/props change
  2. React creates new Virtual DOM tree
  3. Compares new tree with previous (diffing algorithm)
  4. Calculates minimum changes needed (reconciliation)
  5. Updates only changed parts in real DOM (batching)
// React optimizes this:
function App() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <h1>Counter</h1>
      <p>{count}</p> {/* Only this updates, not entire div */}
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
}

2. useState Hook - Common Patterns

import { useState } from 'react';

// Basic usage
const [count, setCount] = useState(0);
const [user, setUser] = useState({ name: 'John', age: 30 });

// Functional update (when new state depends on previous)
setCount(prevCount => prevCount + 1);

// Object state - always merge manually
setUser(prevUser => ({
  ...prevUser,
  age: 31 // Only update age
}));

// Lazy initialization (expensive computation)
const [data, setData] = useState(() => {
  const initialData = expensiveComputation();
  return initialData;
});

// Multiple state variables vs single object
// ❌ Single object (causes unnecessary re-renders)
const [state, setState] = useState({
  name: '',
  email: '',
  age: 0
});

// ✅ Separate state variables
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState(0);

// State batching (React 18+)
function handleClick() {
  setCount(count + 1);
  setUser({ name: 'Jane' });
  // Only one re-render (automatic batching)
}

3. useEffect Hook - Lifecycle and Cleanup

import { useEffect, useState } from 'react';

function Component() {
  // Run once on mount (empty dependency array)
  useEffect(() => {
    console.log('Component mounted');
    
    // Cleanup on unmount
    return () => {
      console.log('Component unmounting');
    };
  }, []);

  // Run when dependencies change
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `Count: ${count}`;
  }, [count]);

  // No dependency array - runs after every render (avoid!)
  useEffect(() => {
    console.log('Runs after every render');
  });

  // Fetching data
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;

    async function fetchData() {
      try {
        const response = await fetch('/api/data');
        const json = await response.json();
        if (!cancelled) {
          setData(json);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err);
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    }

    fetchData();

    return () => {
      cancelled = true; // Prevent state update if unmounted
    };
  }, []);

  // Event listeners cleanup
  useEffect(() => {
    function handleScroll() {
      console.log('Scrolling');
    }

    window.addEventListener('scroll', handleScroll);

    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  }, []);

  // Timer cleanup
  useEffect(() => {
    const timerId = setInterval(() => {
      console.log('Tick');
    }, 1000);

    return () => clearInterval(timerId);
  }, []);
}

4. useMemo and useCallback - Performance Optimization

import { useMemo, useCallback, useState } from 'react';

function ExpensiveComponent({ items }) {
  // useMemo - memoize computed values
  const total = useMemo(() => {
    console.log('Calculating total...');
    return items.reduce((sum, item) => sum + item.price, 0);
  }, [items]); // Recalculate only when items change

  // Without useMemo, this runs on every render
  const sortedItems = useMemo(() => {
    return [...items].sort((a, b) => a.price - b.price);
  }, [items]);

  // useCallback - memoize functions
  const handleClick = useCallback((id) => {
    console.log('Clicked:', id);
  }, []); // Function reference stays the same

  return (
    <div>
      <p>Total: $\{total}</p>
      {sortedItems.map(item => (
        <Item key={item.id} item={item} onClick={handleClick} />
      ))}
    </div>
  );
}

// Child component wrapped in memo
const Item = React.memo(({ item, onClick }) => {
  console.log('Item rendered:', item.id);
  return (
    <div onClick={() => onClick(item.id)}>
      {item.name} - $\{item.price}
    </div>
  );
});

// When to use:
// ✅ Expensive calculations
// ✅ Prevent unnecessary re-renders of child components
// ❌ Don't use for simple operations (overhead not worth it)

5. useRef - Refs and DOM Access

import { useRef, useEffect } from 'react';

function Component() {
  // Access DOM elements
  const inputRef = useRef(null);
  
  useEffect(() => {
    inputRef.current.focus(); // Focus input on mount
  }, []);

  // Store mutable value that doesn't cause re-render
  const countRef = useRef(0);
  
  function handleClick() {
    countRef.current += 1; // No re-render
    console.log('Clicked:', countRef.current);
  }

  // Store previous value
  const [value, setValue] = useState('');
  const prevValueRef = useRef('');
  
  useEffect(() => {
    prevValueRef.current = value;
  }, [value]);
  
  console.log('Current:', value, 'Previous:', prevValueRef.current);

  // Store interval/timeout ID
  const intervalRef = useRef(null);
  
  function startTimer() {
    intervalRef.current = setInterval(() => {
      console.log('Tick');
    }, 1000);
  }
  
  function stopTimer() {
    clearInterval(intervalRef.current);
  }

  return (
    <div>
      <input ref={inputRef} />
      <button onClick={handleClick}>Click (no re-render)</button>
    </div>
  );
}

6. useContext - Global State Management

import { createContext, useContext, useState } from 'react';

// Create context
const ThemeContext = createContext(null);

// Provider component
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };
  
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// Custom hook for easier usage
function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

// Usage in components
function App() {
  return (
    <ThemeProvider>
      <Header />
      <Content />
    </ThemeProvider>
  );
}

function Header() {
  const { theme, toggleTheme } = useTheme();
  
  return (
    <header className={theme}>
      <button onClick={toggleTheme}>
        Toggle Theme
      </button>
    </header>
  );
}

7. Custom Hooks

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    async function fetchData() {
      try {
        const response = await fetch(url);
        const json = await response.json();
        setData(json);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    }

    fetchData();
  }, [url]);

  return { data, loading, error };
}

// Usage
function UserProfile({ userId }) {
  const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  
  return <div>{user.name}</div>;
}

// useLocalStorage - Persist state
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initialValue;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

// Usage
function Settings() {
  const [settings, setSettings] = useLocalStorage('settings', {
    theme: 'light',
    notifications: true
  });
}

// useDebounce - Debounced value
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => clearTimeout(handler);
  }, [value, delay]);

  return debouncedValue;
}

// Usage
function SearchComponent() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 500);

  useEffect(() => {
    if (debouncedQuery) {
      // Fetch search results
    }
  }, [debouncedQuery]);

  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

8. Component Lifecycle (Class vs Hooks)

// Class Component
class ClassComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  componentDidMount() {
    // After first render
    console.log('Mounted');
  }

  componentDidUpdate(prevProps, prevState) {
    // After updates
    if (prevState.count !== this.state.count) {
      console.log('Count changed');
    }
  }

  componentWillUnmount() {
    // Before component is removed
    console.log('Unmounting');
  }

  render() {
    return <div>{this.state.count}</div>;
  }
}

// Equivalent with Hooks
function HookComponent() {
  const [count, setCount] = useState(0);

  // componentDidMount
  useEffect(() => {
    console.log('Mounted');
  }, []);

  // componentDidUpdate for specific value
  useEffect(() => {
    console.log('Count changed');
  }, [count]);

  // componentWillUnmount
  useEffect(() => {
    return () => {
      console.log('Unmounting');
    };
  }, []);

  return <div>{count}</div>;
}

9. React.memo and Component Optimization

import React, { memo } from 'react';

// Without memo - re-renders even if props don't change
function ExpensiveComponent({ data }) {
  console.log('Rendering ExpensiveComponent');
  return <div>{data}</div>;
}

// With memo - only re-renders if props change
const MemoizedComponent = memo(function ExpensiveComponent({ data }) {
  console.log('Rendering MemoizedComponent');
  return <div>{data}</div>;
});

// Custom comparison function
const CustomMemoComponent = memo(
  function Component({ user }) {
    return <div>{user.name}</div>;
  },
  (prevProps, nextProps) => {
    // Return true to skip re-render
    return prevProps.user.id === nextProps.user.id;
  }
);

// Parent component
function Parent() {
  const [count, setCount] = useState(0);
  const [data] = useState('Static data');

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Count: {count}
      </button>
      {/* This re-renders on every count change */}
      <ExpensiveComponent data={data} />
      
      {/* This doesn't re-render (data hasn't changed) */}
      <MemoizedComponent data={data} />
    </div>
  );
}

10. Error Boundaries

// Error boundaries must be class components
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    // Update state so next render shows fallback UI
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    // Log error to error reporting service
    console.error('Error caught:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div>
          <h1>Something went wrong</h1>
          <p>{this.state.error?.message}</p>
          <button onClick={() => this.setState({ hasError: false })}>
            Try again
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

// Usage
function App() {
  return (
    <ErrorBoundary>
      <ProblematicComponent />
    </ErrorBoundary>
  );
}

// Error boundaries catch:
// ✅ Errors in render methods
// ✅ Errors in lifecycle methods
// ✅ Errors in constructor

// Error boundaries DON'T catch:
// ❌ Event handlers (use try-catch)
// ❌ Async code (setTimeout, promises)
// ❌ Server-side rendering
// ❌ Errors in error boundary itself
Key Interview Takeaways:
  • Virtual DOM optimizes updates through diffing and reconciliation
  • useState for component state, useEffect for side effects
  • useMemo for expensive calculations, useCallback for memoized functions
  • useRef for DOM access and storing mutable values
  • useContext for global state (avoid prop drilling)
  • Custom hooks for reusable logic
  • React.memo to prevent unnecessary re-renders
  • Error boundaries catch rendering errors
  • Always cleanup useEffect (event listeners, timers, subscriptions)