StackInterview logoStackInterview icon

Explore

Library

Resources

Articles

Insights

StackInterview

StackInterview helps developers prepare for full-stack interviews with structured questions, real company interview insights, and modern technology coverage.

About UsFAQContactPrivacy PolicyTerms of Service

© 2026 StackInterview. Built for engineers, by engineers.

Developed and Maintained by Abhijeet Kushwaha

All Articles
⚛️React & Frontend22 min read

40+ React Hooks Interview Questions and Answers (2026)

React Hooks appear in every React interview. Master 40+ questions on useState, useEffect, useMemo, custom hooks, and React 18 features with real code examples. Free, no paywall.

React Hooks fundamentally changed how developers write React - and they've dominated interview question banks ever since. This guide covers 40+ questions junior and mid-level developers face at product companies, organized from basic to advanced: state hooks, effect hooks, performance hooks, custom hooks, and React 18 additions like useTransition and useDeferredValue. Every question includes a clean code example and an interview tip.

reactreact-hooksinterview-questionsfrontendreact-2026coding-interviewuseEffectuseState
On this page
  1. 40+ React Hooks Interview Questions and Answers (2026)
  2. Section 1 — Basic React Hooks (Q1–Q12)
  3. Q1. What are React Hooks and why were they introduced?
  4. Q2. What are the Rules of Hooks?
  5. Q3. How does `useState` work?
  6. Q4. What is lazy initialization in `useState`?
  7. Q5. How does `useEffect` work and what does the dependency array do?
  8. Q6. How do you clean up in `useEffect`?
  9. Q7. What is the difference between `useEffect` and `useLayoutEffect`?
  10. Q8. What is `useRef` used for?
  11. Q9. What is `useContext` and when should you use it?
  12. Q10. How does `useReducer` differ from `useState`?
  13. Q11. What is `useCallback` and when should you use it?
  14. Q12. What is `useMemo` and how is it different from `useCallback`?
  15. Section 2 — Advanced React Hooks (Q13–Q22)
  16. Q13. What is `useTransition` and when would you use it? (React 18)
  17. Q14. What is `useDeferredValue`? How does it differ from `useTransition`? (React 18)
  18. Q15. What is a stale closure in React hooks and how do you fix it?
  19. Q16. What happens when you call `setState` multiple times in one event handler?
  20. Q17. What is `useImperativeHandle` and when would you use it?
  21. Q18. How do you fetch data correctly with `useEffect`?
  22. Q19. What is the `useId` hook? (React 18)
  23. Q20. How do you conditionally skip a `useEffect`?
  24. Q21. What does the `use` hook do? (React 19)
  25. Q22. How do you avoid infinite loops in `useEffect`?
  26. Section 3 — Custom Hooks (Q23–Q29)
  27. Q23. What are custom hooks and when should you create one?
  28. Q24. Build a `useLocalStorage` hook
  29. Q25. Build a `useDebounce` hook
  30. Q26. Build a `useMediaQuery` hook
  31. Q27. What patterns make custom hooks composable?
  32. Q28. Can custom hooks call other custom hooks?
  33. Q29. How do you test a custom hook?
  34. Section 4 — Performance & Best Practices (Q30–Q37)
  35. Q30. How do you prevent unnecessary re-renders with hooks?
  36. Q31. What is the relationship between `React.memo` and hooks?
  37. Q32. What are common dependency array mistakes?
  38. Q33. Can you use hooks in class components?
  39. Q34. How do you handle forms with hooks?
  40. Q35. How do you share state logic between components without prop drilling?
  41. Q36. What are the performance implications of context overuse?
  42. Q37. What is React's reconciliation algorithm and how does it affect hook design?
  43. Section 5 — Live Coding Interview Scenarios (Q38–Q42)
  44. Q38. Build a counter with increment, decrement, and reset
  45. Q39. Fetch and display paginated data
  46. Q40. Implement a toggle hook
  47. Q41. Build a `usePrevious` hook
  48. Q42. Build a `useOnClickOutside` hook
  49. Frequently Asked Questions
  50. What React Hooks are asked most frequently in interviews?
  51. What's the difference between `useMemo` and `useCallback` in one sentence?
  52. How do I answer "what are the rules of hooks" without sounding like I'm reciting a list?
  53. Do I need to know React 18 hooks like `useTransition` for a mid-level interview?
  54. Should I use `useEffect` for data fetching in 2026?
  55. Conclusion
Practice

Test your knowledge

Real interview questions asked at top product companies.

Practice Now
More Articles

React Hooks have appeared in virtually every React interview since they shipped in React 16.8 — and for good reason. Hooks replaced class component lifecycle methods, enabled logic reuse across components, and set the pattern for everything in modern React including Server Components and the concurrent renderer. React is used by 40.6% of all professional developers today, making it the most-used frontend library for five consecutive years (Stack Overflow Developer Survey 2025). That market share translates directly into hiring volume — and hooks knowledge is table stakes for every React role.

This guide answers 40+ hooks questions across five categories, each with a realistic code example and a tip on what the interviewer is actually checking. All content is free — no login, no paywall.

Key Takeaways

  • React Hooks shipped in React 16.8 (February 2019) and are now used in 95%+ of new React codebases

  • useState, useEffect, useCallback, useMemo, and useRef appear in virtually every React interview — know all five cold

  • React 18 added useTransition and useDeferredValue for concurrent rendering; these are the differentiating questions at mid-to-senior levels

  • Custom hooks are the most common live-coding task — build useFetch, useDebounce, and useLocalStorage from memory before your interview

React interview questions guide → complete guide covering all 50 React interview questions including hooks, patterns, and Server Components


Section 1 — Basic React Hooks (Q1–Q12)

React introduced Hooks in February 2019 with React 16.8, and the community adopted them almost immediately — within two years, hooks were the dominant pattern in production React codebases. Basic hooks questions test whether you understand the mental model behind state and side effects in functional components, not just the syntax.

JavaScript and React code with hooks syntax on a dark code editor screen
JavaScript and React code with hooks syntax on a dark code editor screen

Q1. What are React Hooks and why were they introduced?

React Hooks are functions that let you use state, lifecycle features, and other React capabilities inside functional components — without writing a class. They were introduced in React 16.8 to solve three problems that plagued class components: difficulty sharing stateful logic between components, complex lifecycle methods that did unrelated things in one place, and confusing this behavior that tripped up even experienced developers.

// Before hooks: you needed a class to use state
class Counter extends React.Component {
  state = { count: 0 };
  render() {
    return <button onClick={() => this.setState({ count: this.state.count + 1 })}>
      {this.state.count}
    </button>;
  }
}

// After hooks: a plain function does the same thing
function Counter() {
  const [count, setCount] = React.useState(0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

Interview tip: Interviewers want more than "hooks let functional components have state." Lead with the three problems hooks solved — reusability, separation of concerns, and this confusion — to show you understand the design motivation.


Q2. What are the Rules of Hooks?

There are exactly two rules. First, only call hooks at the top level of a function — never inside loops, conditionals, or nested functions. Second, only call hooks from React function components or custom hooks — never from regular JavaScript functions. These rules exist because React tracks hook call order to associate state with each hook; any conditional or loop would break that sequence.

// WRONG — hook inside a condition breaks the order React expects
function UserProfile({ isLoggedIn }) {
  if (isLoggedIn) {
    const [name, setName] = useState(''); // ❌ conditional hook call
  }
}

// CORRECT — condition lives inside the hook's callback, not around it
function UserProfile({ isLoggedIn }) {
  const [name, setName] = useState('');
  useEffect(() => {
    if (isLoggedIn) {
      fetchUserName().then(setName);
    }
  }, [isLoggedIn]);
}

Interview tip: If asked why these rules exist, explain that React relies on the stable, ordered list of hook calls across renders to link each useState and useEffect call to its stored value. Conditional calls would shift the index, causing bugs that are extremely hard to trace.


Q3. How does `useState` work?

useState returns a state variable and a setter function. When you call the setter, React schedules a re-render and replaces the state value with the new one. The state value from useState is read-only during the current render — you always get the value captured when the component rendered, not a live reference.

import { useState } from 'react';

function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState(null);

  async function handleSubmit(e) {
    e.preventDefault();
    try {
      await loginUser({ email, password });
    } catch (err) {
      setError(err.message); // triggers a re-render with the error message
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={e => setEmail(e.target.value)} />
      <input type="password" value={password} onChange={e => setPassword(e.target.value)} />
      {error && <p className="error">{error}</p>}
      <button type="submit">Login</button>
    </form>
  );
}

Interview tip: Know the functional updater form: setCount(prev => prev + 1). Use it whenever the new state depends on the old state — this avoids stale closure bugs, especially inside async callbacks.


Q4. What is lazy initialization in `useState`?

If computing the initial state is expensive, you can pass a function to useState instead of a value. React calls that function only on the first render — subsequent renders ignore it entirely. This is called lazy initialization and it prevents re-running expensive operations like parsing localStorage or filtering large arrays on every render.

// Without lazy init — parses localStorage on EVERY render (wasteful)
const [cart, setCart] = useState(JSON.parse(localStorage.getItem('cart') || '[]'));

// With lazy init — parses localStorage only ONCE on mount
const [cart, setCart] = useState(() => {
  const stored = localStorage.getItem('cart');
  return stored ? JSON.parse(stored) : [];
});

Interview tip: Interviewers sometimes catch candidates off guard with "why would you pass a function to useState?" The answer isn't about functions as values — it's about deferring expensive computation to first render only.


Q5. How does `useEffect` work and what does the dependency array do?

useEffect runs a side effect after the component renders. The dependency array controls when the effect re-runs: an empty array ([]) means run once after mount, a list of values means re-run whenever any of those values change, and omitting the array means re-run after every render. The effect runs asynchronously — after the browser has painted — so it doesn't block the UI.

import { useState, useEffect } from 'react';

function UserCard({ userId }) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    setIsLoading(true);
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setIsLoading(false);
      });
  }, [userId]); // re-fetch whenever userId changes

  if (isLoading) return <p>Loading...</p>;
  return <div>{user?.name}</div>;
}

Interview tip: Be ready to explain what happens if you omit the dependency array entirely. The answer: the effect runs after every single render, which almost always causes an infinite loop when the effect itself triggers a state update.


Q6. How do you clean up in `useEffect`?

Return a function from your effect callback. React calls that cleanup function before running the effect again (on the next render) and when the component unmounts. Cleanup is essential for subscriptions, event listeners, timers, and WebSocket connections — skipping it causes memory leaks.

useEffect(() => {
  const controller = new AbortController();

  fetch(`/api/notifications`, { signal: controller.signal })
    .then(res => res.json())
    .then(setNotifications)
    .catch(err => {
      if (err.name !== 'AbortError') console.error(err);
    });

  // cleanup: cancel the in-flight request if userId changes or component unmounts
  return () => controller.abort();
}, []);
// Another common pattern — event listener cleanup
useEffect(() => {
  function handleResize() {
    setWindowWidth(window.innerWidth);
  }
  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize); // ✅ always clean up
}, []);

Interview tip: "What happens if you don't clean up a setInterval?" is a classic follow-up. The timer keeps firing even after the component unmounts, trying to call setState on an unmounted component. It won't crash React 18+ (the warning was removed), but it wastes CPU and can cause logic bugs.


Q7. What is the difference between `useEffect` and `useLayoutEffect`?

Both run after render, but at different moments. useEffect runs asynchronously after the browser has painted the screen — it's the default choice for data fetching, subscriptions, and logging. useLayoutEffect runs synchronously after DOM mutations but before the browser paints — use it when you need to measure a DOM element or apply a correction that must be invisible to the user (like repositioning a tooltip).

// useLayoutEffect — measure DOM before paint to avoid flicker
function Tooltip({ targetRef, content }) {
  const tooltipRef = useRef(null);

  useLayoutEffect(() => {
    const targetRect = targetRef.current.getBoundingClientRect();
    const tooltipEl = tooltipRef.current;
    // position tooltip above/below the target without a visible jump
    tooltipEl.style.top = `${targetRect.bottom + 8}px`;
    tooltipEl.style.left = `${targetRect.left}px`;
  });

  return <div ref={tooltipRef} className="tooltip">{content}</div>;
}

Interview tip: The rule of thumb: default to useEffect. Switch to useLayoutEffect only when you see a visual flash or flicker caused by a DOM measurement. Also note that useLayoutEffect doesn't run during server-side rendering — it'll produce a warning in Next.js if misused.


Q8. What is `useRef` used for?

useRef returns a mutable object ({ current: value }) that persists across renders without triggering a re-render when changed. It has two main uses: holding a reference to a DOM element, and storing any mutable value that you want to persist but that shouldn't cause a re-render when it changes (like a timer ID, previous prop value, or a flag).

function SearchInput({ onSearch }) {
  const inputRef = useRef(null); // DOM reference
  const debounceTimer = useRef(null); // mutable value, no re-render needed

  function handleChange(e) {
    clearTimeout(debounceTimer.current);
    debounceTimer.current = setTimeout(() => {
      onSearch(e.target.value);
    }, 300);
  }

  // Focus the input on mount
  useEffect(() => {
    inputRef.current.focus();
  }, []);

  return <input ref={inputRef} onChange={handleChange} />;
}

Interview tip: A common follow-up is "why not just use a regular variable instead of useRef?" Regular variables are re-created on every render, so they can't hold state across renders. useRef gives you a stable container that survives re-renders without causing them.


Q9. What is `useContext` and when should you use it?

useContext subscribes a component to a React context value without prop drilling. Any component wrapped in the context's Provider can call useContext(MyContext) to read and react to the current value. When the Provider's value prop changes, all consumers re-render.

const ThemeContext = React.createContext('light');

function App() {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Sidebar />
      <MainContent />
    </ThemeContext.Provider>
  );
}

// Any descendant — no matter how deep — can read the theme
function ThemeToggle() {
  const { theme, setTheme } = useContext(ThemeContext);
  return (
    <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
      Current: {theme}
    </button>
  );
}

Interview tip: Context isn't a state management solution for high-frequency updates. If the value changes often (e.g., cursor position, filter values), every consumer re-renders. For that use case, Zustand or Redux is a better fit. Context shines for low-frequency values: auth state, theme, locale.


Q10. How does `useReducer` differ from `useState`?

useReducer manages state through a reducer function (state, action) => newState — the same pattern as Redux. It's better than useState when state has multiple sub-values that change together, when the next state depends on the current state in complex ways, or when you want co-located, testable state logic. useState is simpler for isolated, independent values.

const initialState = { items: [], isLoading: false, error: null };

function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return { ...state, items: [...state.items, action.payload] };
    case 'REMOVE_ITEM':
      return { ...state, items: state.items.filter(i => i.id !== action.payload) };
    case 'SET_LOADING':
      return { ...state, isLoading: action.payload };
    case 'SET_ERROR':
      return { ...state, error: action.payload, isLoading: false };
    default:
      return state;
  }
}

function Cart() {
  const [state, dispatch] = useReducer(cartReducer, initialState);

  function addToCart(product) {
    dispatch({ type: 'ADD_ITEM', payload: product });
  }

  return (
    <div>
      {state.items.map(item => <CartItem key={item.id} item={item} dispatch={dispatch} />)}
    </div>
  );
}

Interview tip: Interviewers often ask when you'd choose useReducer over useState. The answer: when you have 3+ related state fields that update together, or when the next state depends on multiple previous values at once. A shopping cart, form with validation, or wizard step state are all good candidates.


Q11. What is `useCallback` and when should you use it?

useCallback returns a memoized version of a callback function. It only creates a new function reference when one of its dependencies changes. This matters because functions defined inside a component get a new reference on every render — which breaks referential equality checks in React.memo child components or useEffect dependency arrays.

function ProductList({ categoryId, onProductSelect }) {
  const [products, setProducts] = useState([]);

  // Without useCallback — new function reference every render
  // → ProductCard re-renders even if products haven't changed
  const handleSelect = (product) => onProductSelect(product);

  // With useCallback — stable reference until onProductSelect changes
  const handleSelect = useCallback(
    (product) => onProductSelect(product),
    [onProductSelect]
  );

  return products.map(p => (
    <ProductCard key={p.id} product={p} onSelect={handleSelect} />
  ));
}

// Wrapped in React.memo — won't re-render unless its props change
const ProductCard = React.memo(({ product, onSelect }) => (
  <div onClick={() => onSelect(product)}>{product.name}</div>
));

Interview tip: useCallback only helps when the wrapped function is passed to a React.memo component or used in a useEffect dependency array. Wrapping every callback blindly adds overhead without benefit.


Q12. What is `useMemo` and how is it different from `useCallback`?

useMemo memoizes the result of a computation and only recomputes when dependencies change. useCallback memoizes the function itself. Think of it this way: useMemo(() => fn(), deps) is equivalent to useCallback(fn, deps) — except useMemo calls the function and returns the value, while useCallback returns the function without calling it.

function OrderSummary({ orders, filter }) {
  // useMemo — cache the filtered + sorted result, not the function
  const processedOrders = useMemo(() => {
    console.log('Recomputing orders...');
    return orders
      .filter(o => o.status === filter)
      .sort((a, b) => new Date(b.date) - new Date(a.date));
  }, [orders, filter]); // only recompute when orders or filter changes

  // useCallback — cache the function reference for a child component
  const handleRefund = useCallback((orderId) => {
    initiateRefund(orderId);
  }, []); // stable reference — no dependencies

  return (
    <div>
      {processedOrders.map(order => (
        <OrderRow key={order.id} order={order} onRefund={handleRefund} />
      ))}
    </div>
  );
}

Interview tip: The most common follow-up is "when should you NOT use these hooks?" The answer: when the computation is cheap (string formatting, arithmetic, small arrays) or when the component rarely re-renders. Memoization itself has a cost — the comparison check on every render. Profile before optimizing.


Section 2 — Advanced React Hooks (Q13–Q22)

Advanced hooks questions move beyond syntax into timing, edge cases, and React 18's concurrent features. According to internal hiring data from multiple mid-size product companies, candidates who can discuss useTransition, stale closures, and dependency array pitfalls in detail score significantly higher in technical screens than those who only know basic hook APIs.

Pattern we see in interviews: Companies using React 18 in production — especially those running Next.js 13+ or Remix — now regularly ask about useTransition and useDeferredValue. If you're targeting a mid-to-senior role at a product company, treating these as optional prep is a mistake.

React 18 concurrent rendering and hooks flow diagram concept on a developer workstation
React 18 concurrent rendering and hooks flow diagram concept on a developer workstation

Q13. What is `useTransition` and when would you use it? (React 18)

useTransition lets you mark a state update as non-urgent, telling React it's safe to interrupt that update to handle more urgent work like user input. It returns an [isPending, startTransition] pair. Wrap low-priority updates (navigation, filtering large lists, tab switching) in startTransition so they don't block typing or clicking.

import { useState, useTransition } from 'react';

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

  function handleSearch(e) {
    const value = e.target.value;
    setQuery(value); // urgent — update the input immediately

    startTransition(() => {
      // non-urgent — React can defer this if the user keeps typing
      const filtered = hugeProductList.filter(p =>
        p.name.toLowerCase().includes(value.toLowerCase())
      );
      setResults(filtered);
    });
  }

  return (
    <>
      <input value={query} onChange={handleSearch} placeholder="Search products..." />
      {isPending ? <Spinner /> : <ResultList results={results} />}
    </>
  );
}

Interview tip: The key insight is that startTransition doesn't delay the update — it marks it as interruptible. If the user types another character before the transition finishes, React throws away the in-progress work and starts fresh with the new input. This is fundamentally different from debouncing.


Q14. What is `useDeferredValue`? How does it differ from `useTransition`? (React 18)

useDeferredValue accepts a value and returns a deferred version that lags behind the source value when React is busy. It's the hook equivalent of startTransition for cases where you don't control the state update — for example, when a prop comes from a parent you can't modify.

import { useState, useDeferredValue, memo } from 'react';

function SearchResults({ query }) {
  // The deferred version stays on the old value while React renders the new one
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;

  return (
    <div style={{ opacity: isStale ? 0.7 : 1 }}>
      {/* This expensive list only re-renders when deferredQuery settles */}
      <ExpensiveResultList query={deferredQuery} />
    </div>
  );
}

// Use useTransition when you own the state update
// Use useDeferredValue when you receive the value as a prop

Interview tip: The difference in one sentence: use useTransition when you own the state setter, use useDeferredValue when you receive the value from outside (a prop or context). Both achieve similar outcomes — keeping the UI responsive — but from different ownership positions.


Q15. What is a stale closure in React hooks and how do you fix it?

A stale closure occurs when an effect or callback captures a variable (usually state or props) from a previous render and keeps using its old value even after it's been updated. This is one of the most common bugs in React codebases.

// BUG — stale closure: count inside setInterval is always 0
function StaleCounter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1); // ❌ always reads count = 0 from closure
    }, 1000);
    return () => clearInterval(id);
  }, []); // empty deps means effect runs once — count is forever 0

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

// FIX 1 — functional updater doesn't need to read count from closure
function FixedCounter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(prev => prev + 1); // ✅ always gets the latest count
    }, 1000);
    return () => clearInterval(id);
  }, []);

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

// FIX 2 — useRef to hold latest value without causing re-renders
function FixedCounterRef() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);
  countRef.current = count;

  useEffect(() => {
    const id = setInterval(() => {
      setCount(countRef.current + 1); // ✅ always reads latest via ref
    }, 1000);
    return () => clearInterval(id);
  }, []);

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

Interview tip: Stale closures are the single most common hooks bug in production. Interviewers often present a broken component and ask you to spot the problem. Look for: effects with empty dependency arrays that use state/props inside, and callbacks that read state without using the functional updater form.


Q16. What happens when you call `setState` multiple times in one event handler?

In React 18, all state updates inside event handlers, Promises, setTimeout callbacks, and native event handlers are automatically batched into a single re-render. In React 17, only updates inside React event handlers were batched — async updates triggered separate renders.

function ProfileEditor() {
  const [name, setName] = useState('');
  const [bio, setBio] = useState('');
  const [avatar, setAvatar] = useState(null);

  async function handleSave() {
    const data = await saveProfile({ name, bio, avatar });

    // React 18 — all three trigger ONE re-render (automatic batching in async context)
    setName(data.name);
    setBio(data.bio);
    setAvatar(data.avatarUrl);
  }

  // To opt OUT of batching (rare): wrap in ReactDOM.flushSync
  function handleImmediateUpdate() {
    ReactDOM.flushSync(() => setName('Abhijeet'));
    // DOM is updated here, before the next line runs
    console.log('Name updated in DOM');
    setBio('Updated bio');
  }
}

Interview tip: React 18's automatic batching is often presented as a "what changed in React 18" question. The concrete implication: if you previously had hacks like wrapping async setState calls in setTimeout(fn, 0) to control render timing, React 18 renders them more efficiently by default.


Q17. What is `useImperativeHandle` and when would you use it?

useImperativeHandle customizes what a parent component sees when it holds a ref to a child. Instead of exposing the raw DOM node, you expose a curated API. It's used with forwardRef and is mainly needed for component library authors or when you want to expose focus(), scrollTo(), or reset() methods without leaking the entire DOM element.

import { useRef, useImperativeHandle, forwardRef } from 'react';

const FancyInput = forwardRef(function FancyInput(props, ref) {
  const inputRef = useRef(null);

  useImperativeHandle(ref, () => ({
    focus() {
      inputRef.current.focus();
    },
    clear() {
      inputRef.current.value = '';
    },
    getValue() {
      return inputRef.current.value;
    }
    // The parent cannot access inputRef.current directly — only this API
  }));

  return <input ref={inputRef} className="fancy-input" {...props} />;
});

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

  return (
    <>
      <FancyInput ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>Focus</button>
      <button onClick={() => inputRef.current.clear()}>Clear</button>
    </>
  );
}

Interview tip: This hook is rarely needed in application code. If an interviewer asks about it, they're testing whether you know the full hooks API, not expecting you to use it daily. The correct answer includes: you only need it for imperative APIs, and it's specifically designed for component libraries and design systems.


Q18. How do you fetch data correctly with `useEffect`?

The correct pattern handles three concerns: an abort signal to cancel in-flight requests when the component unmounts or deps change, loading and error states, and avoiding setting state on an unmounted component.

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [status, setStatus] = useState('idle'); // 'idle' | 'loading' | 'error' | 'success'

  useEffect(() => {
    if (!userId) return;

    const controller = new AbortController();
    setStatus('loading');

    fetch(`/api/users/${userId}`, { signal: controller.signal })
      .then(res => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then(data => {
        setUser(data);
        setStatus('success');
      })
      .catch(err => {
        if (err.name === 'AbortError') return; // component unmounted — ignore
        setStatus('error');
      });

    return () => controller.abort();
  }, [userId]);

  if (status === 'loading') return <Skeleton />;
  if (status === 'error') return <ErrorMessage />;
  if (!user) return null;
  return <ProfileCard user={user} />;
}

Interview tip: In a real codebase, reach for React Query or SWR instead of raw useEffect data fetching. But interviewers ask about the raw pattern to test your understanding of cleanup, race conditions, and async state management. Showing that you'd use a library in production while knowing the underlying mechanics is the ideal answer.


Q19. What is the `useId` hook? (React 18)

useId generates a stable, unique ID that's consistent between server and client renders. It solves a specific problem: generating IDs for accessibility attributes (like htmlFor/id pairs on form labels) without causing hydration mismatches in server-rendered apps.

import { useId } from 'react';

function FormField({ label, type = 'text' }) {
  const id = useId(); // generates something like ":r3:"

  return (
    <div className="field">
      <label htmlFor={id}>{label}</label>
      <input id={id} type={type} />
    </div>
  );
}

// Safe to render multiple FormField instances — each gets a unique ID
function SignupForm() {
  return (
    <form>
      <FormField label="Email" type="email" />
      <FormField label="Username" />
      <FormField label="Password" type="password" />
    </form>
  );
}

Interview tip: Before useId, developers used Math.random() or incrementing counters for generated IDs — both caused hydration mismatches in SSR. useId is the official solution. Note: don't use it as a database key or list key — it's for accessibility attribute wiring only.


Q20. How do you conditionally skip a `useEffect`?

You can't skip calling useEffect (that violates the rules of hooks), but you can put a guard condition at the top of the effect body to bail out early.

// WRONG — conditional hook call
function Component({ shouldFetch, userId }) {
  if (shouldFetch) {
    useEffect(() => fetch(`/api/${userId}`), [userId]); // ❌ breaks rules of hooks
  }
}

// CORRECT — condition lives inside the effect
function Component({ shouldFetch, userId }) {
  useEffect(() => {
    if (!shouldFetch || !userId) return; // ✅ early return inside the effect
    fetch(`/api/users/${userId}`).then(/* ... */);
  }, [shouldFetch, userId]);
}

Interview tip: This is a classic rules-of-hooks gotcha. The answer pattern is always the same: move the condition inside the hook, not around it. Include shouldFetch in the dependency array so the effect re-evaluates when the flag changes.


Q21. What does the `use` hook do? (React 19)

use is a new React 19 primitive that reads the value of a Promise or context. Unlike other hooks, it can be called inside loops and conditionals. When used with a Promise, React suspends the component until the Promise resolves, integrating natively with boundaries.

// React 19 — use() with a Promise + Suspense
function UserCard({ userPromise }) {
  const user = use(userPromise); // suspends until resolved
  return <div>{user.name}</div>;
}

function App() {
  const userPromise = fetchUser(userId); // created outside the component

  return (
    <Suspense fallback={<Skeleton />}>
      <UserCard userPromise={userPromise} />
    </Suspense>
  );
}

Interview tip: use is React 19 and most teams haven't adopted it yet. Mentioning it signals you're tracking the ecosystem. The key differentiator: it's the first hook that isn't limited to the top level of a component — you can call it conditionally, which opens new patterns for data loading inside render.


Q22. How do you avoid infinite loops in `useEffect`?

Infinite loops in useEffect happen when the effect updates state that's listed in the dependency array, creating a cycle: render → effect → state update → re-render → effect → … The fix is to remove the cycling value from the deps, use the functional updater form, or restructure the logic.

// BUG — infinite loop: effect updates 'items', 'items' is a dep
useEffect(() => {
  setItems(items.map(i => ({ ...i, processed: true }))); // ❌ triggers re-render → effect → loop
}, [items]);

// FIX 1 — functional updater: no need to read 'items' from closure
useEffect(() => {
  setItems(prev => prev.map(i => ({ ...i, processed: true })));
}, []); // ✅ runs once on mount

// FIX 2 — process outside the effect entirely (often the real solution)
const processedItems = useMemo(() => items.map(i => ({ ...i, processed: true })), [items]);

Interview tip: If an interviewer shows you a component with an infinite loop, first check whether a state setter is called with a value derived from a dep. The eslint-plugin-react-hooks exhaustive-deps rule catches most of these at write time — mention that you'd rely on the linter in a real project.


Section 3 — Custom Hooks (Q23–Q29)

Custom hooks are the most common live-coding task in React interviews. They test whether you can abstract reusable logic — the core promise of hooks. Every custom hook is a function whose name starts with use and that may call other hooks internally.

Q23. What are custom hooks and when should you create one?

A custom hook is any JavaScript function that starts with use and calls other hooks. You should extract logic into a custom hook when: the same stateful logic appears in two or more components, a component has several related state and effect values that logically belong together, or you want to make complex effects testable in isolation.

// Logic scattered across multiple components — time to extract
// Component A and Component B both have this identical block:
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => { /* fetch logic */ }, [url]);

// Extract it into a custom hook once, reuse everywhere
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    if (!url) return;
    setLoading(true);
    const controller = new AbortController();

    fetch(url, { signal: controller.signal })
      .then(res => res.json())
      .then(data => { setData(data); setLoading(false); })
      .catch(err => { if (err.name !== 'AbortError') setError(err.message); setLoading(false); });

    return () => controller.abort();
  }, [url]);

  return { data, loading, error };
}

// Usage in any component
function BlogPost({ slug }) {
  const { data: post, loading, error } = useFetch(`/api/posts/${slug}`);
  if (loading) return <Skeleton />;
  if (error) return <p>{error}</p>;
  return <article>{post?.content}</article>;
}

Interview tip: The interviewer is checking whether you understand that custom hooks are about logic reuse, not UI reuse (that's components). Every custom hook should have a clear, single responsibility — if it does too much, split it.


Q24. Build a `useLocalStorage` hook

useLocalStorage syncs React state with localStorage, persisting values across page refreshes. The implementation needs lazy initialization, JSON serialization, and a consistent setter API matching useState.

function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (err) {
      console.warn(`useLocalStorage: failed to read key "${key}"`, err);
      return initialValue;
    }
  });

  const setValue = useCallback((value) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (err) {
      console.warn(`useLocalStorage: failed to write key "${key}"`, err);
    }
  }, [key, storedValue]);

  return [storedValue, setValue];
}

// Usage — drop-in replacement for useState
function Settings() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  return <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>{theme}</button>;
}

Interview tip: Interviewers look for: lazy initialization (not recalculating on every render), the functional updater support (value instanceof Function), and error handling for SSR environments where window doesn't exist. Missing any one of these is a gap they'll note.


Q25. Build a `useDebounce` hook

useDebounce delays updating a value until the user has stopped changing it for a given delay period. It's the building block for search-as-you-type, autocomplete, and form validation.

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

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer); // clears timer if value changes before delay
  }, [value, delay]);

  return debouncedValue;
}

// Usage in a search component
function ProductSearch() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 400);

  const { data: results } = useFetch(
    debouncedQuery ? `/api/search?q=${debouncedQuery}` : null
  );

  return (
    <>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Search products..."
      />
      <ResultList results={results} />
    </>
  );
}

Interview tip: This is one of the top three custom hooks asked in live-coding interviews. Build it from memory: useState for the debounced value, useEffect with setTimeout that clears on cleanup. A common follow-up: "How would you also debounce a callback function?" — that's a useDebounceCallback using useRef and useCallback.


Q26. Build a `useMediaQuery` hook

useMediaQuery returns a boolean indicating whether a CSS media query matches, enabling responsive behavior in JavaScript logic without CSS-only solutions.

function useMediaQuery(query) {
  const [matches, setMatches] = useState(() => {
    if (typeof window === 'undefined') return false; // SSR safety
    return window.matchMedia(query).matches;
  });

  useEffect(() => {
    if (typeof window === 'undefined') return;
    const mediaQuery = window.matchMedia(query);
    setMatches(mediaQuery.matches);

    function handleChange(e) {
      setMatches(e.matches);
    }

    mediaQuery.addEventListener('change', handleChange);
    return () => mediaQuery.removeEventListener('change', handleChange);
  }, [query]);

  return matches;
}

// Usage
function ResponsiveNav() {
  const isMobile = useMediaQuery('(max-width: 768px)');
  return isMobile ? <MobileNav /> : <DesktopNav />;
}

Interview tip: SSR safety is the detail that separates good answers from great ones. Always check typeof window === 'undefined' before touching browser APIs in a hook. This is especially critical in Next.js where components render on the server first.


Q27. What patterns make custom hooks composable?

Good custom hooks follow the same composability rules as Unix tools: do one thing well, accept inputs via parameters, and return clear outputs. Compose complex behavior by combining smaller hooks rather than building monolithic ones.

// Small, composable hooks
function useBoolean(initial = false) {
  const [value, setValue] = useState(initial);
  const toggle = useCallback(() => setValue(v => !v), []);
  const setTrue = useCallback(() => setValue(true), []);
  const setFalse = useCallback(() => setValue(false), []);
  return { value, toggle, setTrue, setFalse };
}

function useAsync(asyncFn, deps) {
  const [state, setState] = useState({ status: 'idle', data: null, error: null });

  useEffect(() => {
    setState({ status: 'loading', data: null, error: null });
    asyncFn()
      .then(data => setState({ status: 'success', data, error: null }))
      .catch(error => setState({ status: 'error', data: null, error }));
  }, deps);

  return state;
}

// Compose them into a more specific hook
function useModal(fetchDataFn) {
  const { value: isOpen, setTrue: open, setFalse: close } = useBoolean(false);
  const { status, data } = useAsync(fetchDataFn, [isOpen]);

  return { isOpen, open, close, status, data };
}

Interview tip: Composability is the interviewer's real question behind "how do you share logic between custom hooks?" Show that you think in building blocks — useBoolean, useAsync, useDebounce as atomic primitives that combine into higher-level hooks like useModal or useInfiniteScroll.


Q28. Can custom hooks call other custom hooks?

Yes, and this is encouraged. Custom hooks compose just like regular functions — any hook can call another hook, as long as the overall call order is stable across renders.

// A hook that composes three other hooks
function useProductSearch() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 350);
  const { data: results, loading, error } = useFetch(
    debouncedQuery ? `/api/products/search?q=${encodeURIComponent(debouncedQuery)}` : null
  );

  return { query, setQuery, results, loading, error };
}

// Component is completely clean — all complexity is in the hook
function ProductSearchPage() {
  const { query, setQuery, results, loading } = useProductSearch();
  return (
    <main>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      {loading ? <Spinner /> : <ProductGrid products={results} />}
    </main>
  );
}

Interview tip: This is the answer to "how would you refactor a complex component with 200 lines of hooks?" — extract related hook calls into a domain-specific custom hook. The component becomes a thin connector between hooks and JSX.


Q29. How do you test a custom hook?

Use @testing-library/react's renderHook utility to mount a hook in isolation, act to trigger state updates, and waitFor to handle async effects.

import { renderHook, act, waitFor } from '@testing-library/react';
import { useFetch } from './useFetch';

// Mock fetch globally
global.fetch = jest.fn();

describe('useFetch', () => {
  it('returns data on success', async () => {
    fetch.mockResolvedValueOnce({
      ok: true,
      json: () => Promise.resolve({ id: 1, name: 'Test User' }),
    });

    const { result } = renderHook(() => useFetch('/api/users/1'));

    expect(result.current.loading).toBe(true);

    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });

    expect(result.current.data).toEqual({ id: 1, name: 'Test User' });
    expect(result.current.error).toBeNull();
  });
});

Interview tip: renderHook is the key API to mention. Without it, you'd have to create a dummy component just to test the hook — renderHook wraps that pattern for you. If the interviewer asks about testing hooks that use context, pass a wrapper option to renderHook with your Provider.


Section 4 — Performance & Best Practices (Q30–Q37)

According to React's own profiler documentation, unnecessary re-renders are the most common cause of React performance problems in production applications. This section covers how to identify and prevent them using hooks.

Q30. How do you prevent unnecessary re-renders with hooks?

Three tools work together: React.memo prevents child components from re-rendering when parent state changes but their props haven't, useCallback keeps function prop references stable across renders, and useMemo keeps derived value references stable. They only help when used together for the same component.

// Problem: ParentComponent re-renders → new 'handleDelete' ref → TableRow re-renders
function OrdersPage() {
  const [orders, setOrders] = useState([]);
  const [filter, setFilter] = useState('all');

  // Step 1: stable function reference
  const handleDelete = useCallback((id) => {
    setOrders(prev => prev.filter(o => o.id !== id));
  }, []);

  // Step 2: stable derived value
  const filteredOrders = useMemo(() =>
    filter === 'all' ? orders : orders.filter(o => o.status === filter),
    [orders, filter]
  );

  return (
    <div>
      <FilterBar filter={filter} onChange={setFilter} />
      {filteredOrders.map(order => (
        // Step 3: memoized child — only re-renders when order or handleDelete changes
        <OrderRow key={order.id} order={order} onDelete={handleDelete} />
      ))}
    </div>
  );
}

const OrderRow = React.memo(function OrderRow({ order, onDelete }) {
  return (
    <tr>
      <td>{order.id}</td>
      <td>{order.status}</td>
      <td><button onClick={() => onDelete(order.id)}>Delete</button></td>
    </tr>
  );
});

Interview tip: "Profile before you optimize" is the answer interviewers want to hear alongside the technical solution. The React DevTools Profiler shows which components re-render and why. Adding memo + useCallback to every component adds overhead — only apply them where the profiler shows a real problem.


Q31. What is the relationship between `React.memo` and hooks?

React.memo is a higher-order component (HOC) that memoizes the render output of a functional component. It's not a hook, but it works hand-in-hand with useCallback and useMemo — without stable props, React.memo can't do its job.

// React.memo does a SHALLOW comparison of props by default
// If you pass objects or arrays as props, they get new references every render

// Problem — object prop breaks React.memo
const UserBadge = React.memo(({ config }) => <Badge {...config} />);

function Parent() {
  return <UserBadge config={{ size: 'lg', variant: 'pro' }} />; // ❌ new object each render
}

// Fix — stable reference with useMemo
function Parent() {
  const config = useMemo(() => ({ size: 'lg', variant: 'pro' }), []);
  return <UserBadge config={config} />; // ✅ same reference across renders
}

// Custom comparator when you need deep comparison
const UserBadge = React.memo(
  ({ config }) => <Badge {...config} />,
  (prevProps, nextProps) => prevProps.config.size === nextProps.config.size
);

Q32. What are common dependency array mistakes?

The three most common mistakes are: missing dependencies (stale closures), object/array literals in deps (new reference every render causes loops), and over-specifying with values that never change.

const BASE_URL = 'https://api.example.com'; // stable constant — no need in deps

function ProductList({ categoryId }) {
  const [products, setProducts] = useState([]);

  // ❌ Missing dep: 'categoryId' not in array — stale closure bug
  useEffect(() => {
    fetch(`${BASE_URL}/products?cat=${categoryId}`).then(/* ... */);
  }, []);

  // ❌ Object in deps — new ref every render — infinite loop
  const params = { cat: categoryId, limit: 20 };
  useEffect(() => {
    fetch(`${BASE_URL}/products`, { params }).then(/* ... */);
  }, [params]);

  // ✅ Correct — primitive dep, no object literals
  useEffect(() => {
    fetch(`${BASE_URL}/products?cat=${categoryId}&limit=20`).then(/* ... */);
  }, [categoryId]);
}

Interview tip: The eslint-plugin-react-hooks rule react-hooks/exhaustive-deps catches missing deps automatically. Always enable it. The rule might suggest dependencies you think are unnecessary — before disabling with an eslint-ignore comment, understand why the rule flagged them.


Q33. Can you use hooks in class components?

No. Hooks only work inside function components and other custom hooks. If you're working with a class component, you have two options: convert it to a functional component, or wrap it in a functional component that uses hooks and passes values down as props.

// Can't use hooks in a class component — but can wrap it
function withAuth(WrappedComponent) {
  return function WithAuthWrapper(props) {
    const { user, isLoading } = useAuth(); // hook called in function component
    if (isLoading) return <Spinner />;
    return <WrappedComponent {...props} user={user} />;
  };
}

// Apply to the class component
class LegacyDashboard extends React.Component {
  render() {
    return <div>Welcome, {this.props.user.name}</div>;
  }
}

export default withAuth(LegacyDashboard);

Q34. How do you handle forms with hooks?

The simplest approach uses useState for each field. For complex forms with validation, use useReducer. For production, reach for React Hook Form — it minimizes re-renders by using uncontrolled inputs with refs rather than state.

// Controlled form with useReducer for multi-field validation
function reducer(state, action) {
  switch (action.type) {
    case 'SET_FIELD':
      return { ...state, values: { ...state.values, [action.field]: action.value } };
    case 'SET_ERROR':
      return { ...state, errors: { ...state.errors, [action.field]: action.message } };
    case 'CLEAR_ERROR':
      return { ...state, errors: { ...state.errors, [action.field]: null } };
    default:
      return state;
  }
}

function RegistrationForm() {
  const [state, dispatch] = useReducer(reducer, {
    values: { email: '', password: '', username: '' },
    errors: {},
  });

  function validateEmail(value) {
    if (!value.includes('@')) {
      dispatch({ type: 'SET_ERROR', field: 'email', message: 'Invalid email' });
    } else {
      dispatch({ type: 'CLEAR_ERROR', field: 'email' });
    }
  }

  return (
    <form>
      <input
        value={state.values.email}
        onChange={e => dispatch({ type: 'SET_FIELD', field: 'email', value: e.target.value })}
        onBlur={e => validateEmail(e.target.value)}
      />
      {state.errors.email && <span>{state.errors.email}</span>}
    </form>
  );
}

Interview tip: Mentioning React Hook Form signals production experience. The key reason it's preferred: it uses uncontrolled inputs (register + ref) so only the form itself re-renders on validation, not every field on every keystroke. Show you understand the performance difference, not just the API.


Q35. How do you share state logic between components without prop drilling?

Three main patterns: Context + useContext for widely-shared state, custom hooks for logic reuse without shared state, and external state managers (Zustand, Jotai) for global state that updates frequently.

// Pattern: custom hook + context for shared, updatable state
const CartContext = React.createContext(null);

function CartProvider({ children }) {
  const [items, dispatch] = useReducer(cartReducer, []);

  const addItem = useCallback((product) =>
    dispatch({ type: 'ADD', payload: product }), []);
  const removeItem = useCallback((id) =>
    dispatch({ type: 'REMOVE', payload: id }), []);
  const total = useMemo(() =>
    items.reduce((sum, item) => sum + item.price * item.qty, 0), [items]);

  return (
    <CartContext.Provider value={{ items, addItem, removeItem, total }}>
      {children}
    </CartContext.Provider>
  );
}

// Custom hook makes the consumption clean and type-safe
function useCart() {
  const ctx = useContext(CartContext);
  if (!ctx) throw new Error('useCart must be used within CartProvider');
  return ctx;
}

Q36. What are the performance implications of context overuse?

Every component that calls useContext(MyContext) re-renders when the context value changes — even if the specific piece of data it reads didn't change. This makes context inappropriate for high-frequency state like filter values, cursor positions, or real-time data.

// ❌ One big context — EVERY consumer re-renders on any change
const AppContext = createContext({
  user: null,        // changes rarely
  cart: [],          // changes on every add/remove
  theme: 'light',   // changes rarely
  notifications: [], // can change frequently
});

// ✅ Split by update frequency
const AuthContext = createContext(null);    // changes once on login
const ThemeContext = createContext('light'); // changes rarely
const CartContext = createContext(null);     // changes on user action
// Frequent updates → external store (Zustand) instead of context

Interview tip: The rule of thumb: split contexts by update frequency. If two values always update together, they can share a context. If one updates often and one rarely, separate them so consumers of the slow-changing value don't re-render on every fast update.


Q37. What is React's reconciliation algorithm and how does it affect hook design?

React's reconciler (Fiber) identifies what changed between renders using a diffing algorithm. For lists, it uses keys. For hooks, it uses call order. This is why hooks can't be called conditionally — the reconciler matches each hook call by position in the call stack, not by name.

// React tracks hooks by ORDER — this is why order must be stable
function MyComponent({ hasExtra }) {
  const [a] = useState(1); // Hook slot 0
  const [b] = useState(2); // Hook slot 1
  // if (hasExtra) { const [c] = useState(3); } // Would put something in slot 2 conditionally — BROKEN
  const [d] = useState(4); // Hook slot 2 (or 3 if conditional above was allowed)
}

// React's internal list per component instance:
// [{ type: 'state', value: 1 }, { type: 'state', value: 2 }, { type: 'state', value: 4 }]
// Call order = array index. Any shift = wrong state read.

Section 5 — Live Coding Interview Scenarios (Q38–Q42)

These are the hands-on problems interviewers give in real interviews. Work through each until you can solve them without looking at the solution.

Q38. Build a counter with increment, decrement, and reset

function useCounter(initialValue = 0, { min, max } = {}) {
  const [count, setCount] = useState(initialValue);

  const increment = useCallback(() =>
    setCount(c => max !== undefined ? Math.min(c + 1, max) : c + 1), [max]);

  const decrement = useCallback(() =>
    setCount(c => min !== undefined ? Math.max(c - 1, min) : c - 1), [min]);

  const reset = useCallback(() => setCount(initialValue), [initialValue]);

  return { count, increment, decrement, reset };
}

function Counter() {
  const { count, increment, decrement, reset } = useCounter(0, { min: 0, max: 10 });
  return (
    <div>
      <button onClick={decrement} disabled={count === 0}>-</button>
      <span>{count}</span>
      <button onClick={increment} disabled={count === 10}>+</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

Q39. Fetch and display paginated data

function usePaginatedFetch(baseUrl, pageSize = 10) {
  const [page, setPage] = useState(1);
  const [allData, setAllData] = useState([]);
  const [hasMore, setHasMore] = useState(true);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true);
    fetch(`${baseUrl}?page=${page}&limit=${pageSize}`)
      .then(res => res.json())
      .then(({ data, total }) => {
        setAllData(prev => page === 1 ? data : [...prev, ...data]);
        setHasMore(page * pageSize < total);
        setLoading(false);
      });
  }, [baseUrl, page, pageSize]);

  const loadMore = useCallback(() => setPage(p => p + 1), []);
  const reset = useCallback(() => { setPage(1); setAllData([]); }, []);

  return { data: allData, loading, hasMore, loadMore, reset };
}

Q40. Implement a toggle hook

function useToggle(initial = false) {
  const [state, setState] = useState(initial);
  const toggle = useCallback(() => setState(s => !s), []);
  return [state, toggle];
}

// Usage
function Accordion({ title, children }) {
  const [isOpen, toggleOpen] = useToggle(false);
  return (
    <div>
      <button onClick={toggleOpen}>{title} {isOpen ? '▲' : '▼'}</button>
      {isOpen && <div>{children}</div>}
    </div>
  );
}

Q41. Build a `usePrevious` hook

function usePrevious(value) {
  const ref = useRef(undefined);

  useEffect(() => {
    ref.current = value; // runs after render — ref holds the value from the previous render
  });

  return ref.current; // returns the value from BEFORE this render
}

function PriceTag({ price }) {
  const previousPrice = usePrevious(price);
  const direction = previousPrice !== undefined
    ? price > previousPrice ? '↑' : price < previousPrice ? '↓' : '–'
    : '';

  return <span>{price} {direction}</span>;
}

Q42. Build a `useOnClickOutside` hook

function useOnClickOutside(ref, handler) {
  useEffect(() => {
    function listener(e) {
      if (!ref.current || ref.current.contains(e.target)) return;
      handler(e);
    }

    document.addEventListener('mousedown', listener);
    document.addEventListener('touchstart', listener);
    return () => {
      document.removeEventListener('mousedown', listener);
      document.removeEventListener('touchstart', listener);
    };
  }, [ref, handler]);
}

// Usage — closes a dropdown when clicking outside it
function Dropdown({ options }) {
  const [isOpen, setIsOpen] = useState(false);
  const dropdownRef = useRef(null);

  useOnClickOutside(dropdownRef, () => setIsOpen(false));

  return (
    <div ref={dropdownRef}>
      <button onClick={() => setIsOpen(o => !o)}>Open</button>
      {isOpen && <ul>{options.map(o => <li key={o}>{o}</li>)}</ul>}
    </div>
  );
}

Frequently Asked Questions

What React Hooks are asked most frequently in interviews?

useState and useEffect appear in virtually 100% of React interviews — every interviewer tests these. useCallback and useMemo come up in mid-level interviews when discussing performance. Custom hooks (useFetch, useDebounce) are the standard live-coding task. At senior levels, expect useTransition, useDeferredValue, and questions about stale closures and dependency array pitfalls.

React interview questions full guide → complete 50-question React interview guide covering components, patterns, performance, and routing

What's the difference between `useMemo` and `useCallback` in one sentence?

useMemo memoizes a computed value (calls the function and stores the result); useCallback memoizes a function reference (stores the function itself without calling it). useCallback(fn, deps) is exactly equivalent to useMemo(() => fn, deps).

How do I answer "what are the rules of hooks" without sounding like I'm reciting a list?

Lead with why the rules exist: React tracks hook state by call order — a stable, numbered list per component instance. Conditional or looped calls would shift the index and map hooks to the wrong state slots. The rule isn't arbitrary; it's the implementation's constraint. Knowing the why is what separates a strong answer from a memorized one.

Do I need to know React 18 hooks like `useTransition` for a mid-level interview?

At companies actively using React 18 in production — which includes most teams on Next.js 13+ — yes. useTransition and useDeferredValue come up regularly in mid-level and above interviews as a way to test whether you understand concurrent rendering. At companies still on React 17, they're a nice-to-have signal of staying current with the ecosystem.

React 18 features guide → deep dive on concurrent rendering, automatic batching, and new APIs in React 18

Should I use `useEffect` for data fetching in 2026?

For learning and interviews, yes — understanding the raw pattern (abort signal, cleanup, error state) is expected. For production, use React Query or SWR instead. They handle caching, deduplication, background refetching, and stale-while-revalidate out of the box. In an interview, the correct answer is: "I know how to implement it with useEffect, but in production I'd reach for React Query because it solves caching and race conditions that raw useEffect doesn't."

Next.js interview questions → guide covering data fetching patterns including Server Components, React Query, and SWR in Next.js


Conclusion

React Hooks interview questions cover a wider surface area than most candidates expect. Basic hooks (useState, useEffect, useRef) are table stakes — know the mental model, not just the syntax. Performance hooks (useCallback, useMemo) require understanding when they help and when they add overhead for no gain. Custom hooks are the real differentiator in live coding rounds — build useFetch, useDebounce, useLocalStorage, and useToggle from memory. React 18 additions (useTransition, useDeferredValue) separate candidates who track the ecosystem from those who don't.

The questions that trip candidates aren't recall questions — they're the edge cases: stale closures, dependency array mistakes, infinite loops, and why React.memo sometimes doesn't help. Work through the code examples in this guide until you can identify the bug before reading the fix.

Key Takeaways

  • Know the rules of hooks and the reason behind them — React tracks state by call order, not by name

  • Stale closures are the #1 runtime bug in hooks code — always use the functional updater form for state that depends on its previous value

  • Custom hooks are the live-coding standard: useFetch, useDebounce, useLocalStorage should be muscle memory

  • useTransition = you own the state setter; useDeferredValue = value comes from outside as a prop

JavaScript interview questions guide → complete guide to JavaScript fundamentals, closures, async patterns, and ES6+ asked in frontend interviews

More from React & Frontend

⚛️

Top 50 TypeScript Interview Questions and Answers 2026

22 min read

⚛️

Top 40 Next.js Interview Questions and Answers (2026)

18 min read

⚛️

30 Golden Frontend Topics Every Senior Developer Must Master in 2026

18 min read

Browse All Articles