Top 30 React Interview Questions: Complete Guide to Succeed
The 30 most asked React interview questions in 2026. Detailed answers, code examples and tips to land your React developer position.

React technical interviews evaluate understanding of fundamental concepts, advanced patterns, and best practices. This guide presents the 30 most frequently asked questions, with detailed answers and code examples for effective preparation.
These questions are organized by difficulty level. Mastering the fundamentals before tackling advanced concepts allows for more structured preparation.
React Fundamentals
1. What is the Virtual DOM and why does React use it?
The Virtual DOM is a lightweight JavaScript representation of the real DOM. React uses this abstraction to optimize interface updates.
The process works in three steps: React first creates a virtual copy of the DOM, then compares this copy with the previous version when changes occur (diffing algorithm), and finally applies only the necessary modifications to the real DOM (reconciliation).
// Simplified example of the concept
// When state changes, React doesn't recreate the entire DOM
function Counter() {
const [count, setCount] = useState(0)
// Only the span containing count will be updated in the real DOM
// The rest of the component is untouched
return (
<div>
<h1>Counter</h1>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
)
}This approach avoids costly DOM operations and enables performant updates even for complex interfaces.
2. What is the difference between functional and class components?
Functional components are JavaScript functions that receive props and return JSX. Since React 16.8, hooks allow using state and lifecycle in functional components.
// Functional component (recommended)
// More concise, easier to test, supports hooks
function Welcome({ name }) {
const [visits, setVisits] = useState(0)
useEffect(() => {
setVisits(v => v + 1)
}, [])
return <h1>Hello {name}, visit #{visits}</h1>
}
// Class component (legacy)
// More verbose, requires this binding
class WelcomeClass extends React.Component {
state = { visits: 0 }
componentDidMount() {
this.setState(prev => ({ visits: prev.visits + 1 }))
}
render() {
return <h1>Hello {this.props.name}, visit #{this.state.visits}</h1>
}
}Functional components are now the standard. Class components remain supported but are no longer recommended for new projects.
3. How does JSX work?
JSX is a syntax extension for JavaScript that allows writing markup in code. It's not HTML but JavaScript in disguise.
// What we write (JSX)
const element = (
<div className="container">
<h1>Title</h1>
<p>Paragraph</p>
</div>
)
// What Babel compiles (pure JavaScript)
const element = React.createElement(
'div',
{ className: 'container' },
React.createElement('h1', null, 'Title'),
React.createElement('p', null, 'Paragraph')
)Differences from HTML include: className instead of class, htmlFor instead of for, camelCase for attributes (onClick, tabIndex), and mandatory closing of self-closing tags.
4. What is state vs props?
Props are data passed from a parent component to a child. They are read-only. State is a component's internal state, modifiable via setters.
// name and role are props (immutable)
function UserCard({ name, role }) {
// isExpanded is state (mutable)
const [isExpanded, setIsExpanded] = useState(false)
return (
<div className="card">
<h2>{name}</h2>
<p>{role}</p>
{/* Modifying state triggers a re-render */}
<button onClick={() => setIsExpanded(!isExpanded)}>
{isExpanded ? 'Collapse' : 'Details'}
</button>
{isExpanded && <UserDetails name={name} />}
</div>
)
}
// Usage
<UserCard name="Alice" role="Developer" />The fundamental rule: props flow down (parent → child), state is local to each component.
5. Why are keys important in lists?
Keys help React identify which elements have changed, been added, or removed in a list. Without unique and stable keys, React can have unexpected behaviors.
// ❌ Bad practice: index as key
// Problem: if order changes, React loses tracking
{items.map((item, index) => (
<ListItem key={index} item={item} />
))}
// ✅ Good practice: unique and stable identifier
{items.map(item => (
<ListItem key={item.id} item={item} />
))}
// Concrete example of the problem with indices
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React' },
{ id: 2, text: 'Create a project' }
])
// When deleting the first element with key={index}
// React will think element 0's content changed
// instead of understanding an element was removed
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
)
}React Hooks
6. Explain useState and its common pitfalls
useState manages local state in a functional component. The setter can take a value or an update function.
// Declaration with initial value
const [count, setCount] = useState(0)
// ❌ Pitfall: multiple updates in the same cycle
function increment() {
setCount(count + 1) // count = 0, sets 1
setCount(count + 1) // count = 0 still, sets 1
setCount(count + 1) // count = 0 still, sets 1
// Final result: 1 (not 3)
}
// ✅ Solution: use the update function
function incrementCorrect() {
setCount(prev => prev + 1) // 0 → 1
setCount(prev => prev + 1) // 1 → 2
setCount(prev => prev + 1) // 2 → 3
// Final result: 3
}
// ❌ Pitfall: object mutation
const [user, setUser] = useState({ name: 'Alice', age: 25 })
user.age = 26 // Direct mutation, no re-render
// ✅ Solution: create a new object
setUser({ ...user, age: 26 })
// or
setUser(prev => ({ ...prev, age: 26 }))7. How does useEffect work with its dependency array?
useEffect executes side effects after render. The dependency array controls when the effect runs.
// Executed on every render (rare, usually avoid)
useEffect(() => {
console.log('Render completed')
})
// Executed only on mount (equivalent to componentDidMount)
useEffect(() => {
console.log('Component mounted')
// Cleanup on unmount (equivalent to componentWillUnmount)
return () => {
console.log('Component unmounted')
}
}, [])
// Executed when userId changes
useEffect(() => {
async function fetchUser() {
const response = await fetch(`/api/users/${userId}`)
const data = await response.json()
setUser(data)
}
fetchUser()
}, [userId])
// ❌ Missing dependency - subtle bug
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1) // count is "captured" at its initial value
}, 1000)
return () => clearInterval(timer)
}, []) // count is missing from dependencies
// ✅ Fix with update function
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1) // No need for count in deps
}, 1000)
return () => clearInterval(timer)
}, [])Always enable eslint-plugin-react-hooks to detect missing dependencies. This rule prevents many hard-to-diagnose bugs.
8. When to use useMemo and useCallback?
These hooks enable memoization to avoid unnecessary recalculations or re-creations. Be careful not to overuse them.
// useMemo: memoizes a computed value
function ProductList({ products, filter }) {
// Recalculated only if products or filter change
const filteredProducts = useMemo(() => {
console.log('Filtering...')
return products.filter(p => p.category === filter)
}, [products, filter])
return <ul>{filteredProducts.map(p => <li key={p.id}>{p.name}</li>)}</ul>
}
// useCallback: memoizes a function
function ParentComponent() {
const [count, setCount] = useState(0)
// Without useCallback, handleClick is recreated on every render
// Causing unnecessary re-renders of ExpensiveChild
const handleClick = useCallback((id) => {
console.log('Clicked:', id)
}, []) // Empty deps = stable function
return (
<>
<span>{count}</span>
<button onClick={() => setCount(c => c + 1)}>+</button>
{/* React.memo on ExpensiveChild for this to be effective */}
<ExpensiveChild onClick={handleClick} />
</>
)
}
// ❌ Over-optimization: not needed here
const SimpleComponent = () => {
// This calculation is trivial, useMemo adds overhead
const doubled = useMemo(() => 2 * 2, [])
return <span>{doubled}</span>
}Use these hooks only when a performance problem is identified or to stabilize references passed to memoized components.
9. How does useRef work and what are its use cases?
useRef creates a mutable reference that persists between renders without triggering a re-render when it changes.
// Case 1: Access a DOM element
function TextInput() {
const inputRef = useRef(null)
const focusInput = () => {
inputRef.current.focus()
}
return (
<>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus</button>
</>
)
}
// Case 2: Store a mutable value without re-render
function Timer() {
const [seconds, setSeconds] = useState(0)
const intervalRef = useRef(null)
const start = () => {
// Store the interval ID to be able to stop it
intervalRef.current = setInterval(() => {
setSeconds(s => s + 1)
}, 1000)
}
const stop = () => {
clearInterval(intervalRef.current)
}
return (
<div>
<span>{seconds}s</span>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
)
}
// Case 3: Keep the previous value
function usePrevious(value) {
const ref = useRef()
useEffect(() => {
ref.current = value
}, [value])
return ref.current
}10. Explain useContext and when to use it
useContext accesses a React context without prop drilling. Ideal for global data like theme or logged-in user.
const ThemeContext = createContext({
theme: 'light',
toggleTheme: () => {}
})
// 2. Create the provider
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light')
const toggleTheme = useCallback(() => {
setTheme(prev => prev === 'light' ? 'dark' : 'light')
}, [])
// Memoize the value to avoid unnecessary re-renders
const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme])
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
)
}
// 3. Use the context
function ThemedButton() {
const { theme, toggleTheme } = useContext(ThemeContext)
return (
<button
onClick={toggleTheme}
className={theme === 'dark' ? 'bg-gray-800 text-white' : 'bg-white text-gray-800'}
>
{theme === 'dark' ? 'Light' : 'Dark'} mode
</button>
)
}
// 4. Wrap the application
function App() {
return (
<ThemeProvider>
<Header />
<Main />
<Footer />
</ThemeProvider>
)
}Ready to ace your React / Next.js interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Advanced Patterns
11. What is a Higher-Order Component (HOC)?
A HOC is a function that takes a component and returns a new enhanced component. Less used since hooks but still present in some libraries.
// HOC that adds logging
function withLogging(WrappedComponent) {
return function WithLogging(props) {
useEffect(() => {
console.log(`${WrappedComponent.name} mounted with props:`, props)
return () => {
console.log(`${WrappedComponent.name} unmounted`)
}
}, [])
return <WrappedComponent {...props} />
}
}
// HOC that handles authentication
function withAuth(WrappedComponent) {
return function WithAuth(props) {
const { user, isLoading } = useAuth()
if (isLoading) return <LoadingSpinner />
if (!user) return <Navigate to="/login" />
return <WrappedComponent {...props} user={user} />
}
}
// Usage
const ProtectedDashboard = withAuth(Dashboard)
const LoggedButton = withLogging(Button)12. Explain the Render Props pattern
The Render Props pattern shares logic between components via a prop that is a function.
// Component with render prop
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 })
useEffect(() => {
const handleMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY })
}
window.addEventListener('mousemove', handleMove)
return () => window.removeEventListener('mousemove', handleMove)
}, [])
// Call the render function with data
return render(position)
}
// Usage
function App() {
return (
<MouseTracker
render={({ x, y }) => (
<div>
Position: {x}, {y}
</div>
)}
/>
)
}
// Modern version with custom hook (preferred)
function useMousePosition() {
const [position, setPosition] = useState({ x: 0, y: 0 })
useEffect(() => {
const handleMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY })
}
window.addEventListener('mousemove', handleMove)
return () => window.removeEventListener('mousemove', handleMove)
}, [])
return position
}
function App() {
const { x, y } = useMousePosition()
return <div>Position: {x}, {y}</div>
}13. How to create a custom hook?
Custom hooks extract and reuse stateful logic between components.
function useLocalStorage(key, initialValue) {
// Initialize with localStorage value or default
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
console.error(error)
return initialValue
}
})
// Setter wrapper that syncs with localStorage
const setValue = useCallback((value) => {
try {
// Support update functions
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')
const [fontSize, setFontSize] = useLocalStorage('fontSize', 16)
return (
<div>
<select value={theme} onChange={e => setTheme(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<input
type="range"
min="12"
max="24"
value={fontSize}
onChange={e => setFontSize(Number(e.target.value))}
/>
</div>
)
}14. What is the Compound Components Pattern?
This pattern creates components that work together implicitly, like <select> and <option> tags.
// Shared context between components
const TabsContext = createContext()
function Tabs({ children, defaultValue }) {
const [activeTab, setActiveTab] = useState(defaultValue)
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
)
}
function TabList({ children }) {
return <div className="tab-list" role="tablist">{children}</div>
}
function Tab({ value, children }) {
const { activeTab, setActiveTab } = useContext(TabsContext)
const isActive = activeTab === value
return (
<button
role="tab"
aria-selected={isActive}
className={`tab ${isActive ? 'active' : ''}`}
onClick={() => setActiveTab(value)}
>
{children}
</button>
)
}
function TabPanel({ value, children }) {
const { activeTab } = useContext(TabsContext)
if (activeTab !== value) return null
return (
<div role="tabpanel" className="tab-panel">
{children}
</div>
)
}
// Attach sub-components
Tabs.List = TabList
Tabs.Tab = Tab
Tabs.Panel = TabPanel
// Intuitive usage
function App() {
return (
<Tabs defaultValue="profile">
<Tabs.List>
<Tabs.Tab value="profile">Profile</Tabs.Tab>
<Tabs.Tab value="settings">Settings</Tabs.Tab>
<Tabs.Tab value="billing">Billing</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="profile">Profile content...</Tabs.Panel>
<Tabs.Panel value="settings">Settings content...</Tabs.Panel>
<Tabs.Panel value="billing">Billing content...</Tabs.Panel>
</Tabs>
)
}15. How to implement Controlled vs Uncontrolled pattern?
Controlled components have their state managed by React, uncontrolled ones use the DOM directly.
// CONTROLLED component
// State is in React, component reflects that state
function ControlledInput() {
const [value, setValue] = useState('')
const handleSubmit = (e) => {
e.preventDefault()
console.log('Submitted value:', value)
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<button type="submit">Submit</button>
</form>
)
}
// UNCONTROLLED component
// State is in the DOM, we read it when needed
function UncontrolledInput() {
const inputRef = useRef(null)
const handleSubmit = (e) => {
e.preventDefault()
console.log('Submitted value:', inputRef.current.value)
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
ref={inputRef}
defaultValue=""
/>
<button type="submit">Submit</button>
</form>
)
}
// Component that supports BOTH modes
function FlexibleInput({ value, defaultValue, onChange }) {
const isControlled = value !== undefined
const [internalValue, setInternalValue] = useState(defaultValue || '')
const currentValue = isControlled ? value : internalValue
const handleChange = (e) => {
if (!isControlled) {
setInternalValue(e.target.value)
}
onChange?.(e.target.value)
}
return (
<input
type="text"
value={currentValue}
onChange={handleChange}
/>
)
}Performance and Optimization
16. How does React.memo work?
React.memo is a HOC that memoizes a component to avoid re-renders if props haven't changed.
// Memoized component
const ExpensiveList = React.memo(function ExpensiveList({ items, onItemClick }) {
console.log('ExpensiveList rendered')
return (
<ul>
{items.map(item => (
<li key={item.id} onClick={() => onItemClick(item.id)}>
{item.name}
</li>
))}
</ul>
)
})
// Parent using the memoized component
function Parent() {
const [count, setCount] = useState(0)
const [items] = useState([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' }
])
// ❌ This function is recreated on every render
// So ExpensiveList re-renders despite memo
const handleClick = (id) => console.log(id)
// ✅ Stable function with useCallback
const handleClickStable = useCallback((id) => {
console.log(id)
}, [])
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<ExpensiveList items={items} onItemClick={handleClickStable} />
</div>
)
}
// Custom comparison
const MemoWithCustomCompare = React.memo(
function Component({ user, onClick }) {
return <div onClick={onClick}>{user.name}</div>
},
(prevProps, nextProps) => {
// Return true if props are equal (skip re-render)
return prevProps.user.id === nextProps.user.id
}
)17. What is code splitting and React.lazy?
Code splitting divides the bundle into chunks loaded on demand, reducing initial load time.
// Lazy loading components
const Dashboard = lazy(() => import('./Dashboard'))
const Settings = lazy(() => import('./Settings'))
const Profile = lazy(() => import('./Profile'))
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</Suspense>
)
}
// Conditional lazy loading
function FeaturePanel({ showAdvanced }) {
// Component is only loaded if needed
const AdvancedOptions = lazy(() => import('./AdvancedOptions'))
return (
<div>
<BasicOptions />
{showAdvanced && (
<Suspense fallback={<Skeleton />}>
<AdvancedOptions />
</Suspense>
)}
</div>
)
}
// Named exports with lazy
const { Chart } = lazy(() =>
import('./Charts').then(module => ({ default: module.Chart }))
)18. How to optimize long lists?
Long lists can cause performance issues. Virtualization renders only visible elements.
// With react-window (virtualization library)
import { FixedSizeList } from 'react-window'
function VirtualizedList({ items }) {
const Row = ({ index, style }) => (
<div style={style} className="list-item">
{items[index].name}
</div>
)
return (
<FixedSizeList
height={400}
width="100%"
itemCount={items.length}
itemSize={50}
>
{Row}
</FixedSizeList>
)
}
// Optimization without library: pagination
function PaginatedList({ items, pageSize = 20 }) {
const [page, setPage] = useState(0)
const paginatedItems = useMemo(() => {
const start = page * pageSize
return items.slice(start, start + pageSize)
}, [items, page, pageSize])
const totalPages = Math.ceil(items.length / pageSize)
return (
<div>
<ul>
{paginatedItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
<div className="pagination">
<button
onClick={() => setPage(p => p - 1)}
disabled={page === 0}
>
Previous
</button>
<span>{page + 1} / {totalPages}</span>
<button
onClick={() => setPage(p => p + 1)}
disabled={page >= totalPages - 1}
>
Next
</button>
</div>
</div>
)
}Virtualization becomes relevant beyond a few hundred elements. For smaller lists, pagination or progressive loading is often sufficient.
19. How to avoid unnecessary re-renders?
Identifying and eliminating unnecessary re-renders is crucial for performance. Several techniques optimize rendering.
// Technique 1: Separate components
// ❌ Everything re-renders when count changes
function BadExample() {
const [count, setCount] = useState(0)
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<ExpensiveComponent /> {/* Unnecessary re-render */}
</div>
)
}
// ✅ ExpensiveComponent no longer re-renders
function GoodExample() {
return (
<div>
<Counter />
<ExpensiveComponent />
</div>
)
}
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}
// Technique 2: Pass children instead of rendering directly
// ✅ children is stable, no re-render of children
function ContextProvider({ children }) {
const [value, setValue] = useState(0)
return (
<Context.Provider value={value}>
{children} {/* children doesn't re-render when value changes */}
</Context.Provider>
)
}
// Technique 3: Use DevTools to identify re-renders
// React DevTools > Profiler > "Highlight updates when components render"20. What is automatic batching in React 18+?
React 18 automatically groups state updates to reduce the number of re-renders.
// Before React 18: two re-renders
function handleClick() {
setCount(c => c + 1) // Re-render
setFlag(f => !f) // Re-render
}
// React 18+: single re-render (automatic batching)
function handleClick() {
setCount(c => c + 1) // Batched
setFlag(f => !f) // Batched
// Single re-render at the end
}
// Even in async callbacks (React 18 novelty)
async function handleSubmit() {
const response = await fetch('/api/submit')
// These two updates are batched
setData(response.data)
setLoading(false)
// Single re-render
}
// To force immediate re-render (rare)
import { flushSync } from 'react-dom'
function handleClick() {
flushSync(() => {
setCount(c => c + 1)
})
// DOM updated here
flushSync(() => {
setFlag(f => !f)
})
// DOM updated here
}Ready to ace your React / Next.js interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Modern React (18+)
21. Explain useTransition and useDeferredValue
These hooks mark updates as non-urgent to maintain interface responsiveness.
// useTransition: mark an update as non-urgent
function SearchResults() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [isPending, startTransition] = useTransition()
const handleChange = (e) => {
// Urgent update: input stays responsive
setQuery(e.target.value)
// Non-urgent update: can be interrupted
startTransition(() => {
const filtered = filterLargeDataset(e.target.value)
setResults(filtered)
})
}
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <span>Searching...</span>}
<ul>
{results.map(r => <li key={r.id}>{r.name}</li>)}
</ul>
</div>
)
}
// useDeferredValue: defer a value
function DeferredSearch() {
const [query, setQuery] = useState('')
// deferredQuery is "behind" during rapid updates
const deferredQuery = useDeferredValue(query)
// Show indicator during the "lag"
const isStale = query !== deferredQuery
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
<div style={{ opacity: isStale ? 0.5 : 1 }}>
{/* Uses deferred value, fewer calculations */}
<ExpensiveList filter={deferredQuery} />
</div>
</div>
)
}22. How does Suspense work for data fetching?
Suspense manages loading states declaratively. With React 18+, it extends to data fetching.
function UserProfile({ userId }) {
return (
<Suspense fallback={<ProfileSkeleton />}>
<ProfileContent userId={userId} />
</Suspense>
)
}
// The component "suspends" during loading
function ProfileContent({ userId }) {
// This function suspends if data isn't ready
const user = useUserData(userId)
return (
<div>
<h1>{user.name}</h1>
<p>{user.bio}</p>
</div>
)
}
// Nested Suspense boundaries
function Dashboard() {
return (
<Suspense fallback={<DashboardSkeleton />}>
<Header />
<div className="grid">
<Suspense fallback={<StatsSkeleton />}>
<Stats />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<Chart />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<RecentActivity />
</Suspense>
</div>
</Suspense>
)
}23. What is useActionState (React 19)?
useActionState (formerly useFormState) simplifies form handling with Server Actions.
'use server'
async function submitForm(prevState, formData) {
const email = formData.get('email')
const password = formData.get('password')
// Validation
if (!email || !password) {
return { error: 'All fields are required' }
}
try {
await createUser({ email, password })
return { success: true, message: 'Account created!' }
} catch (e) {
return { error: e.message }
}
}
// Component
function SignupForm() {
const [state, formAction, isPending] = useActionState(submitForm, {})
return (
<form action={formAction}>
{state.error && (
<div className="error">{state.error}</div>
)}
{state.success && (
<div className="success">{state.message}</div>
)}
<input name="email" type="email" placeholder="Email" />
<input name="password" type="password" placeholder="Password" />
<button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create account'}
</button>
</form>
)
}24. How do Server Components work?
Server Components run exclusively on the server, reducing JavaScript sent to the client.
// No "use client" = Server Component by default
import { prisma } from '@/lib/prisma'
// async/await directly in the component
export default async function ProductList() {
// Direct database call (no API)
const products = await prisma.product.findMany({
orderBy: { createdAt: 'desc' },
take: 10
})
return (
<div>
<h2>Recent Products</h2>
<ul>
{products.map(product => (
<li key={product.id}>
{product.name} - ${product.price}
{/* Can include Client Components */}
<AddToCartButton productId={product.id} />
</li>
))}
</ul>
</div>
)
}
// AddToCartButton.jsx
'use client'
// This component needs interactivity
export function AddToCartButton({ productId }) {
const [isPending, startTransition] = useTransition()
const handleClick = () => {
startTransition(async () => {
await addToCart(productId)
})
}
return (
<button onClick={handleClick} disabled={isPending}>
{isPending ? '...' : 'Add'}
</button>
)
}25. Explain SSR streaming with React 18
SSR streaming sends HTML progressively to the browser instead of waiting for complete render.
import { Suspense } from 'react'
export default function Page() {
return (
<div>
{/* Immediate render */}
<Header />
{/* Streamed when ready */}
<Suspense fallback={<MainContentSkeleton />}>
<MainContent />
</Suspense>
{/* Streamed independently */}
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
{/* Immediate render */}
<Footer />
</div>
)
}
// The browser receives:
// 1. HTML shell with Header, skeletons, Footer
// 2. MainContent when its request completes
// 3. Sidebar when its request completes
// loading.jsx for route-level streaming
export default function Loading() {
return <PageSkeleton />
}State Management
26. When to use Redux vs Context API vs other solutions?
The choice depends on state complexity and project needs.
// Context API: simple state, few updates
// ✅ Good for: theme, user, preferences
const ThemeContext = createContext()
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light')
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
)
}
// Redux / Zustand: complex state, frequent updates
// ✅ Good for: cart, filters, complex business data
import { create } from 'zustand'
const useCartStore = create((set, get) => ({
items: [],
total: 0,
addItem: (product) => set((state) => {
const existing = state.items.find(i => i.id === product.id)
if (existing) {
return {
items: state.items.map(i =>
i.id === product.id ? { ...i, quantity: i.quantity + 1 } : i
)
}
}
return { items: [...state.items, { ...product, quantity: 1 }] }
}),
removeItem: (id) => set((state) => ({
items: state.items.filter(i => i.id !== id)
})),
getTotal: () => get().items.reduce((sum, i) => sum + i.price * i.quantity, 0)
}))
// React Query / SWR: server state (cache, refetch)
// ✅ Good for: API data, server synchronization
import { useQuery, useMutation } from '@tanstack/react-query'
function Products() {
const { data, isLoading } = useQuery({
queryKey: ['products'],
queryFn: () => fetch('/api/products').then(r => r.json())
})
const mutation = useMutation({
mutationFn: (newProduct) => fetch('/api/products', {
method: 'POST',
body: JSON.stringify(newProduct)
})
})
}27. How to implement the Reducer pattern?
The Reducer pattern centralizes complex state update logic.
// Define actions
const ACTIONS = {
ADD_TODO: 'ADD_TODO',
TOGGLE_TODO: 'TOGGLE_TODO',
DELETE_TODO: 'DELETE_TODO',
SET_FILTER: 'SET_FILTER'
}
// Pure reducer (no side effects)
function todoReducer(state, action) {
switch (action.type) {
case ACTIONS.ADD_TODO:
return {
...state,
todos: [
...state.todos,
{ id: Date.now(), text: action.payload, completed: false }
]
}
case ACTIONS.TOGGLE_TODO:
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
)
}
case ACTIONS.DELETE_TODO:
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload)
}
case ACTIONS.SET_FILTER:
return { ...state, filter: action.payload }
default:
return state
}
}
// Usage with useReducer
function TodoApp() {
const [state, dispatch] = useReducer(todoReducer, {
todos: [],
filter: 'all'
})
const addTodo = (text) => {
dispatch({ type: ACTIONS.ADD_TODO, payload: text })
}
const toggleTodo = (id) => {
dispatch({ type: ACTIONS.TOGGLE_TODO, payload: id })
}
const filteredTodos = useMemo(() => {
switch (state.filter) {
case 'completed':
return state.todos.filter(t => t.completed)
case 'active':
return state.todos.filter(t => !t.completed)
default:
return state.todos
}
}, [state.todos, state.filter])
return (
<div>
<TodoForm onAdd={addTodo} />
<TodoList todos={filteredTodos} onToggle={toggleTodo} />
<FilterButtons
current={state.filter}
onChange={(f) => dispatch({ type: ACTIONS.SET_FILTER, payload: f })}
/>
</div>
)
}Testing
28. How to test a React component?
Component tests verify rendering and interface behavior.
import { render, screen, fireEvent } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Button from './Button'
describe('Button', () => {
// Basic rendering test
it('renders with correct text', () => {
render(<Button>Click me</Button>)
expect(screen.getByRole('button')).toHaveTextContent('Click me')
})
// Interaction test
it('calls onClick when clicked', async () => {
const handleClick = jest.fn()
render(<Button onClick={handleClick}>Click</Button>)
await userEvent.click(screen.getByRole('button'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
// Disabled state test
it('does not call onClick when disabled', async () => {
const handleClick = jest.fn()
render(<Button disabled onClick={handleClick}>Click</Button>)
await userEvent.click(screen.getByRole('button'))
expect(handleClick).not.toHaveBeenCalled()
})
// Async state test
it('shows loading state', async () => {
render(<Button isLoading>Submit</Button>)
expect(screen.getByRole('button')).toBeDisabled()
expect(screen.getByTestId('spinner')).toBeInTheDocument()
})
})
// Testing a custom hook
import { renderHook, act } from '@testing-library/react'
import useCounter from './useCounter'
describe('useCounter', () => {
it('increments counter', () => {
const { result } = renderHook(() => useCounter(0))
act(() => {
result.current.increment()
})
expect(result.current.count).toBe(1)
})
})29. How to mock API calls in tests?
Mocks isolate tests from external dependencies.
// With MSW (Mock Service Worker) - recommended
import { setupServer } from 'msw/node'
import { http, HttpResponse } from 'msw'
const server = setupServer(
http.get('/api/users', () => {
return HttpResponse.json([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
])
}),
http.post('/api/users', async ({ request }) => {
const body = await request.json()
return HttpResponse.json({ id: 3, ...body }, { status: 201 })
})
)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
test('loads and displays users', async () => {
render(<UserList />)
expect(screen.getByText('Loading...')).toBeInTheDocument()
await waitFor(() => {
expect(screen.getByText('Alice')).toBeInTheDocument()
expect(screen.getByText('Bob')).toBeInTheDocument()
})
})
// Override for a specific test
test('handles error state', async () => {
server.use(
http.get('/api/users', () => {
return HttpResponse.json({ error: 'Server error' }, { status: 500 })
})
)
render(<UserList />)
await waitFor(() => {
expect(screen.getByText('Loading error')).toBeInTheDocument()
})
})30. How to structure tests for optimal coverage?
A balanced testing strategy combines different testing levels.
// Recommended testing pyramid:
// - Many unit tests (fast, isolated)
// - Some integration tests (components + hooks)
// - Few E2E tests (critical scenarios)
// Unit tests: pure logic
// utils/formatPrice.test.js
describe('formatPrice', () => {
it('formats with 2 decimals', () => {
expect(formatPrice(10)).toBe('$10.00')
})
it('handles zero', () => {
expect(formatPrice(0)).toBe('$0.00')
})
})
// Integration tests: complete component
// features/Checkout/Checkout.test.jsx
describe('Checkout', () => {
it('completes purchase flow', async () => {
render(
<CartProvider>
<Checkout />
</CartProvider>
)
// Fill the form
await userEvent.type(screen.getByLabelText('Email'), 'test@example.com')
await userEvent.type(screen.getByLabelText('Card'), '4242424242424242')
// Submit
await userEvent.click(screen.getByRole('button', { name: 'Pay' }))
// Verify result
await waitFor(() => {
expect(screen.getByText('Order confirmed')).toBeInTheDocument()
})
})
})
// E2E tests with Playwright
// e2e/checkout.spec.ts
test('user can complete checkout', async ({ page }) => {
await page.goto('/products')
await page.click('[data-testid="add-to-cart-1"]')
await page.click('[data-testid="checkout-button"]')
await page.fill('[name="email"]', 'test@example.com')
await page.fill('[name="card"]', '4242424242424242')
await page.click('button[type="submit"]')
await expect(page.locator('text=Order confirmed')).toBeVisible()
})Conclusion
These 30 questions cover the essential React knowledge expected in interviews. Key areas to master:
- ✅ Fundamentals: Virtual DOM, JSX, props vs state, components
- ✅ Hooks: useState, useEffect, useMemo, useCallback, useRef, useContext
- ✅ Patterns: HOC, Render Props, Compound Components, custom hooks
- ✅ Performance: React.memo, lazy loading, virtualization
- ✅ Modern React: Suspense, Transitions, Server Components
- ✅ State management: Context, Redux/Zustand, React Query
- ✅ Testing: React Testing Library, mocks, testing strategy
React interview preparation is not just about memorization. Practicing with real projects helps consolidate these concepts and explain them naturally during the interview.
Start practicing!
Test your knowledge with our interview simulators and technical tests.
Tags
Share
Related articles

React Server Components in Production: Patterns and Pitfalls
React Server Components in production: battle-tested patterns, common anti-patterns, and debugging strategies for robust Next.js 15 applications.

Advanced React Hooks: Patterns and Optimizations
Master advanced React Hooks with proven patterns. Custom hooks, optimized useEffect, useMemo, useCallback and performance techniques.

React 19: Server Components in Production - The Complete Guide
Master React 19 Server Components in production. Architecture, patterns, streaming, caching, and optimizations for high-performance applications.