⚛️
Intermediate
14 min readReact Interview Questions
React hooks, components, lifecycle, and best practices
Essential React Interview Questions
Master React concepts that are frequently asked in interviews. This guide covers hooks, lifecycle, virtual DOM, reconciliation, and performance optimization.
1. Explain Virtual DOM and Reconciliation
The Virtual DOM is a lightweight JavaScript representation of the actual DOM. React uses it to optimize DOM updates.
How it works:
- Component state/props change
- React creates new Virtual DOM tree
- Compares new tree with previous (diffing algorithm)
- Calculates minimum changes needed (reconciliation)
- Updates only changed parts in real DOM (batching)
// React optimizes this:
function App() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Counter</h1>
<p>{count}</p> {/* Only this updates, not entire div */}
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
2. useState Hook - Common Patterns
import { useState } from 'react';
// Basic usage
const [count, setCount] = useState(0);
const [user, setUser] = useState({ name: 'John', age: 30 });
// Functional update (when new state depends on previous)
setCount(prevCount => prevCount + 1);
// Object state - always merge manually
setUser(prevUser => ({
...prevUser,
age: 31 // Only update age
}));
// Lazy initialization (expensive computation)
const [data, setData] = useState(() => {
const initialData = expensiveComputation();
return initialData;
});
// Multiple state variables vs single object
// ❌ Single object (causes unnecessary re-renders)
const [state, setState] = useState({
name: '',
email: '',
age: 0
});
// ✅ Separate state variables
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState(0);
// State batching (React 18+)
function handleClick() {
setCount(count + 1);
setUser({ name: 'Jane' });
// Only one re-render (automatic batching)
}
3. useEffect Hook - Lifecycle and Cleanup
import { useEffect, useState } from 'react';
function Component() {
// Run once on mount (empty dependency array)
useEffect(() => {
console.log('Component mounted');
// Cleanup on unmount
return () => {
console.log('Component unmounting');
};
}, []);
// Run when dependencies change
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
// No dependency array - runs after every render (avoid!)
useEffect(() => {
console.log('Runs after every render');
});
// Fetching data
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchData() {
try {
const response = await fetch('/api/data');
const json = await response.json();
if (!cancelled) {
setData(json);
}
} catch (err) {
if (!cancelled) {
setError(err);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchData();
return () => {
cancelled = true; // Prevent state update if unmounted
};
}, []);
// Event listeners cleanup
useEffect(() => {
function handleScroll() {
console.log('Scrolling');
}
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
// Timer cleanup
useEffect(() => {
const timerId = setInterval(() => {
console.log('Tick');
}, 1000);
return () => clearInterval(timerId);
}, []);
}
4. useMemo and useCallback - Performance Optimization
import { useMemo, useCallback, useState } from 'react';
function ExpensiveComponent({ items }) {
// useMemo - memoize computed values
const total = useMemo(() => {
console.log('Calculating total...');
return items.reduce((sum, item) => sum + item.price, 0);
}, [items]); // Recalculate only when items change
// Without useMemo, this runs on every render
const sortedItems = useMemo(() => {
return [...items].sort((a, b) => a.price - b.price);
}, [items]);
// useCallback - memoize functions
const handleClick = useCallback((id) => {
console.log('Clicked:', id);
}, []); // Function reference stays the same
return (
<div>
<p>Total: $\{total}</p>
{sortedItems.map(item => (
<Item key={item.id} item={item} onClick={handleClick} />
))}
</div>
);
}
// Child component wrapped in memo
const Item = React.memo(({ item, onClick }) => {
console.log('Item rendered:', item.id);
return (
<div onClick={() => onClick(item.id)}>
{item.name} - $\{item.price}
</div>
);
});
// When to use:
// ✅ Expensive calculations
// ✅ Prevent unnecessary re-renders of child components
// ❌ Don't use for simple operations (overhead not worth it)
5. useRef - Refs and DOM Access
import { useRef, useEffect } from 'react';
function Component() {
// Access DOM elements
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus(); // Focus input on mount
}, []);
// Store mutable value that doesn't cause re-render
const countRef = useRef(0);
function handleClick() {
countRef.current += 1; // No re-render
console.log('Clicked:', countRef.current);
}
// Store previous value
const [value, setValue] = useState('');
const prevValueRef = useRef('');
useEffect(() => {
prevValueRef.current = value;
}, [value]);
console.log('Current:', value, 'Previous:', prevValueRef.current);
// Store interval/timeout ID
const intervalRef = useRef(null);
function startTimer() {
intervalRef.current = setInterval(() => {
console.log('Tick');
}, 1000);
}
function stopTimer() {
clearInterval(intervalRef.current);
}
return (
<div>
<input ref={inputRef} />
<button onClick={handleClick}>Click (no re-render)</button>
</div>
);
}
6. useContext - Global State Management
import { createContext, useContext, useState } from 'react';
// Create context
const ThemeContext = createContext(null);
// Provider component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Custom hook for easier usage
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
// Usage in components
function App() {
return (
<ThemeProvider>
<Header />
<Content />
</ThemeProvider>
);
}
function Header() {
const { theme, toggleTheme } = useTheme();
return (
<header className={theme}>
<button onClick={toggleTheme}>
Toggle Theme
</button>
</header>
);
}
7. Custom Hooks
import { useState, useEffect } from 'react';
// useFetch - Data fetching hook
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchData() {
try {
const response = await fetch(url);
const json = await response.json();
setData(json);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
}
fetchData();
}, [url]);
return { data, loading, error };
}
// Usage
function UserProfile({ userId }) {
const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{user.name}</div>;
}
// useLocalStorage - Persist state
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// Usage
function Settings() {
const [settings, setSettings] = useLocalStorage('settings', {
theme: 'light',
notifications: true
});
}
// useDebounce - Debounced value
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
// Usage
function SearchComponent() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 500);
useEffect(() => {
if (debouncedQuery) {
// Fetch search results
}
}, [debouncedQuery]);
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
8. Component Lifecycle (Class vs Hooks)
// Class Component
class ClassComponent extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
componentDidMount() {
// After first render
console.log('Mounted');
}
componentDidUpdate(prevProps, prevState) {
// After updates
if (prevState.count !== this.state.count) {
console.log('Count changed');
}
}
componentWillUnmount() {
// Before component is removed
console.log('Unmounting');
}
render() {
return <div>{this.state.count}</div>;
}
}
// Equivalent with Hooks
function HookComponent() {
const [count, setCount] = useState(0);
// componentDidMount
useEffect(() => {
console.log('Mounted');
}, []);
// componentDidUpdate for specific value
useEffect(() => {
console.log('Count changed');
}, [count]);
// componentWillUnmount
useEffect(() => {
return () => {
console.log('Unmounting');
};
}, []);
return <div>{count}</div>;
}
9. React.memo and Component Optimization
import React, { memo } from 'react';
// Without memo - re-renders even if props don't change
function ExpensiveComponent({ data }) {
console.log('Rendering ExpensiveComponent');
return <div>{data}</div>;
}
// With memo - only re-renders if props change
const MemoizedComponent = memo(function ExpensiveComponent({ data }) {
console.log('Rendering MemoizedComponent');
return <div>{data}</div>;
});
// Custom comparison function
const CustomMemoComponent = memo(
function Component({ user }) {
return <div>{user.name}</div>;
},
(prevProps, nextProps) => {
// Return true to skip re-render
return prevProps.user.id === nextProps.user.id;
}
);
// Parent component
function Parent() {
const [count, setCount] = useState(0);
const [data] = useState('Static data');
return (
<div>
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
{/* This re-renders on every count change */}
<ExpensiveComponent data={data} />
{/* This doesn't re-render (data hasn't changed) */}
<MemoizedComponent data={data} />
</div>
);
}
10. Error Boundaries
// Error boundaries must be class components
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
// Update state so next render shows fallback UI
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// Log error to error reporting service
console.error('Error caught:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div>
<h1>Something went wrong</h1>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
);
}
return this.props.children;
}
}
// Usage
function App() {
return (
<ErrorBoundary>
<ProblematicComponent />
</ErrorBoundary>
);
}
// Error boundaries catch:
// ✅ Errors in render methods
// ✅ Errors in lifecycle methods
// ✅ Errors in constructor
// Error boundaries DON'T catch:
// ❌ Event handlers (use try-catch)
// ❌ Async code (setTimeout, promises)
// ❌ Server-side rendering
// ❌ Errors in error boundary itself
Key Interview Takeaways:
- Virtual DOM optimizes updates through diffing and reconciliation
- useState for component state, useEffect for side effects
- useMemo for expensive calculations, useCallback for memoized functions
- useRef for DOM access and storing mutable values
- useContext for global state (avoid prop drilling)
- Custom hooks for reusable logic
- React.memo to prevent unnecessary re-renders
- Error boundaries catch rendering errors
- Always cleanup useEffect (event listeners, timers, subscriptions)