React Interview Questions (2026)
The React ecosystem continues to evolve at a rapid pace. As we navigate through 2026, building production-ready applications requires deep expertise beyond basic component composition and standard `useState` hooks. Modern React architecture embraces React Server Components (RSC), concurrent rendering features, granular memoization, custom hooks, and optimized state synchronization across distributed client-server boundaries.
Whether you are interviewing for senior frontend engineering roles or seeking to solidify your mastery of modern React internals, this comprehensive guide covers the most critical React interview questions for 2026, complete with deep-dive technical explanations and production-grade code examples.
1. What are React Server Components (RSC), and how do they differ from Client Components?
React Server Components (RSC) represent a foundational paradigm shift in how React applications are architected and rendered. Unlike traditional client-side components that execute entirely inside the browser, RSCs execute exclusively on the server prior to sending any HTML or serialized payload down to the client.
- Zero Client-Side Bundle Impact: Server Components can import heavy libraries, database connectors, or markdown parsers without adding a single byte to the client JavaScript bundle. Dependencies remain on the server.
- Direct Backend Access: RSCs can query databases, read local file systems, or fetch secure internal APIs directly without needing intermediate REST or GraphQL wrapper endpoints.
- Client Components Boundary: When you need interactivity (e.g., `useState`, `useEffect`, browser event listeners like `onClick`), you explicitly opt into client execution using the `'use client'` directive at the top of the file.
// app/posts/page.jsx (React Server Component by default in App Router)
import db from '@/lib/db';
import LikeButton from '@/components/LikeButton'; // Client Component
export default async function PostsPage() {
// Direct server-side database query
const posts = await db.query('SELECT * FROM posts ORDER BY created_at DESC');
return (
<main className="max-w-2xl mx-auto p-6">
<h1 className="text-3xl font-bold mb-6">Community Posts</h1>
{posts.map(post => (
<article key={post.id} className="mb-4 p-4 bg-white rounded shadow">
<h2 className="text-xl font-semibold">{post.title}</h2>
<p className="text-gray-700 mb-3">{post.content}</p>
{/* Interactive client component island */}
<LikeButton postId={post.id} initialLikes={post.likes} />
</article>
))}
</main>
);
}
2. Explain Concurrent React and how features like `useTransition` and `useDeferredValue` improve user experience.
Concurrency in React is an underlying implementation mechanism that enables React to prepare multiple versions of the UI simultaneously. By breaking rendering work into small, interruptible chunks, React ensures that high-priority updates (such as keyboard typing, button clicks, or animations) are never blocked by heavy rendering computations (such as filtering large lists or rendering complex component trees).
`useTransition` allows developers to mark state updates as non-urgent transitions, letting the browser keep input fields responsive while background filtering or data fetching occurs.
`useDeferredValue` accepts a value and returns a deferred version of that value that 'lags behind' the original value during heavy renders, preventing UI stutter.
// components/SearchFilter.jsx
'use client';
import { useState, useTransition, useMemo } from 'react';
export default function SearchFilter({ items }) {
const [query, setQuery] = useState('');
const [filteredItems, setFilteredItems] = useState(items);
const [isPending, startTransition] = useTransition();
const handleInputChange = (e) => {
const value = e.target.value;
setQuery(value); // Urgent state update (keeps input responsive)
startTransition(() => {
// Non-urgent state update (filtered list calculation)
const results = items.filter(item => item.toLowerCase().includes(value.toLowerCase()));
setFilteredItems(results);
});
};
return (
<div className="p-4">
<input
type="text"
value={query}
onChange={handleInputChange}
placeholder="Search items..."
className="border p-2 rounded w-full mb-4"
/>
{isPending && <p className="text-sm text-gray-500">Filtering results...</p>}
<ul className="space-y-1">
{filteredItems.map((item, index) => (
<li key={index} className="p-2 bg-gray-50 rounded">{item}</li>
))}
</ul>
</div>
);
}
3. When should you use `useMemo` and `useCallback`? Are they always performant?
`useMemo` caches the result of an expensive calculation between re-renders, while `useCallback` caches a function definition between re-renders.
Important caveat: Neither hook is free. Both require memory allocation and overhead during every render to evaluate dependency array shallow comparisons. Overusing `useMemo` or `useCallback` for trivial computations or inline callbacks can actually degrade performance rather than improve it.
- Use `useMemo` when calculating derived data from large datasets that would cause noticeable main-thread freezes if re-computed on every render.
- Use `useCallback` when passing callback functions down to heavily optimized child components wrapped in `React.memo` that rely on referential equality to prevent unnecessary re-renders.
import { useState, useMemo, useCallback } from 'react';
export default function AnalyticsDashboard({ rawData }) {
const [filter, setFilter] = useState('active');
const [count, setCount] = useState(0);
// Memoize expensive calculation
const processedData = useMemo(() => {
console.log('Running expensive data processing...');
return rawData.filter(item => item.status === filter);
}, [rawData, filter]);
// Memoize callback passed to child component
const handleItemClick = useCallback((id) => {
console.log('Item clicked:', id);
}, []);
return (
<div>
<button onClick={() => setCount(count + 1)}>Re-render Parent: {count}</button>
{/* Render child components using processedData and handleItemClick */}
</div>
);
}
4. How do you design robust Custom Hooks? Provide an example of a reusable `useLocalStorage` hook.
Custom hooks are JavaScript functions whose names start with `use` and that may call other hooks. They encapsulate complex stateful logic, side effects, and subscription management, allowing clean code reuse across multiple components without duplicating boilerplate.
// hooks/useLocalStorage.js
'use client';
import { useState, useEffect } from 'react';
export function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
if (typeof window === 'undefined') return initialValue;
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});
useEffect(() => {
try {
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(storedValue));
}
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
}, [key, storedValue]);
return [storedValue, setStoredValue];
}
5. When should you choose Zustand, Redux Toolkit, or React Context over local component state?
Choosing the correct state management solution depends heavily on data frequency, scope, and architectural complexity:
- Local State (`useState`, `useReducer`): Best for UI state isolated to a single component or small subtree (e.g., form inputs, toggle modals, accordion open states).
- React Context (`useContext`): Ideal for low-frequency global settings such as user themes, authentication sessions, or localization dictionaries where updates are rare. (Avoid Context for high-frequency state like real-time stock tickers or chat feeds, as every context consumer re-renders on any value change).
- Zustand / Redux Toolkit: Perfect for complex global state requiring robust middleware, asynchronous thunks, DevTools inspection, selector memoization, and selective component subscriptions without unnecessary re-renders.
// store/useAuthStore.js (Zustand Example)
import { create } from 'zustand';
export const useAuthStore = create((set) => ({
user: null,
isAuthenticated: false,
login: (userData) => set({ user: userData, isAuthenticated: true }),
logout: () => set({ user: null, isAuthenticated: false }),
}));
6. How do Error Boundaries work in React, and what can they NOT catch?
An **Error Boundary** is a React component that catches JavaScript errors anywhere in its child component tree, logs those errors, and displays a fallback UI instead of crashing the entire component tree.
What Error Boundaries CANNOT catch:
- Event handlers (e.g., an `onClick` callback error requires standard `try/catch` blocks).
- Asynchronous code (e.g., `setTimeout` or `async/await` promise rejections).
- Server-side rendering errors.
- Errors thrown in the error boundary component itself (rather than its children).
// components/ErrorBoundary.jsx
'use client';
import React, { Component } from 'react';
export default class ErrorBoundary extends Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error("ErrorBoundary caught an error:", error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="p-6 bg-red-50 border border-red-200 rounded text-center">
<h2 className="text-xl font-bold text-red-700">Something went wrong.</h2>
<p className="text-sm text-gray-600 mt-2">{this.state.error?.message}</p>
<button
onClick={() => this.setState({ hasError: false })}
className="mt-4 bg-red-600 text-white px-4 py-2 rounded"
>
Try Again
</button>
</div>
);
}
return this.props.children;
}
}
Conclusion
Mastering modern React interview concepts in 2026 requires more than memorizing hook syntaxes. Understanding React Server Components, concurrent rendering pipelines, memoization trade-offs, and robust state architectures will set you apart and ensure you can engineer high-performance, scalable web applications.