React Hooks interviews test more than API recall. Strong answers explain state snapshots, reactive dependencies, setup/cleanup symmetry, stale closures, and when memoization is actually measurable. These 17 questions use the React 19.2 model and distinguish public guarantees from common folklore.
Table of Contents
- Hook Fundamentals Questions
- useState Questions
- useEffect Questions
- useMemo and useCallback Questions
- Custom Hooks Questions
- useRef and useLayoutEffect Questions
- State Management Questions
- Quick Reference
Hook Fundamentals Questions
These questions test your core understanding of React Hooks.
What are React Hooks and why were they introduced?
React Hooks are functions that let you use state and other React features in functional components. Before Hooks, you needed class components to use state or lifecycle methods, which led to complex patterns like render props and higher-order components. Hooks solve this by letting you extract stateful logic into reusable functions.
The most important hooks are:
useStatefor local component stateuseEffectfor synchronizing with external systemsuseContextfor consuming React ContextuseReffor mutable values that don't trigger re-rendersuseMemoanduseCallbackfor performance optimization
React 19.2 also includes hooks for transitions, optimistic UI, Actions, external stores, IDs, and Effect Events. Choose an API based on the problem rather than treating useEffect as a general “run code after render” tool.
What are the rules of Hooks?
Two main rules:
- Only call Hooks at the top level—before early returns, never inside loops, conditions, nested functions, event handlers, or
try/catch/finally. - Only call Hooks from React function components or custom Hooks—not from regular JavaScript functions or class components.
These rules let React associate Hook calls with the correct component state. eslint-plugin-react-hooks checks both call placement and reactive dependencies.
Is React's use API an exception to the Rules of Hooks?
Yes, because use is a React API rather than a Hook despite its name. use(context) reads context, and use(promise) reads a cached Promise during render, suspending while it is pending. Unlike Hooks, use may appear in a loop or condition:
function Message({ shouldRead, messagePromise }) {
if (!shouldRead) return null;
const message = use(messagePromise);
return <p>{message}</p>;
}It still has restrictions: call it only while a component or Hook renders, do not place it inside try/catch, and pass use(promise) a Promise whose identity is cached across retries. Handle pending and rejected states with Suspense and Error Boundaries.
useState Questions
These questions test your understanding of state management with useState.
How do you use useState for different scenarios?
// Basic usage
const [count, setCount] = useState(0);
// Object state
const [user, setUser] = useState({ name: '', email: '' });
// Updating object state (always create new object)
setUser(prev => ({ ...prev, name: 'John' }));
// Functional update (when new state depends on old)
setCount(prev => prev + 1);
// Lazy initial state (expensive computation)
const [data, setData] = useState(() => computeExpensiveValue());useEffect Questions
These questions test your understanding of side effects and the dependency array.
How does the useEffect dependency array work?
The dependency array controls when the effect runs. Here are the three patterns:
// Runs after every render
useEffect(() => {
console.log('Every render');
});
// Does not re-run because of changing props/state
useEffect(() => {
const connection = connect();
return () => connection.disconnect();
}, []);
// Runs when dependencies change
useEffect(() => {
console.log('userId changed');
fetchUser(userId);
}, [userId]);
// With cleanup
useEffect(() => {
const subscription = subscribeToData(id);
return () => {
subscription.unsubscribe(); // Cleanup
};
}, [id]);Dependencies are not a menu of triggers. Include every reactive value read by setup and cleanup; React compares each one with its previous value using Object.is. To remove a dependency, restructure the code so the Effect no longer reads it instead of suppressing the linter.
React 19.2's useEffectEvent can separate genuinely non-reactive event logic from an Effect while still reading the latest committed props and state. It is not a general escape hatch for dependencies: an Effect Event may be called only from Effects or other Effect Events, must be omitted from dependency arrays, and does not have stable identity.
An empty array does not mean “exactly once forever.” Setup runs when that component instance is committed, cleanup runs when it is removed, and development Strict Mode performs an additional stress-test cycle.
Why does useEffect run twice in Strict Mode?
In development, Strict Mode runs an extra setup → cleanup → setup cycle for each Effect. This exposes code that leaks subscriptions, fails to undo mutations, or assumes mounting happens only once. It does not make production Effects run twice.
The fix is not a useRef flag that suppresses the second setup. Make cleanup mirror setup so users cannot tell whether the sequence ran once or was stress-tested:
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);What causes an infinite loop in useEffect and how do you fix it?
This mistake appears in almost every React interview:
// BUG: Infinite loop!
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}); // Missing dependency array!
return <div>{user?.name}</div>;
}What happens?
- Component renders
- useEffect runs, fetches data
- setUser triggers re-render
- useEffect runs again (no deps = runs every render)
- Infinite loop!
Three Ways to Fix It:
1. Declare the reactive dependency and handle stale requests
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then(res => res.json())
.then(data => setUser(data))
.catch(error => {
if (error.name !== 'AbortError') reportError(error);
});
return () => controller.abort();
}, [userId]);2. Remove an unnecessary Effect
// Derive render data directly instead of setting derived state in an Effect.
const fullName = `${firstName} ${lastName}`;3. Ignore an obsolete response when the API cannot be aborted
useEffect(() => {
let cancelled = false;
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
if (!cancelled) setUser(data);
});
return () => { cancelled = true; };
}, [userId]);For application data, a framework loader or client cache often handles server rendering, deduplication, caching, and request waterfalls better than a hand-written fetch Effect.
useMemo and useCallback Questions
These questions test your understanding of performance optimization with memoization.
What is the difference between useMemo and useCallback?
This is one of the most commonly failed interview questions because candidates often mix them up.
// useCallback - memoizes a FUNCTION
const handleClick = useCallback(() => {
console.log('Clicked:', id);
}, [id]);
// useMemo - memoizes a VALUE
const expensiveResult = useMemo(() => {
return computeExpensiveValue(data);
}, [data]);When should you use useCallback?
Use useCallback when a stable function reference has a measured purpose—for example, a prop passed to a memoized child or a dependency of another Hook:
function Parent() {
const [count, setCount] = useState(0);
// Stable while dependencies match; useful at this memoized boundary
const handleIncrement = useCallback(() => {
setCount(c => c + 1);
}, []);
return <Child onIncrement={handleIncrement} />;
}
const Child = React.memo(({ onIncrement }) => {
console.log('Child rendered');
return <button onClick={onIncrement}>+</button>;
});When should you use useMemo?
Use useMemo when profiling shows a calculation is expensive, or when a stable result is required for an effective memoized boundary:
function FilteredList({ items, filter }) {
// React can reuse the result while items and filter match
const filteredItems = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
return <List items={filteredItems} />;
}When is useMemo or useCallback unnecessary?
Over-optimization is a common trap. Not everything needs memoization:
// DON'T do this - premature optimization
const value = useMemo(() => a + b, [a, b]); // Simple math doesn't need memoization
// DO use it for actual expensive operations
const sorted = useMemo(() =>
[...largeArray].sort((a, b) => a.value - b.value),
[largeArray]
);Code must remain correct if React discards either cache. When React Compiler is enabled, it can automatically memoize components, values, and functions; keep manual memoization when it provides necessary precision, and test carefully before removing existing calls.
Custom Hooks Questions
These questions test your ability to extract and reuse stateful logic.
What is a custom hook and when should you create one?
A custom hook is a JavaScript function starting with 'use' that can call other hooks. Create custom hooks to extract and reuse stateful logic between components. Each component using the hook gets its own isolated state—custom hooks share logic, not state.
How do you implement a useLocalStorage hook?
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
if (typeof window === 'undefined') return initialValue;
try {
const item = window.localStorage.getItem(key);
return item === null ? initialValue : JSON.parse(item);
} catch {
return initialValue;
}
});
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
reportStorageError(error);
}
}, [key, storedValue]);
return [storedValue, setStoredValue];
}
// Usage
function App() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
Current: {theme}
</button>
);
}This remains a teaching example, not a complete persistence layer. Production code should define key-change behavior, hydration strategy, schema/version handling, quota failures, and cross-tab synchronization with the storage event.
How do you implement a useFetch hook with loading and error states?
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);
setError(null);
fetch(url, { signal: controller.signal })
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(data => {
setData(data);
setLoading(false);
})
.catch(err => {
if (err.name === 'AbortError') return;
setError(err.message);
setLoading(false);
});
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
// Usage
function UserProfile({ userId }) {
const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
if (loading) return <Spinner />;
if (error) return <Error message={error} />;
return <Profile user={user} />;
}This handles request cancellation but still lacks caching, deduplication, retries, server rendering, and Suspense integration. Prefer a framework data API or an established client cache when those capabilities matter.
useRef and useLayoutEffect Questions
These questions test your understanding of refs and synchronous effects.
What is useRef and when do you use it?
useRef returns a mutable object with a .current property that persists across renders without causing re-renders when changed. Two main uses:
- Accessing DOM elements directly
- Storing mutable values that don't need to trigger re-renders (like timer IDs)
// DOM access
function TextInput() {
const inputRef = useRef(null);
const focusInput = () => inputRef.current.focus();
return <input ref={inputRef} />;
}
// Mutable value without re-render
function Timer() {
const intervalRef = useRef(null);
const start = () => {
intervalRef.current = setInterval(() => {
console.log('tick');
}, 1000);
};
const stop = () => clearInterval(intervalRef.current);
useEffect(() => {
return () => clearInterval(intervalRef.current);
}, []);
return (
<>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</>
);
}Do not read or write ref.current during rendering except for predictable initialization. Refs are an escape hatch for values that do not participate in rendered output; rendered data belongs in state.
What is the difference between useEffect and useLayoutEffect?
Both are client-only Effects. useLayoutEffect runs after React changes the DOM but blocks the browser from repainting until its work and scheduled state updates finish. Use it for layout measurements or visual corrections that must happen before repaint. For most external synchronization, useEffect is the better default because it does not block paint.
// useLayoutEffect - runs before paint
useLayoutEffect(() => {
// Measure element, adjust position
const height = ref.current.getBoundingClientRect().height;
setPosition(calculatePosition(height));
}, []);
// useEffect - does not block paint (default choice)
useEffect(() => {
// Fetch data, set up subscriptions
fetchData();
}, []);State Management Questions
These questions test your understanding of state management patterns.
How do you choose useState vs useReducer for related state?
Three approaches depending on how the state changes:
- Multiple
useStatecalls for independent values - Single
useStatewith an object for related values useReducerwhen named actions and centralized transitions make the logic easier to understand
// Multiple useState - simple, independent values
const [name, setName] = useState('');
const [email, setEmail] = useState('');
// Object state - related values
const [form, setForm] = useState({ name: '', email: '' });
const updateField = (field, value) =>
setForm(prev => ({ ...prev, [field]: value }));
// useReducer - complex, interdependent updates
const [state, dispatch] = useReducer(reducer, initialState);
dispatch({ type: 'SUBMIT_FORM' });useState and useReducer are interchangeable in capability. A functional setState update already handles next-state-from-previous-state correctly; choose a reducer for organization and testability, not because it is inherently more performant.
Quick Reference
| Hook | Purpose | Key Point |
|---|---|---|
| useState | Local state | Use functional updates for state based on prev |
| useEffect | Synchronize with external systems | Declare all reactive deps; mirror setup with cleanup |
| useContext | Consume context | Triggers re-render when context changes |
| useRef | DOM/mutable values | Changes don't cause re-renders |
| useMemo | Cache calculation results | Performance optimization, not a guarantee |
| useCallback | Cache function definitions | Useful at measured memoization boundaries |
| useReducer | Centralize state transitions | Choose for clarity, actions, and testability |
| useLayoutEffect | Layout-critical client Effect | Blocks repaint; use sparingly |
| useEffectEvent | Non-reactive logic called by Effects | Latest values; never a dependency or child prop |
use API | Read context or cached Promises in render | May be conditional; integrates with Suspense |
Practice Questions
Test yourself before your interview:
1. What's wrong with this code?
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(async () => {
const data = await fetchResults(query);
setResults(data);
}, [query]);
return <List items={results} />;
}2. Will the Child component re-render when Parent re-renders? How would you fix it?
function Parent() {
const [count, setCount] = useState(0);
const handleClick = () => console.log('clicked');
return (
<>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<Child onClick={handleClick} />
</>
);
}
const Child = React.memo(({ onClick }) => {
console.log('Child rendered');
return <button onClick={onClick}>Click me</button>;
});3. Implement a useToggle custom hook that returns [value, toggle].
Answers:
- useEffect callback can't be async directly. Wrap in an IIFE or define async function inside.
- Without React Compiler, yes: the new function makes the memoized child's prop unequal. Declare the handler outside the component if it uses no reactive values, or use
useCallbackat this measured boundary. With React Compiler enabled, automatic memoization may already skip the child render. const useToggle = (initial = false) => { const [value, setValue] = useState(initial); const toggle = useCallback(() => setValue(v => !v), []); return [value, toggle]; }
Official Sources
- Built-in React Hooks
- Rules of Hooks
useuseStateuseReduceruseEffect- Synchronizing with Effects
StrictModeuseEffectEventuseMemouseCallbackuseRefuseLayoutEffect- React Compiler
Related Articles
- Complete Frontend Developer Interview Guide - comprehensive preparation guide for frontend interviews
- 9 Advanced React Interview Questions - Deep dive into React patterns, performance, and architecture questions
- React 19 Interview Guide - New features like Actions, use() hook, and Server Components
- JavaScript Closures Interview Guide - Understanding closures is essential for hooks (they use closures internally!)
- Top 5 React Interview Mistakes - Common hooks and rendering mistakes to avoid
Frequently Asked Questions
What are React Hooks?
React Hooks are functions that let function components use state, context, Effects, refs, and other React features. Hooks also let teams package reusable stateful logic in custom Hooks. They complement rather than literally replace every class API; Error Boundaries, for example, still use class lifecycle methods in React itself.
What are the rules of Hooks?
Call Hooks only at the top level of a function component or custom Hook, before conditional returns—not in loops, conditions, callbacks, event handlers, or try/catch. The React 19 use API is a special case: it is not a Hook and may be called conditionally or in loops, but it still must run while a component or Hook renders and cannot be wrapped in try/catch.
What is the difference between useState and useReducer?
useState and useReducer can model the same local state. useState is concise for independent or simple updates; useReducer can make many related transitions easier to name, centralize, and test. Neither is automatically faster, and useReducer is a design choice rather than a requirement whenever next state depends on previous state.
What is the difference between useCallback and useMemo?
useCallback caches a function definition and useMemo caches a calculation result between renders while dependencies match. Both are performance optimizations, not semantic guarantees, and React may discard their caches. Profile first; React Compiler can automatically provide much of this memoization when enabled.
What is the useEffect cleanup function?
An Effect may return cleanup that stops or undoes its setup. React runs cleanup with the old values before setup after a commit with changed dependencies, and once more after removal. In development Strict Mode, React also performs an extra setup-cleanup-setup cycle to expose missing or asymmetric cleanup.
What is a custom hook and when should you create one?
A custom hook is a JavaScript function starting with 'use' that can call other hooks. Create custom hooks to extract and reuse stateful logic between components. For example, a useLocalStorage hook that syncs state with localStorage, or a useFetch hook that handles loading/error states. Custom hooks share logic, not state - each component using the hook gets its own isolated state.
