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
thisbinding 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.
// 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 slotSkip 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
const [count, setCount] = useState(0);Updater Function Form
setCount(count + 1);
setCount(count + 1); // both read the same stale count → only +1
setCount((prev) => prev + 1);
setCount((prev) => prev + 1); // → +2Always use the updater form when the next value depends on the previous one.
Lazy Initial State
// Runs expensiveInit() on EVERY render
const [data, setData] = useState(expensiveInit());
// Runs it only once
const [data, setData] = useState(() => expensiveInit());State Updates Are Asynchronous
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
// 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.
useEffect(() => {
// effect
return () => {
// cleanup
};
}, [dependencies]);The Three Dependency Cases
| Dependency array | When the effect runs |
|---|---|
| omitted | After every render |
[] | Once after mount |
[a, b] | On mount, then whenever a or b changes |
Fetching Data
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
// 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
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // count is frozen at 0 forever
}, 1000);
return () => clearInterval(id);
}, []);Fix with the updater form:
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
useEffect | useLayoutEffect | |
|---|---|---|
| Timing | After paint | After DOM mutation, before paint |
| Blocking | No | Yes — blocks paint |
| Use for | Data fetching, subscriptions, logging | Measuring DOM, preventing visual flicker |
| SSR | Safe | Warns (no DOM on server) |
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
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
function Timer() {
const intervalRef = useRef(null);
const start = () => {
intervalRef.current = setInterval(tick, 1000);
};
const stop = () => clearInterval(intervalRef.current);
}useRef vs useState
useRef | useState | |
|---|---|---|
| Triggers re-render | No | Yes |
| Value survives renders | Yes | Yes |
| Read during render | Not recommended | Yes |
| Mutation | Direct: ref.current = x | Via 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
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
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:
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.
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.
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:
<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.
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 when | Use useReducer when |
|---|---|
| One or two independent values | Many related values |
| Simple updates | Next state depends on previous |
| Logic lives in the component | Logic is worth testing on its own |
| Few update sites | Same 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.
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.
// 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.
const handleSearch = useCallback((term) => {
fetchResults(term);
}, []);useCallback(fn, deps) is exactly useMemo(() => fn, deps).
Why It Matters
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 caches | Where it goes | |
|---|---|---|
useMemo | A value | Inside the component |
useCallback | A function | Inside the component |
React.memo | The render output | Wraps 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.
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:
const { data, loading, error } = useFetch("/api/users");More Useful Custom Hooks
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).
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.
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.
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.
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.
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.
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.
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.
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
| Hooks | Classes | |
|---|---|---|
| Code volume | Less | More |
| Logic reuse | Custom hooks | HOC / render props |
this binding | None | Required |
| Split by | Concern | Lifecycle method |
| Bundle size | Smaller | Larger |
| Error boundaries | Not supported | Supported |
| Performance | Slightly 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.