You've spent months learning React.
You can build components, manage state, and create beautiful UIs. But when the interviewer asks "Explain the React fiber architecture" or "How would you optimize a component that re-renders 1000 times?" — your mind goes blank.
This guide gives you 50+ real React interview questions with expert answers that interviewers actually ask at companies like Meta, Airbnb, Netflix, and top startups.
Why React Interviews Are Different
React interviews test more than just syntax knowledge. Companies want developers who understand:
- How React works under the hood (reconciliation, virtual DOM, fiber)
- Performance optimization (when and why components re-render)
- State management patterns (local vs global, when to use each)
- Real-world problem solving (handling edge cases, debugging)
Knowing useState isn't enough. You need to explain why you'd choose it over useReducer, and when each makes sense.
Let's dive into the questions.
React Fundamentals Questions
1. What is the Virtual DOM and how does React use it?
Answer:
The Virtual DOM is a lightweight JavaScript representation of the actual DOM. When state changes in React:
- React creates a new Virtual DOM tree
- It compares (diffs) this with the previous Virtual DOM
- It calculates the minimal set of changes needed
- It batches and applies only those changes to the real DOM
Why this matters: Direct DOM manipulation is expensive. The Virtual DOM lets React batch updates and minimize actual DOM operations.
Follow-up to prepare for: "What's the time complexity of React's diffing algorithm?" (O(n) due to assumptions React makes about element types)
2. Explain the difference between controlled and uncontrolled components
Answer:
Controlled components: Form data is handled by React state. The component "controls" the input value.
function ControlledInput() {
const [value, setValue] = useState('');
return <input value={value} onChange={e => setValue(e.target.value)} />;
}
Uncontrolled components: Form data is handled by the DOM itself. You access values via refs.
function UncontrolledInput() {
const inputRef = useRef();
const handleSubmit = () => console.log(inputRef.current.value);
return <input ref={inputRef} />;
}
When to use each:
- Controlled: When you need validation, conditional disabling, or real-time formatting
- Uncontrolled: Simple forms, file inputs, or integrating with non-React code
3. What are React keys and why are they important?
Answer:
Keys help React identify which items in a list have changed, been added, or removed. They should be:
- Stable: Don't change between renders
- Unique: Among siblings (not globally)
- Predictable: Ideally tied to data identity
Bad practice:
// Using index as key - causes bugs with reordering
items.map((item, index) => <Item key={index} {...item} />)
Good practice:
// Using unique identifier
items.map(item => <Item key={item.id} {...item} />)
Why index is problematic: If you reorder items or insert at the beginning, React thinks components shifted rather than moved, causing unnecessary re-renders and state bugs.
React Hooks Questions
4. Explain the rules of hooks and why they exist
Answer:
The two rules:
- Only call hooks at the top level (not inside loops, conditions, or nested functions)
- Only call hooks from React functions (functional components or custom hooks)
Why these rules exist:
React relies on the order of hook calls to maintain state between renders. If you call hooks conditionally:
// BAD - breaks hook order
function Component({ isLoggedIn }) {
if (isLoggedIn) {
const [user, setUser] = useState(null); // Called conditionally!
}
const [theme, setTheme] = useState('dark');
}
On first render with isLoggedIn=true, React sees: useState, useState
On second render with isLoggedIn=false, React sees: useState
React can't match the second useState to its previous state because the order changed.
5. What's the difference between useEffect, useLayoutEffect, and useInsertionEffect?
Answer:
| Hook | When it runs | Use case |
|---|---|---|
useEffect |
After paint (async) | Data fetching, subscriptions, most side effects |
useLayoutEffect |
Before paint (sync) | DOM measurements, scroll position, animations |
useInsertionEffect |
Before any DOM mutations | CSS-in-JS libraries injecting styles |
Example where useLayoutEffect matters:
function Tooltip({ targetRef }) {
const [position, setPosition] = useState({ top: 0, left: 0 });
// useEffect causes flicker - tooltip appears, then moves
// useLayoutEffect positions correctly before paint
useLayoutEffect(() => {
const rect = targetRef.current.getBoundingClientRect();
setPosition({ top: rect.bottom, left: rect.left });
}, [targetRef]);
return <div style={position}>Tooltip</div>;
}
6. How does useCallback work and when should you use it?
Answer:
useCallback returns a memoized version of a callback that only changes when dependencies change.
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
When to use it:
- Passing callbacks to optimized child components:
function Parent() {
const [count, setCount] = useState(0);
// Without useCallback, handleClick is new every render
// causing ExpensiveChild to re-render unnecessarily
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return <ExpensiveChild onClick={handleClick} />;
}
const ExpensiveChild = React.memo(({ onClick }) => {
// Only re-renders when onClick actually changes
});
- Dependencies of other hooks:
const fetchData = useCallback(() => {
return api.getData(userId);
}, [userId]);
useEffect(() => {
fetchData();
}, [fetchData]); // Safe dependency
When NOT to use it: Don't wrap every function. The memoization itself has overhead. Only use when passing to memoized children or as hook dependencies.
7. Explain useMemo vs useCallback
Answer:
// useMemo - memoizes a VALUE
const memoizedValue = useMemo(() => computeExpensive(a, b), [a, b]);
// useCallback - memoizes a FUNCTION
const memoizedFn = useCallback(() => doSomething(a, b), [a, b]);
// These are equivalent:
useCallback(fn, deps)
useMemo(() => fn, deps)
Rule of thumb:
useMemofor expensive calculations that return valuesuseCallbackfor function references passed to children
8. What is useRef and what are its use cases?
Answer:
useRef returns a mutable ref object that persists across renders. The .current property can hold any value.
Use cases:
- Accessing DOM elements:
function TextInput() {
const inputRef = useRef(null);
const focusInput = () => inputRef.current.focus();
return <input ref={inputRef} />;
}
- Storing mutable values that don't trigger re-renders:
function Timer() {
const intervalRef = useRef(null);
useEffect(() => {
intervalRef.current = setInterval(() => {}, 1000);
return () => clearInterval(intervalRef.current);
}, []);
}
- Tracking previous values:
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
Key difference from state: Changing .current does NOT cause a re-render.
State Management Questions
9. When would you use useReducer instead of useState?
Answer:
Use useReducer when:
- State logic is complex:
// Multiple related values
const [state, dispatch] = useReducer(reducer, {
items: [],
loading: false,
error: null,
page: 1
});
- Next state depends on previous state:
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
case 'decrement':
return { ...state, count: state.count - 1 };
}
}
- You want predictable state transitions:
// All state changes go through the reducer
// Easy to debug and test
dispatch({ type: 'ADD_TODO', payload: newTodo });
Rule of thumb: If you find yourself with multiple useState calls that update together, or complex update logic, consider useReducer.
10. Explain React Context and its performance implications
Answer:
Context provides a way to pass data through the component tree without prop drilling.
const ThemeContext = createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button className={theme}>Click</button>;
}
Performance concern: When context value changes, ALL consumers re-render.
Solutions:
- Split contexts by update frequency:
// Separate rarely-changing from frequently-changing
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
<App />
</ThemeContext.Provider>
</UserContext.Provider>
- Memoize context value:
function Provider({ children }) {
const [state, setState] = useState(initial);
// Without useMemo, new object every render
const value = useMemo(() => ({ state, setState }), [state]);
return <Context.Provider value={value}>{children}</Context.Provider>;
}
- Split state and dispatch:
const StateContext = createContext();
const DispatchContext = createContext();
// Components that only dispatch don't re-render on state change
11. Compare Redux, Zustand, and Jotai — when would you use each?
Answer:
| Library | Best for | Approach |
|---|---|---|
| Redux | Large apps with complex state logic, time-travel debugging needed | Single store, actions, reducers, middleware |
| Zustand | Medium apps wanting Redux patterns with less boilerplate | Single store, simpler API, hooks-based |
| Jotai | Apps with many independent pieces of state | Atomic state, bottom-up approach |
Redux:
// Good for: Complex state machines, middleware (logging, async)
const store = createStore(reducer, applyMiddleware(thunk));
dispatch({ type: 'FETCH_USER', payload: userId });
Zustand:
// Good for: Redux-like but simpler, no boilerplate
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
Jotai:
// Good for: Independent pieces of state, derived state
const countAtom = atom(0);
const doubleCountAtom = atom((get) => get(countAtom) * 2);
Performance Optimization Questions
12. A component re-renders 1000 times per second. How do you debug and fix it?
Answer:
Step 1: Identify the cause with React DevTools Profiler
- Enable "Record why each component rendered"
- Look for unexpected prop or state changes
Step 2: Common causes and fixes:
- New object/array references in props:
// BAD - new array every render
<Child items={data.filter(x => x.active)} />
// GOOD - memoize the filtered result
const activeItems = useMemo(() => data.filter(x => x.active), [data]);
<Child items={activeItems} />
- Inline function handlers:
// BAD - new function every render
<Child onClick={() => handleClick(id)} />
// GOOD - memoize with useCallback
const handleChildClick = useCallback(() => handleClick(id), [id]);
<Child onClick={handleChildClick} />
- Context updates:
// Split frequently-updating context from stable context
- Missing React.memo:
const Child = React.memo(function Child({ data }) {
return <ExpensiveRender data={data} />;
});
Step 3: Verify fix
- Re-run profiler
- Check that unnecessary renders are eliminated
13. Explain React.memo and when it can hurt performance
Answer:
React.memo is a higher-order component that memoizes the rendered output. It only re-renders if props change (shallow comparison).
const MemoizedComponent = React.memo(function MyComponent({ name, data }) {
return <div>{name}: {data.value}</div>;
});
When it helps:
- Component renders often with same props
- Rendering is expensive
- Parent re-renders frequently but child props rarely change
When it hurts:
- Props always change anyway:
// Useless - onClick is new every render
<MemoizedButton onClick={() => doSomething()} />
- Component is cheap to render:
// Overhead of comparison > render cost
const MemoizedText = React.memo(({ text }) => <span>{text}</span>);
- Props are complex objects:
// Deep objects require custom comparison, adding complexity
React.memo(Component, (prevProps, nextProps) => {
return deepEqual(prevProps.data, nextProps.data); // Expensive!
});
14. What is code splitting and how do you implement it in React?
Answer:
Code splitting breaks your bundle into smaller chunks loaded on demand.
React.lazy + Suspense:
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<Loading />}>
<HeavyComponent />
</Suspense>
);
}
Route-based splitting:
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
);
}
Named exports:
// For named exports, create an intermediate module
// utils.js exports { heavyFunction }
// heavyFunction.js
export { heavyFunction as default } from './utils';
// then lazy(() => import('./heavyFunction'))
Advanced React Questions
15. Explain React Fiber architecture
Answer:
Fiber is React's reconciliation engine (introduced in React 16). It enables:
- Incremental rendering: Split rendering work into chunks
- Prioritization: Pause, abort, or reuse work based on priority
- Concurrency: Work on multiple trees simultaneously
How it works:
Each component instance has a corresponding "fiber" node containing:
- Component type and props
- Pointer to parent, child, and sibling fibers
- Work-in-progress state
- Effect tags (what DOM operations to perform)
Two phases:
- Render phase (interruptible): Build the fiber tree, calculate changes
- Commit phase (uninterruptible): Apply changes to DOM
Why it matters: Enables features like Suspense, transitions, and concurrent rendering.
16. What are React Server Components and how do they differ from SSR?
Answer:
| Aspect | SSR | Server Components |
|---|---|---|
| Where rendered | Server, then hydrated on client | Server only, never ships to client |
| JavaScript sent | Full component code | Only serialized output |
| Interactivity | Full after hydration | Must use Client Components |
| Data fetching | Usually separate (getServerSideProps) | Directly in component (async/await) |
Server Components:
// This never ships to the browser
async function ProductList() {
const products = await db.query('SELECT * FROM products');
return (
<ul>
{products.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
);
}
When to use each:
- Server Components: Data fetching, large dependencies, no interactivity
- Client Components: Event handlers, hooks, browser APIs
17. How would you implement infinite scrolling in React?
Answer:
Using Intersection Observer (recommended):
function InfiniteList() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const observerRef = useRef();
const loadMoreRef = useRef();
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && !loading) {
setPage(prev => prev + 1);
}
},
{ threshold: 1.0 }
);
if (loadMoreRef.current) {
observer.observe(loadMoreRef.current);
}
return () => observer.disconnect();
}, [loading]);
useEffect(() => {
setLoading(true);
fetchItems(page).then(newItems => {
setItems(prev => [...prev, ...newItems]);
setLoading(false);
});
}, [page]);
return (
<div>
{items.map(item => <Item key={item.id} {...item} />)}
<div ref={loadMoreRef}>
{loading && <Spinner />}
</div>
</div>
);
}
Performance considerations:
- Virtualize long lists (react-virtual, react-window)
- Debounce scroll events if using scroll listeners
- Clean up items that scroll out of view for very long lists
18. Explain the useTransition hook and when to use it
Answer:
useTransition lets you mark state updates as non-urgent, keeping the UI responsive.
function SearchResults() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
// Urgent: update input immediately
setQuery(e.target.value);
// Non-urgent: can be interrupted
startTransition(() => {
setResults(filterHugeDataset(e.target.value));
});
};
return (
<>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<ResultsList results={results} />
</>
);
}
Use cases:
- Filtering large lists while typing
- Tab switching with heavy content
- Any update where responsiveness matters more than immediate result
19. How do you handle errors in React components?
Answer:
Error Boundaries (class components only):
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
logErrorToService(error, errorInfo);
}
render() {
if (this.state.hasError) {
return <ErrorFallback error={this.state.error} />;
}
return this.props.children;
}
}
Usage:
<ErrorBoundary>
<FeatureComponent />
</ErrorBoundary>
What Error Boundaries DON'T catch:
- Event handlers (use try/catch)
- Async code (use .catch() or try/catch in async functions)
- Server-side rendering
- Errors in the boundary itself
Event handler errors:
function Button() {
const handleClick = async () => {
try {
await riskyOperation();
} catch (error) {
showErrorToast(error.message);
}
};
return <button onClick={handleClick}>Click</button>;
}
Coding Challenges
20. Implement a custom useDebounce hook
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// Usage
function Search() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) {
searchAPI(debouncedQuery);
}
}, [debouncedQuery]);
}
21. 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(setData)
.catch(err => {
if (err.name !== 'AbortError') {
setError(err);
}
})
.finally(() => setLoading(false));
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
// Usage
function UserProfile({ userId }) {
const { data, loading, error } = useFetch(`/api/users/${userId}`);
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <Profile user={data} />;
}
22. Implement a useLocalStorage hook
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
const setValue = useCallback((value) => {
try {
const valueToStore = value instanceof Function
? value(storedValue)
: value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
}, [key, storedValue]);
return [storedValue, setValue];
}
// Usage
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
Current: {theme}
</button>
);
}
System Design Questions for React
23. Design a real-time collaborative text editor
Key considerations:
- Conflict resolution: Use Operational Transformation (OT) or CRDTs
- Optimistic updates: Show local changes immediately
- WebSocket connection: Real-time sync with server
- Presence indicators: Show who's editing where
High-level architecture:
function CollaborativeEditor({ documentId }) {
const [content, setContent] = useState('');
const [cursors, setCursors] = useState({});
const wsRef = useRef();
useEffect(() => {
wsRef.current = new WebSocket(`/ws/doc/${documentId}`);
wsRef.current.onmessage = (event) => {
const { type, payload } = JSON.parse(event.data);
switch (type) {
case 'operation':
setContent(prev => applyOperation(prev, payload));
break;
case 'cursor':
setCursors(prev => ({ ...prev, [payload.userId]: payload.position }));
break;
}
};
return () => wsRef.current.close();
}, [documentId]);
const handleChange = (newContent, operation) => {
setContent(newContent); // Optimistic
wsRef.current.send(JSON.stringify({ type: 'operation', payload: operation }));
};
return <Editor content={content} cursors={cursors} onChange={handleChange} />;
}
24. Design a virtualized list for 100,000 items
Answer:
function VirtualizedList({ items, itemHeight, windowHeight }) {
const [scrollTop, setScrollTop] = useState(0);
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.min(
startIndex + Math.ceil(windowHeight / itemHeight) + 1,
items.length
);
const visibleItems = items.slice(startIndex, endIndex);
const offsetY = startIndex * itemHeight;
const totalHeight = items.length * itemHeight;
return (
<div
style={{ height: windowHeight, overflow: 'auto' }}
onScroll={e => setScrollTop(e.target.scrollTop)}
>
<div style={{ height: totalHeight, position: 'relative' }}>
<div style={{ transform: `translateY(${offsetY}px)` }}>
{visibleItems.map((item, i) => (
<div key={startIndex + i} style={{ height: itemHeight }}>
{item.content}
</div>
))}
</div>
</div>
</div>
);
}
Optimizations:
- Add overscan (render extra items above/below viewport)
- Debounce scroll handler
- Use
will-change: transformfor GPU acceleration
Quick-Fire Questions
25. What's the difference between props and state?
Props: Passed from parent, read-only. State: Managed within component, mutable via setState.
26. What is prop drilling and how do you avoid it?
Passing props through many intermediate components. Avoid with Context, state management libraries, or component composition.
27. What's the purpose of StrictMode?
Helps identify potential problems: deprecated APIs, unsafe lifecycles, unexpected side effects. Double-invokes functions in development to catch issues.
28. Can you modify state directly?
No. state.count = 5 won't trigger re-render. Always use setState or useState setter.
29. What is reconciliation?
React's algorithm for comparing Virtual DOM trees and determining minimal DOM updates needed.
30. What are fragments?
<React.Fragment> or <> lets you group children without adding extra DOM nodes.
Practice These Questions with AI
Reading these questions is step one. Practicing out loud is how you actually get comfortable explaining them.
Interview Whisper's PRACTICE mode lets you:
- Get asked real React interview questions by an AI interviewer
- Practice explaining concepts out loud (or via text)
- Receive instant feedback on clarity and completeness
- Focus on areas where you struggle
Don't walk into your React interview hoping you'll remember these answers. Practice until explaining Fiber architecture and useCallback feels natural.
Start Practicing React Interview Questions with AI
React Interview Preparation Checklist
Core Concepts:
- Virtual DOM and reconciliation
- Component lifecycle (class and hooks)
- Controlled vs uncontrolled components
- Keys and why they matter
Hooks Mastery:
- useState, useEffect, useRef basics
- useCallback vs useMemo - when to use each
- useReducer for complex state
- Custom hooks patterns
Performance:
- React.memo and when it helps/hurts
- Profiling with React DevTools
- Code splitting with lazy/Suspense
- Identifying and fixing re-render issues
Advanced Topics:
- Context API and performance implications
- Error boundaries
- Fiber architecture basics
- Server Components (if targeting React 18+)
Practice:
- Build custom hooks from scratch
- Explain concepts out loud
- Time yourself answering questions
- Do mock interviews
Related Articles
- System Design Interview Questions: Complete 2026 Guide
- FAANG Interview Preparation: Complete 2026 Guide
- 7 Common Coding Interview Mistakes and How to Avoid Them
- Master 10 Essential Algorithm Patterns for Coding Interviews
- LeetCode Interview Strategy: Blind 75 vs NeetCode 150
- STAR Method Interview: Complete Guide with 20+ Examples
- AI Interview Practice Platforms: Complete 2025 Guide
Master React interviews with practice, not just reading. The candidates who get offers can explain these concepts clearly under pressure. Start practicing today.