15+ React Hooks Interview Questions 2025: useState, useEffect & Custom Hooks

·10 min read
reactinterview-questionshooksfrontendjavascript

React Hooks power 99% of modern React applications, yet "explain when to use useCallback vs useMemo" remains one of the most commonly failed interview questions. The gap between using hooks and understanding them is where most candidates stumble.

Table of Contents

  1. Hook Fundamentals Questions
  2. useState Questions
  3. useEffect Questions
  4. useMemo and useCallback Questions
  5. Custom Hooks Questions
  6. useRef and useLayoutEffect Questions
  7. State Management Questions
  8. Quick Reference

Hook Fundamentals Questions

These questions test your core understanding of React Hooks.

What are React Hooks and why were they introduced?

React Hooks are functions that let you use state and other React features in functional components. Before Hooks, you needed class components to use state or lifecycle methods, which led to complex patterns like render props and higher-order components. Hooks solve this by letting you extract stateful logic into reusable functions.

The most important hooks are:

  • useState for local component state
  • useEffect for side effects like data fetching or subscriptions
  • useContext for consuming React Context
  • useRef for mutable values that don't trigger re-renders
  • useMemo and useCallback for performance optimization

What are the rules of Hooks?

Two main rules:

  1. Only call hooks at the top level - never inside loops, conditions, or nested functions. This ensures hooks are called in the same order each render.
  2. Only call hooks from React function components or custom hooks - not from regular JavaScript functions.

These rules ensure React can correctly track hook state between renders.


useState Questions

These questions test your understanding of state management with useState.

How do you use useState for different scenarios?

// Basic usage
const [count, setCount] = useState(0);
 
// Object state
const [user, setUser] = useState({ name: '', email: '' });
 
// Updating object state (always create new object)
setUser(prev => ({ ...prev, name: 'John' }));
 
// Functional update (when new state depends on old)
setCount(prev => prev + 1);
 
// Lazy initial state (expensive computation)
const [data, setData] = useState(() => computeExpensiveValue());

useEffect Questions

These questions test your understanding of side effects and the dependency array.

How does the useEffect dependency array work?

The dependency array controls when the effect runs. Here are the three patterns:

// Runs after every render
useEffect(() => {
  console.log('Every render');
});
 
// Runs once on mount
useEffect(() => {
  console.log('Mount only');
}, []);
 
// Runs when dependencies change
useEffect(() => {
  console.log('userId changed');
  fetchUser(userId);
}, [userId]);
 
// With cleanup
useEffect(() => {
  const subscription = subscribeToData(id);
 
  return () => {
    subscription.unsubscribe(); // Cleanup
  };
}, [id]);

What causes an infinite loop in useEffect and how do you fix it?

This mistake appears in almost every React interview:

// BUG: Infinite loop!
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
 
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => setUser(data));
  }); // Missing dependency array!
 
  return <div>{user?.name}</div>;
}

What happens?

  1. Component renders
  2. useEffect runs, fetches data
  3. setUser triggers re-render
  4. useEffect runs again (no deps = runs every render)
  5. Infinite loop!

Three Ways to Fix It:

1. Add dependency array (correct solution)

useEffect(() => {
  fetch(`/api/users/${userId}`)
    .then(res => res.json())
    .then(data => setUser(data));
}, [userId]); // Only re-run when userId changes

2. Empty array for mount-only

useEffect(() => {
  fetchInitialData();
}, []); // Runs once on mount

3. Use a flag to prevent duplicate fetches

useEffect(() => {
  let cancelled = false;
 
  fetch(`/api/users/${userId}`)
    .then(res => res.json())
    .then(data => {
      if (!cancelled) setUser(data);
    });
 
  return () => { cancelled = true; }; // Cleanup
}, [userId]);

useMemo and useCallback Questions

These questions test your understanding of performance optimization with memoization.

What is the difference between useMemo and useCallback?

This is one of the most commonly failed interview questions because candidates often mix them up.

// useCallback - memoizes a FUNCTION
const handleClick = useCallback(() => {
  console.log('Clicked:', id);
}, [id]);
 
// useMemo - memoizes a VALUE
const expensiveResult = useMemo(() => {
  return computeExpensiveValue(data);
}, [data]);

When should you use useCallback?

Use useCallback when passing functions to child components to prevent unnecessary re-renders:

function Parent() {
  const [count, setCount] = useState(0);
 
  // Without useCallback, this creates a new function every render
  // causing Child to re-render unnecessarily
  const handleIncrement = useCallback(() => {
    setCount(c => c + 1);
  }, []);
 
  return <Child onIncrement={handleIncrement} />;
}
 
const Child = React.memo(({ onIncrement }) => {
  console.log('Child rendered');
  return <button onClick={onIncrement}>+</button>;
});

When should you use useMemo?

Use useMemo for expensive calculations that shouldn't run on every render:

function FilteredList({ items, filter }) {
  // Only recalculates when items or filter change
  const filteredItems = useMemo(() => {
    return items.filter(item =>
      item.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [items, filter]);
 
  return <List items={filteredItems} />;
}

When is useMemo or useCallback unnecessary?

Over-optimization is a common trap. Not everything needs memoization:

// DON'T do this - premature optimization
const value = useMemo(() => a + b, [a, b]); // Simple math doesn't need memoization
 
// DO use it for actual expensive operations
const sorted = useMemo(() =>
  [...largeArray].sort((a, b) => a.value - b.value),
  [largeArray]
);

Custom Hooks Questions

These questions test your ability to extract and reuse stateful logic.

What is a custom hook and when should you create one?

A custom hook is a JavaScript function starting with 'use' that can call other hooks. Create custom hooks to extract and reuse stateful logic between components. Each component using the hook gets its own isolated state—custom hooks share logic, not state.

How do you implement a useLocalStorage hook?

function useLocalStorage(key, initialValue) {
  // Get initial value from localStorage or use default
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      return initialValue;
    }
  });
 
  // Update localStorage when state changes
  const setValue = useCallback((value) => {
    try {
      const valueToStore = value instanceof Function
        ? value(storedValue)
        : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  }, [key, storedValue]);
 
  return [storedValue, setValue];
}
 
// Usage
function App() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
 
  return (
    <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
      Current: {theme}
    </button>
  );
}

How do you implement a useFetch hook with loading and error states?

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
 
  useEffect(() => {
    let cancelled = false;
    setLoading(true);
 
    fetch(url)
      .then(res => {
        if (!res.ok) throw new Error('Network error');
        return res.json();
      })
      .then(data => {
        if (!cancelled) {
          setData(data);
          setLoading(false);
        }
      })
      .catch(err => {
        if (!cancelled) {
          setError(err.message);
          setLoading(false);
        }
      });
 
    return () => { cancelled = true; };
  }, [url]);
 
  return { data, loading, error };
}
 
// Usage
function UserProfile({ userId }) {
  const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
 
  if (loading) return <Spinner />;
  if (error) return <Error message={error} />;
  return <Profile user={user} />;
}

useRef and useLayoutEffect Questions

These questions test your understanding of refs and synchronous effects.

What is useRef and when do you use it?

useRef returns a mutable object with a .current property that persists across renders without causing re-renders when changed. Two main uses:

  1. Accessing DOM elements directly
  2. Storing mutable values that don't need to trigger re-renders (like timer IDs)
// DOM access
function TextInput() {
  const inputRef = useRef(null);
 
  const focusInput = () => inputRef.current.focus();
 
  return <input ref={inputRef} />;
}
 
// Mutable value without re-render
function Timer() {
  const intervalRef = useRef(null);
 
  const start = () => {
    intervalRef.current = setInterval(() => {
      console.log('tick');
    }, 1000);
  };
 
  const stop = () => clearInterval(intervalRef.current);
 
  return <button onClick={start}>Start</button>;
}

What is the difference between useEffect and useLayoutEffect?

Both run after render, but useLayoutEffect runs synchronously before the browser paints. Use useLayoutEffect when you need to measure DOM elements or make visual changes that must happen before the user sees anything. For most cases, useEffect is correct and more performant.

// useLayoutEffect - runs before paint
useLayoutEffect(() => {
  // Measure element, adjust position
  const height = ref.current.getBoundingClientRect().height;
  setPosition(calculatePosition(height));
}, []);
 
// useEffect - runs after paint (default choice)
useEffect(() => {
  // Fetch data, set up subscriptions
  fetchData();
}, []);

State Management Questions

These questions test your understanding of state management patterns.

Three approaches depending on complexity:

  1. Multiple useState calls for independent values
  2. Single useState with an object for related values
  3. useReducer for complex state with interdependent updates
// Multiple useState - simple, independent values
const [name, setName] = useState('');
const [email, setEmail] = useState('');
 
// Object state - related values
const [form, setForm] = useState({ name: '', email: '' });
const updateField = (field, value) =>
  setForm(prev => ({ ...prev, [field]: value }));
 
// useReducer - complex, interdependent updates
const [state, dispatch] = useReducer(reducer, initialState);
dispatch({ type: 'SUBMIT_FORM' });

Quick Reference

HookPurposeKey Point
useStateLocal stateUse functional updates for state based on prev
useEffectSide effectsAlways consider cleanup and deps
useContextConsume contextTriggers re-render when context changes
useRefDOM/mutable valuesChanges don't cause re-renders
useMemoMemoize valuesFor expensive calculations
useCallbackMemoize functionsFor stable references to child components
useReducerComplex stateWhen state logic is complex
useLayoutEffectSync after renderFor DOM measurements before paint

Practice Questions

Test yourself before your interview:

1. What's wrong with this code?

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
 
  useEffect(async () => {
    const data = await fetchResults(query);
    setResults(data);
  }, [query]);
 
  return <List items={results} />;
}

2. Will the Child component re-render when Parent re-renders? How would you fix it?

function Parent() {
  const [count, setCount] = useState(0);
 
  const handleClick = () => console.log('clicked');
 
  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      <Child onClick={handleClick} />
    </>
  );
}
 
const Child = React.memo(({ onClick }) => {
  console.log('Child rendered');
  return <button onClick={onClick}>Click me</button>;
});

3. Implement a useToggle custom hook that returns [value, toggle].

Answers:

  1. useEffect callback can't be async directly. Wrap in an IIFE or define async function inside.
  2. Yes, Child re-renders because handleClick is recreated each render. Fix with useCallback.
  3. const useToggle = (initial = false) => { const [value, setValue] = useState(initial); const toggle = useCallback(() => setValue(v => !v), []); return [value, toggle]; }

Ready to ace your interview?

Get 550+ interview questions with detailed answers in our comprehensive PDF guides.

View PDF Guides