Skip to content

2. Hooks (Most Asked Topic)

Hooks let function components use state, lifecycle and context. Introduced in React 16.8.


1. What Are Hooks?

Functions starting with use that "hook into" React features from a function component.

Before hooks, you needed a class for state. Hooks solved three real problems:

  • Reusing stateful logic (previously HOCs and render props → wrapper hell)
  • Giant components split by lifecycle instead of by concern
  • Confusing this binding in classes

2. Rules of Hooks

Two rules. Interviewers always ask why.

Rule 1 — Only call hooks at the top level

Never inside conditions, loops or nested functions.

jsx
// Wrong
if (isLoggedIn) {
  const [name, setName] = useState("");
}

// Right
const [name, setName] = useState("");
if (isLoggedIn) { /* ... */ }

Rule 2 — Only call hooks from React functions

From a component, or from another custom hook. Not from a plain utility function or a class.

Why These Rules Exist

React stores hook state in an ordered list per component, not by name. It matches state to hook by call order.

Render 1:  useState  →  slot 0
           useEffect →  slot 1
           useState  →  slot 2

Render 2:  useState  →  slot 0  (condition false, hook skipped)
           useState  →  slot 1  ← now reads the WRONG slot

Skip a hook once and every later hook reads someone else's state.

Interview Point

"Because React tracks hooks by call index, not by name" is the answer that separates people who memorised the rule from people who understand it.


3. useState

jsx
const [count, setCount] = useState(0);

Updater Function Form

jsx
setCount(count + 1);
setCount(count + 1); // both read the same stale count → only +1

setCount((prev) => prev + 1);
setCount((prev) => prev + 1); // → +2

Always use the updater form when the next value depends on the previous one.

Lazy Initial State

jsx
// Runs expensiveInit() on EVERY render
const [data, setData] = useState(expensiveInit());

// Runs it only once
const [data, setData] = useState(() => expensiveInit());

State Updates Are Asynchronous

jsx
const handleClick = () => {
  setCount(5);
  console.log(count); // still the old value
};

React batches updates and re-renders once. Since React 18, automatic batching applies everywhere — including promises, setTimeout and native event handlers, not just React events.

State Is Immutable

jsx
// Wrong — same reference, React skips the re-render
user.name = "Rahul";
setUser(user);

// Right — new reference
setUser({ ...user, name: "Rahul" });

React compares with Object.is(). Same reference means no re-render.


4. useEffect

Runs side effects after render.

jsx
useEffect(() => {
  // effect
  return () => {
    // cleanup
  };
}, [dependencies]);

The Three Dependency Cases

Dependency arrayWhen the effect runs
omittedAfter every render
[]Once after mount
[a, b]On mount, then whenever a or b changes

Fetching Data

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

  fetch(`/api/users/${id}`, { signal: controller.signal })
    .then((res) => res.json())
    .then(setUser)
    .catch((err) => {
      if (err.name !== "AbortError") setError(err);
    });

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

The cleanup prevents a race condition: if id changes fast, an older slower response could overwrite a newer one.

Infinite Loop Trap

jsx
// Infinite: setUser triggers a render, which re-runs the effect
useEffect(() => {
  setUser({ name: "Dipak" });
});

// Also infinite: object literal is a new reference each render
useEffect(() => { /* ... */ }, [{ id: 1 }]);

Stale Closure Trap

jsx
useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1); // count is frozen at 0 forever
  }, 1000);
  return () => clearInterval(id);
}, []);

Fix with the updater form:

jsx
setCount((prev) => prev + 1);

Interview Point

The React docs now say: you might not need an effect. Don't use useEffect to transform data for rendering (compute it during render) or to handle a user event (put it in the handler).


5. useEffect vs useLayoutEffect

useEffectuseLayoutEffect
TimingAfter paintAfter DOM mutation, before paint
BlockingNoYes — blocks paint
Use forData fetching, subscriptions, loggingMeasuring DOM, preventing visual flicker
SSRSafeWarns (no DOM on server)
jsx
useLayoutEffect(() => {
  const { height } = ref.current.getBoundingClientRect();
  setTooltipTop(height); // user never sees the wrong position
}, []);

Interview Point

Default to useEffect. Reach for useLayoutEffect only when the user would otherwise see a flash of wrong layout.


6. useRef

Two uses, both common interview material.

Use 1 — Access a DOM node

jsx
function Search() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus();
  }, []);

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

Use 2 — Store a mutable value that does NOT re-render

jsx
function Timer() {
  const intervalRef = useRef(null);

  const start = () => {
    intervalRef.current = setInterval(tick, 1000);
  };

  const stop = () => clearInterval(intervalRef.current);
}

useRef vs useState

useRefuseState
Triggers re-renderNoYes
Value survives rendersYesYes
Read during renderNot recommendedYes
MutationDirect: ref.current = xVia setter

Interview Point

"Why do hooks use refs?" — because a ref is the escape hatch for values that must persist across renders without participating in rendering: timer IDs, previous values, third-party library instances, and the "is this the first render" flag.

Previous Value Pattern

jsx
function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current;
}

7. forwardRef

Refs are not props, so they don't pass through a component by default.

React 18 and earlier

jsx
const Input = React.forwardRef((props, ref) => (
  <input ref={ref} {...props} />
));

// Parent can now do:
<Input ref={myRef} />

React 19

ref is a normal prop for function components. forwardRef is no longer required:

jsx
function Input({ ref, ...props }) {
  return <input ref={ref} {...props} />;
}

forwardRef still works but is deprecated and will be removed in a future version.

useImperativeHandle

Expose a limited API instead of the raw DOM node.

jsx
function Input({ ref }) {
  const innerRef = useRef();

  useImperativeHandle(ref, () => ({
    focus: () => innerRef.current.focus(),
    clear: () => (innerRef.current.value = ""),
  }));

  return <input ref={innerRef} />;
}

The parent gets focus() and clear() only — it cannot touch the DOM node directly.


8. useContext

Reads a context value without prop drilling.

jsx
const ThemeContext = createContext("light");

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Button() {
  const theme = useContext(ThemeContext);
  return <button className={theme}>Save</button>;
}

React 19 Shortcut

<Context> can be used directly as the provider:

jsx
<ThemeContext value="dark">
  <Toolbar />
</ThemeContext>

Interview Point

Context is not a state manager. It is a delivery mechanism. Every consumer re-renders when the provider value changes — see State Management for the split-context fix.


9. useReducer

For complex state where the next state depends on the previous one, or where several values change together.

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

function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return { ...state, count: state.count + state.step };
    case "setStep":
      return { ...state, step: action.payload };
    case "reset":
      return initialState;
    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <button onClick={() => dispatch({ type: "increment" })}>
      {state.count}
    </button>
  );
}

useState vs useReducer

Use useState whenUse useReducer when
One or two independent valuesMany related values
Simple updatesNext state depends on previous
Logic lives in the componentLogic is worth testing on its own
Few update sitesSame updates fired from many places

Interview Point

The reducer is a pure function and can be unit tested with no React at all. That testability is the strongest argument for it.


10. useMemo

Caches a computed value between renders.

jsx
const sortedUsers = useMemo(
  () => users.slice().sort((a, b) => a.name.localeCompare(b.name)),
  [users]
);

Only recomputes when users changes.

The Second Reason To Use It

Referential stability. An object or array created during render is a new reference every time, which breaks React.memo on a child and re-triggers useEffect.

jsx
// New object every render → child always re-renders
const config = { pageSize: 10 };

// Stable reference
const config = useMemo(() => ({ pageSize: 10 }), []);

11. useCallback

Caches a function reference between renders.

jsx
const handleSearch = useCallback((term) => {
  fetchResults(term);
}, []);

useCallback(fn, deps) is exactly useMemo(() => fn, deps).

Why It Matters

jsx
const Child = React.memo(({ onClick }) => <button onClick={onClick} />);

function Parent() {
  // Without useCallback: new function each render → memo is useless
  const onClick = useCallback(() => console.log("hi"), []);
  return <Child onClick={onClick} />;
}

useMemo vs useCallback vs React.memo

What it cachesWhere it goes
useMemoA valueInside the component
useCallbackA functionInside the component
React.memoThe render outputWraps the component

Interview Point

All three are optimisations, not correctness tools. They cost memory and comparison time. Measure with the Profiler first. And note: the React Compiler (stable v1.0, October 2025) auto-memoises, which is making manual useMemo/useCallback largely unnecessary in new codebases.


12. Custom Hooks

A function starting with use that calls other hooks. This is how you share stateful logic.

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

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

    fetch(url, { signal: controller.signal })
      .then((res) => {
        if (!res.ok) throw new Error(res.statusText);
        return res.json();
      })
      .then(setData)
      .catch((err) => {
        if (err.name !== "AbortError") setError(err);
      })
      .finally(() => setLoading(false));

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

  return { data, loading, error };
}

Usage:

jsx
const { data, loading, error } = useFetch("/api/users");

More Useful Custom Hooks

jsx
function useDebounce(value, delay = 500) {
  const [debounced, setDebounced] = useState(value);

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

  return debounced;
}

function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    const onResize = () =>
      setSize({ width: window.innerWidth, height: window.innerHeight });

    window.addEventListener("resize", onResize);
    return () => window.removeEventListener("resize", onResize);
  }, []);

  return size;
}

Interview Point

Custom hooks share logic, not state. Two components using useFetch get two completely separate states. If you need shared state, you need Context or a store.


13. Other Built-in Hooks

useId

Generates a stable unique ID that matches between server and client (SSR-safe).

jsx
function Field() {
  const id = useId();
  return (
    <>
      <label htmlFor={id}>Email</label>
      <input id={id} />
    </>
  );
}

Do not use it for list keys.

useTransition

Marks a state update as non-urgent so typing stays responsive.

jsx
const [isPending, startTransition] = useTransition();

const onChange = (e) => {
  setQuery(e.target.value);          // urgent — input updates instantly
  startTransition(() => {
    setResults(filter(e.target.value)); // non-urgent — can be interrupted
  });
};

useDeferredValue

Same idea, but for a value you receive rather than a setter you call.

jsx
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => filter(deferredQuery), [deferredQuery]);

useSyncExternalStore

Subscribe to a store outside React in a concurrent-safe way. Used internally by Redux, Zustand and Jotai.

jsx
const isOnline = useSyncExternalStore(
  (cb) => {
    window.addEventListener("online", cb);
    window.addEventListener("offline", cb);
    return () => {
      window.removeEventListener("online", cb);
      window.removeEventListener("offline", cb);
    };
  },
  () => navigator.onLine,   // client snapshot
  () => true                // server snapshot
);

14. React 19 Hooks

use()

Reads a promise or a context during render. Unlike other hooks, it can be called conditionally and inside loops.

jsx
function Comments({ commentsPromise }) {
  const comments = use(commentsPromise); // suspends until resolved
  return comments.map((c) => <p key={c.id}>{c.text}</p>);
}

Wrap it in <Suspense> for the fallback.

useActionState

Replaces the useState + onSubmit + loading + error boilerplate.

jsx
const [state, formAction, isPending] = useActionState(
  async (prevState, formData) => {
    const res = await saveUser(formData.get("name"));
    if (!res.ok) return { error: "Save failed" };
    return { success: true };
  },
  { error: null }
);

return (
  <form action={formAction}>
    <input name="name" />
    <button disabled={isPending}>Save</button>
    {state.error && <p>{state.error}</p>}
  </form>
);

useFormStatus

Reads the pending status of the nearest parent form — no prop drilling for a submit button.

jsx
function SubmitButton() {
  const { pending } = useFormStatus();
  return <button disabled={pending}>{pending ? "Saving…" : "Save"}</button>;
}

useOptimistic

Shows the expected result immediately, then reconciles with the real one.

jsx
const [optimisticTodos, addOptimistic] = useOptimistic(
  todos,
  (state, newTodo) => [...state, { ...newTodo, sending: true }]
);

async function add(formData) {
  const text = formData.get("text");
  addOptimistic({ text });   // UI updates instantly
  await saveTodo(text);      // real request
}

Interview Point

The theme of React 19 is removing manual state plumbing: Actions for forms, use() for promises, ref as a prop, and the Compiler for memoisation.


15. Hooks vs Classes

HooksClasses
Code volumeLessMore
Logic reuseCustom hooksHOC / render props
this bindingNoneRequired
Split byConcernLifecycle method
Bundle sizeSmallerLarger
Error boundariesNot supportedSupported
PerformanceSlightly better (no class instances)Slightly heavier

Do Hooks Cover Everything Classes Do?

Almost. The gaps are getSnapshotBeforeUpdate, getDerivedStateFromError and componentDidCatch — the error boundary trio. Everything else has a hook equivalent.

© 2025 DDocs · Dipak's Documentation Guide