useState, useEffect, useMemo, useCallback, useRef, useReducer, custom hooks and the rules of hooks, answered the way an interviewer wants to hear them.
18 questions7 fresher, 8 mid-level, 3 senior
Hooks are the densest part of a React interview. Anyone can list them; the interviewer is looking for whether you know when each one is the wrong choice. The two questions that catch most candidates are the dependency array of useEffect and the difference between useMemo and useCallback, so make sure those two are automatic.
A rule of thumb worth saying out loud during the interview: reach for state, then derive everything you can from it, and only add an effect when you need to synchronise with something outside React.
1.What are React hooks and why were they introduced?
Fresher
Hooks are functions that let a function component use state, effects and other React features. They were introduced in React 16.8 to make stateful logic reusable without wrapper components, and to stop related code being split across lifecycle methods.
Before hooks, only classes could hold state, so sharing stateful logic meant higher order components or render props. Both add layers to the tree, and a screen wrapped in five of them became hard to read and to debug.
Reusable logic. A custom hook packages behaviour and can be called by any component, with no wrapper.
Related code stays together. A subscription's setup and teardown live in one useEffect instead of being split between componentDidMount and componentWillUnmount.
No `this`. No binding, no confusion about which instance a callback refers to.
Smaller components. With no need for a class, most components are a function and a few hook calls.
2.What are the rules of hooks, and why do they exist?
Fresher
Only call hooks at the top level of a component or another hook, never inside conditions, loops or nested functions, and only call them from React function components or custom hooks. React identifies hooks by call order, so the order must be identical on every render.
React does not know the names of your state variables. It keeps a list per component and hands back the first slot to the first useState call, the second slot to the second, and so on. Skip a call on one render and every hook after it receives the wrong slot.
Why the rule is not arbitrary.
// Broken: on renders where isLoggedIn is false, name gets email's slot
if (isLoggedIn) {
const [name, setName] = useState('');
}
const [email, setEmail] = useState('');
// Correct: the condition goes inside, not around
const [name, setName] = useState('');
const [email, setEmail] = useState('');
useState returns a pair: the current value for this render and a setter. Calling the setter schedules a re-render with the new value; it does not change the variable you are currently holding.
const [count, setCount] = useState(0);
// Lazy initial state: the function runs on the first render only
const [rows, setRows] = useState(() => parseHugeCsv(input));
// Functional update: use it whenever the next value depends on the last
setCount((previous) => previous + 1);
The initial value is used on the first render only. Passing a different value later does nothing.
Pass a function as the initial value when computing it is expensive, otherwise the computation runs on every render and the result is thrown away.
React bails out of re-rendering if the new value is Object.is equal to the current one.
State must be treated as immutable. Replace objects and arrays rather than mutating them, or the equality check will see no change.
Likely follow-up questions
What is lazy initial state?
Why does my component not re-render after I update an array?
useEffect runs a function after React has committed the render to the DOM and the browser has painted. It runs after every render by default, or only when a value in the dependency array has changed.
The purpose of an effect is synchronisation with something outside React: a subscription, a timer, a browser API, an analytics call, a non React widget. It is not a general "run this after render" hook, and treating it as one is the single most common source of React bugs.
5.What does the dependency array do, and what changes between no array, an empty array and a populated one?
Fresher
No array means the effect runs after every render. An empty array means it runs once after mount. A populated array means it re-runs whenever one of those values changes by reference or value.
Written as
Runs
useEffect(fn)
After every render
useEffect(fn, [])
Once after mount, cleanup on unmount
useEffect(fn, [a, b])
After mount, and after any render where a or b changed
React compares dependencies with Object.is. Objects, arrays and functions created during render are new references every time, so an effect depending on one runs on every render even when nothing meaningful changed.
Likely follow-up questions
How would you run an effect only when a value changes, not on mount?
What causes an infinite re-render loop with useEffect?
6.What is the cleanup function in useEffect, and when does it run?
Mid-level
The function you return from an effect. React runs it before the effect runs again, and once more when the component unmounts, so every subscription, timer or listener is undone exactly once.
Cleanup also solves the stale response race in data fetching.
Without the flag, switching quickly from user 1 to user 2 can leave user 1's slower response arriving last and overwriting the correct data. AbortController does the same job and also stops the request.
Likely follow-up questions
How does StrictMode help you find a missing cleanup?
7.What is the difference between useEffect and useLayoutEffect?
Mid-level
useEffect runs asynchronously after the browser has painted. useLayoutEffect runs synchronously after the DOM is updated but before paint, so it can measure and adjust layout without the user seeing a flicker.
Use useLayoutEffect when you must read layout, such as an element's size or scroll position, and immediately change something based on it, for example positioning a tooltip.
Everything else belongs in useEffect. useLayoutEffect blocks paint, so slow work in it delays what the user sees.
useLayoutEffect does not run on the server and warns during server rendering, because there is no layout to measure.
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setTooltipTop(height + 8); // applied before the user sees anything
}, [content]);
8.What is useContext, and what are its limitations?
Fresher
useContext reads the nearest provider's value for a context, letting a deep component get data without prop drilling. Its limitation is that every consumer re-renders whenever the provider's value changes, whatever part of the value they actually use.
9.What is useReducer and when would you use it instead of useState?
Mid-level
useReducer holds state and updates it by dispatching actions to a reducer function. Prefer it when several state values change together, when the next state depends on the previous one in non-trivial ways, or when the update logic is worth testing on its own.
useMemo caches the result of a calculation between renders and recomputes it only when its dependencies change. It is worth using for genuinely expensive computations, and for keeping an object or array reference stable so a memoised child or an effect does not fire needlessly.
The two legitimate reasons are cost and identity. Cost: the calculation is slow enough to matter, which you should confirm with the Profiler rather than assume. Identity: the value is passed to a React.memo child, used as a dependency, or provided through context, where a new reference each render defeats the point.
11.What is the difference between useMemo and useCallback?
Mid-level
useMemo caches the value a function returns; useCallback caches the function itself. useCallback(fn, deps) is exactly useMemo(() => fn, deps).
const value = useMemo(() => computeTotal(items), [items]); // caches a number
const onSave = useCallback(() => save(id), [id]); // caches a function
// Equivalent to the line above
const onSave = useMemo(() => () => save(id), [id]);
useCallback exists because functions are recreated on every render, and a new function reference breaks React.memo on a child and re-triggers any effect that depends on it. It has no benefit when the function is passed to a plain DOM element: onClick on a button does not care that the reference changed.
Likely follow-up questions
When does useCallback do nothing?
Why is my React.memo component still re-rendering?
12.What is useRef, and what are its two distinct uses?
Mid-level
useRef returns a mutable object with a .current property that survives re-renders and never triggers one. It is used to reach a DOM node, and to store a mutable value that the UI does not display, such as a timer id or the previous value of a prop.
// 1. A DOM node
const inputRef = useRef(null);
<input ref={inputRef} />;
inputRef.current.focus();
// 2. An instance variable that does not belong in state
const timerRef = useRef(null);
timerRef.current = setTimeout(save, 500);
The decision rule interviewers want: if changing it should update the screen, it is state; if it should not, it is a ref. Writing to a ref during render is also not allowed, because it makes the render impure. Read and write refs in effects and event handlers.
Likely follow-up questions
What is the difference between useRef and a variable declared with let in the component?
13.What is a custom hook, and when should you write one?
Mid-level
A custom hook is a function whose name starts with use and which calls other hooks. Write one when the same stateful logic appears in more than one component, or when a component's logic is complex enough that naming it makes the component readable.
14.What is useTransition, and how does it differ from useDeferredValue?
Senior
Both mark work as non-urgent so React can keep the interface responsive. useTransition wraps the state update you are making and gives you an isPending flag; useDeferredValue takes a value you were given and lets you lag behind it.
// You own the update: mark it as a transition
const [isPending, startTransition] = useTransition();
function onChange(event) {
setQuery(event.target.value); // urgent: the input must keep up
startTransition(() => setResults(search(event.target.value))); // can wait
}
// You only receive the value: defer it
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => search(deferredQuery), [deferredQuery]);
React can interrupt and restart transition work when something urgent arrives, so typing never stalls behind an expensive list render. Neither hook makes the slow work faster; they change when it blocks the user.
Likely follow-up questions
What is concurrent rendering?
Can you mark a state update inside a promise as a transition?
useId generates a unique, stable id that matches between the server render and the client hydration. It exists for accessibility attributes such as linking a label to an input, not for keys or database ids.
A counter or Math.random() produces different values on the server and the client, which causes a hydration mismatch. One useId call can also seed several related ids by suffixing it, for example `${id}-error`.
16.What is useSyncExternalStore and when would you need it?
Senior
It subscribes a component to a store that lives outside React, guaranteeing a consistent snapshot during concurrent rendering. Library authors need it; application code usually only meets it indirectly through Redux, Zustand or a similar store.
The problem it solves is tearing: with concurrent rendering, an external value can change halfway through a render, leaving two parts of the same screen showing different values. useSyncExternalStore forces a consistent read. The subscribe function must be stable, so define it outside the component or memoise it, otherwise React resubscribes on every render.
17.Which hooks did React 19 add, and what are they for?
Senior
React 19 added use, useActionState, useOptimistic and useFormStatus. Together they cover reading a promise or context during render, and handling form submissions with pending, optimistic and error states without hand-rolled state.
`use`. Reads a promise or a context during render. Unlike other hooks it may be called conditionally, and suspending on a promise integrates with the nearest Suspense boundary.
`useActionState`. Wraps an async action and returns the last result, a wrapped action for the form, and a pending flag.
`useOptimistic`. Shows the expected result immediately and reverts automatically if the action fails.
`useFormStatus`. Lets a component inside a form read that form's pending state without prop drilling, which is how a shared submit button knows to disable itself.
const [optimisticLikes, addOptimisticLike] = useOptimistic(
likes,
(current) => current + 1
);
async function like() {
addOptimisticLike();
await sendLike();
}
18.Can you call a hook inside a condition, a loop or a callback?
Fresher
No, with one exception: React 19's use can be called conditionally. Every other hook must be called unconditionally at the top level, because React matches hooks to their stored state by call order.
If the number or order of hook calls differs between two renders of the same component, React throws "Rendered fewer hooks than expected" or silently hands back another hook's state.
Move the condition inside the hook, not around it.
// Wrong
if (userId) {
useEffect(() => { load(userId); }, [userId]);
}
// Right
useEffect(() => {
if (!userId) return;
load(userId);
}, [userId]);
The same applies to rendering a variable number of hooks in a loop. If you need one piece of state per item, render one component per item and let each component own its own state.
Likely follow-up questions
How would you handle one piece of state per list item?