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