React interviews in 2026 are different from what they were two years ago. Server Components changed the mental model. React 19 introduced new primitives. The ecosystem shifted toward simpler state management. If you're still preparing with 2023-era questions, you're going to have a rough time.
This guide covers 50 questions that actually come up in interviews - with real answers, code examples, and the context you need to sound like you actually use React, not like you memorized a blog post.
Let's get into it.
Core Concepts (Questions 1-10)
1. What is the Virtual DOM, and why does React use it?
The real interview answer: The Virtual DOM is a lightweight JavaScript representation of the actual DOM. When state changes, React builds a new virtual DOM tree, diffs it against the previous one (this is called "reconciliation"), and only applies the minimal set of changes to the real DOM.
Why? Because direct DOM manipulation is slow. Batching changes and applying only the diff is way faster, especially for complex UIs.
// You write this declaratively:
function Counter({ count }) {
return <div className="counter">{count}</div>;
}
// React figures out what actually changed in the DOM
// and only updates the text node, not the entire div
Worth noting: React's approach isn't always faster than hand-optimized vanilla JS. The win is that it's fast enough while letting you write declarative code instead of manual DOM manipulation.
2. What is JSX, and how does it work under the hood?
The real interview answer: JSX is syntactic sugar for creating React elements. It looks like HTML but it's JavaScript. Babel (or your build tool) transforms JSX into function calls.
// What you write:
<Button color="blue" onClick={handleClick}>
Submit
</Button>
// What it compiles to (React 17+ automatic runtime):
import { jsx as _jsx } from 'react/jsx-runtime';
_jsx(Button, {
color: "blue",
onClick: handleClick,
children: "Submit"
});
The key thing interviewers want to hear: JSX isn't a template language. It's just JavaScript expressions. That's why you can use map(), ternaries, and variables directly inside it.
3. Explain the React component lifecycle (in a hooks world).
The real interview answer: Class components had explicit lifecycle methods - componentDidMount, componentDidUpdate, componentWillUnmount. With hooks, the mental model is different. You think in terms of synchronization, not lifecycles.
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
// Runs after render (similar to componentDidMount + componentDidUpdate)
const controller = new AbortController();
fetchUser(userId, { signal: controller.signal })
.then(setUser);
// Cleanup function (similar to componentWillUnmount)
return () => controller.abort();
}, [userId]); // Only re-run when userId changes
if (!user) return <Loading />;
return <div>{user.name}</div>;
}
The shift is from "what happens at each phase" to "what external systems do I need to sync with." That's why Dan Abramov always said useEffect isn't a lifecycle hook - it's a synchronization mechanism.
4. What's the difference between controlled and uncontrolled components?
The real interview answer: A controlled component has its value managed by React state. An uncontrolled component manages its own internal state through the DOM.
// Controlled - React owns the value
function ControlledInput() {
const [value, setValue] = useState('');
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}
// Uncontrolled - the DOM owns the value
function UncontrolledInput() {
const inputRef = useRef(null);
const handleSubmit = () => {
console.log(inputRef.current.value);
};
return <input ref={inputRef} defaultValue="" />;
}
When to use which? Controlled for most cases - you get instant validation, conditional disabling, enforced input formats. Uncontrolled for simple forms where you only need the value on submit, or when integrating with non-React code.
5. Why do we need keys in lists, and what happens if you use index as a key?
The real interview answer: Keys help React identify which items in a list changed, were added, or were removed during reconciliation. Without stable keys, React can't efficiently reorder elements - it has to destroy and recreate them.
// Bad - index as key causes bugs with stateful components
{items.map((item, index) => (
<TodoItem key={index} item={item} />
))}
// Good - stable unique identifier
{items.map((item) => (
<TodoItem key={item.id} item={item} />
))}
Using index as a key is fine for static lists that never reorder. But if items can be added, removed, or reordered, index keys cause subtle bugs: input values stick to the wrong items, animations break, and component state gets mixed up between items.
6. What are refs, and when should you use them?
The real interview answer: Refs give you a way to hold a mutable value that persists across renders without triggering a re-render when it changes. The most common use case is accessing DOM elements directly.
function VideoPlayer() {
const videoRef = useRef(null);
const handlePlay = () => {
videoRef.current.play(); // Direct DOM access
};
return (
<div>
<video ref={videoRef} src="/video.mp4" />
<button onClick={handlePlay}>Play</button>
</div>
);
}
// Also useful for storing mutable values without re-renders
function useInterval(callback, delay) {
const savedCallback = useRef(callback);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
const tick = () => savedCallback.current();
const id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay]);
}
Use refs for: DOM access, storing previous values, holding timers/subscriptions, and any mutable value that shouldn't trigger a re-render.
7. What are React Fragments, and why do they exist?
The real interview answer: Fragments let you group multiple elements without adding an extra DOM node. Before Fragments, you'd wrap everything in a <div>, which messed up CSS layouts (especially flexbox and grid) and produced invalid HTML in some cases (like <tr> needing to be a direct child of <tbody>).
// Without fragments - extra div in the DOM
function TableRow() {
return (
<div> {/* This breaks table layout! */}
<td>Name</td>
<td>Age</td>
</div>
);
}
// With fragments - no extra DOM node
function TableRow() {
return (
<>
<td>Name</td>
<td>Age</td>
</>
);
}
// Long syntax when you need a key
{items.map(item => (
<Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.description}</dd>
</Fragment>
))}
8. What are Portals, and when would you use them?
The real interview answer: Portals render children into a different DOM node than their parent component, while keeping them in the same React tree. Events still bubble up through the React tree, not the DOM tree.
import { createPortal } from 'react-dom';
function Modal({ children, isOpen }) {
if (!isOpen) return null;
return createPortal(
<div className="modal-overlay">
<div className="modal-content">
{children}
</div>
</div>,
document.getElementById('modal-root')
);
}
Use portals for: modals, tooltips, dropdowns, and toasts - anything that needs to visually "break out" of its parent's overflow or z-index context but still behave as a child in React's component tree.
9. What are Error Boundaries, and how do they work?
The real interview answer: Error Boundaries are components that catch JavaScript errors in their child component tree during rendering, lifecycle methods, and constructors. They let you show a fallback UI instead of crashing the whole app.
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// Log to your error tracking service
logErrorToService(error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="error-fallback">
<h2>Something went wrong</h2>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
);
}
return this.props.children;
}
}
// Usage
<ErrorBoundary>
<UserProfile userId={userId} />
</ErrorBoundary>
Important caveat: Error Boundaries don't catch errors in event handlers, async code, or server-side rendering. For event handlers, you still need regular try/catch. They're class components because there's no hook equivalent for getDerivedStateFromError yet.
10. What does Strict Mode do?
The real interview answer: StrictMode is a development-only tool that helps you find bugs early. It intentionally double-invokes certain functions to expose side effects you didn't mean to create.
import { StrictMode } from 'react';
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>
);
What it actually does:
- Double-renders components to find impure rendering
- Double-runs effects (mount, unmount, mount) to verify cleanup works
- Warns about deprecated APIs like string refs and legacy context
- Checks for unsafe lifecycle methods in class components
The double-invocation trips people up. If your useEffect fires twice in development, that's Strict Mode verifying your cleanup function works. Your code should handle being mounted, unmounted, and remounted gracefully.
Hooks Deep Dive (Questions 11-20)
11. How does useState work internally?
The real interview answer: useState returns a stateful value and a setter function. React stores state in a linked list associated with the component's fiber node. Each call to useState during render corresponds to a specific position in that list - which is why you can't call hooks conditionally.
function Form() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [submitted, setSubmitted] = useState(false);
// Functional updates when new state depends on previous state
const [count, setCount] = useState(0);
const increment = () => setCount(prev => prev + 1);
// Lazy initialization for expensive computations
const [data, setData] = useState(() => {
return expensiveComputation();
});
}
Key detail: setState doesn't immediately update the value. React batches state updates and re-renders. If you call setCount(count + 1) three times in a row, you get one increment. Use the functional form setCount(prev => prev + 1) when the next state depends on the previous one.
12. Explain useEffect - dependencies, cleanup, and common pitfalls.
The real interview answer: useEffect synchronizes your component with an external system. It runs after the browser paints, not during render.
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
// Setup: connect to external system
const connection = createConnection(roomId);
connection.on('message', (msg) => {
setMessages(prev => [...prev, msg]);
});
// Cleanup: disconnect when roomId changes or component unmounts
return () => {
connection.disconnect();
};
}, [roomId]); // Only re-run when roomId changes
return <MessageList messages={messages} />;
}
Common pitfalls:
- Missing dependencies: The linter warns you for a reason. Suppressing it causes stale closures.
- Object/array dependencies:
{a: 1}!=={a: 1}in JavaScript. Destructure to primitives or useuseMemo. - No cleanup: Forgetting to abort fetch requests, clear timers, or unsubscribe causes memory leaks.
- Using useEffect for derived state: If you can compute it during render, do that instead. Don't use an effect to "sync" state with props.
// Bad - unnecessary effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// Good - compute during render
const fullName = firstName + ' ' + lastName;
13. When would you use useContext?
The real interview answer: useContext reads a value from a React Context. It's how you share data across the component tree without prop drilling.
const ThemeContext = createContext('light');
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const value = useMemo(() => ({
theme,
toggleTheme: () => setTheme(t => t === 'light' ? 'dark' : 'light')
}), [theme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
function ThemedButton() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button
className={theme === 'dark' ? 'btn-dark' : 'btn-light'}
onClick={toggleTheme}
>
Toggle Theme
</button>
);
}
Gotcha: every component that calls useContext(ThemeContext) re-renders when the context value changes. That's why you memoize the value object and keep your contexts focused. Don't stuff everything into one giant context.
14. How does useReducer compare to useState?
The real interview answer: useReducer is useState for complex state logic. Instead of directly setting state, you dispatch actions that a reducer function processes. Same pattern as Redux, but local to the component.
function todoReducer(state, action) {
switch (action.type) {
case 'ADD':
return [...state, { id: Date.now(), text: action.text, done: false }];
case 'TOGGLE':
return state.map(todo =>
todo.id === action.id ? { ...todo, done: !todo.done } : todo
);
case 'DELETE':
return state.filter(todo => todo.id !== action.id);
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
function TodoList() {
const [todos, dispatch] = useReducer(todoReducer, []);
return (
<div>
<AddTodo onAdd={(text) => dispatch({ type: 'ADD', text })} />
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={() => dispatch({ type: 'TOGGLE', id: todo.id })}
onDelete={() => dispatch({ type: 'DELETE', id: todo.id })}
/>
))}
</div>
);
}
Use useReducer when: state transitions depend on multiple values, you have complex update logic, or you want to pass dispatch down instead of multiple callback functions.
15. When should you use useMemo?
The real interview answer: useMemo caches the result of an expensive computation between renders. It only recalculates when its dependencies change.
function ProductList({ products, filter }) {
// Only re-filter when products or filter changes
const filteredProducts = useMemo(() => {
return products.filter(p =>
p.name.toLowerCase().includes(filter.toLowerCase())
);
}, [products, filter]);
// Only re-sort when filteredProducts changes
const sortedProducts = useMemo(() => {
return [...filteredProducts].sort((a, b) => b.rating - a.rating);
}, [filteredProducts]);
return <ProductGrid products={sortedProducts} />;
}
When to use it:
- Filtering/sorting large arrays
- Complex calculations
- Maintaining referential equality for objects passed as props or effect dependencies
When NOT to use it:
- Simple computations (the overhead of
useMemoitself can be worse) - Primitives (strings, numbers - they're compared by value anyway)
- Premature optimization - profile first, memoize second
16. What's the difference between useMemo and useCallback?
The real interview answer: They do the same thing. useCallback(fn, deps) is literally useMemo(() => fn, deps). useMemo caches a computed value, useCallback caches a function reference.
function ParentComponent({ items }) {
// useMemo caches the computed result
const total = useMemo(() => {
return items.reduce((sum, item) => sum + item.price, 0);
}, [items]);
// useCallback caches the function itself
const handleClick = useCallback((id) => {
console.log(`Clicked item ${id}`);
}, []); // No dependencies - same function every render
return (
<div>
<p>Total: ${total}</p>
{items.map(item => (
<MemoizedItem key={item.id} item={item} onClick={handleClick} />
))}
</div>
);
}
useCallback is only useful when you pass the function to a memoized child component (React.memo). Otherwise, caching the function reference does nothing because the parent still re-renders and re-renders the child anyway.
17. What is useRef, and how is it different from useState?
The real interview answer: Both persist values across renders. The difference: updating a ref does NOT trigger a re-render.
function StopWatch() {
const [time, setTime] = useState(0);
const intervalRef = useRef(null); // Mutable, no re-render
const start = () => {
intervalRef.current = setInterval(() => {
setTime(t => t + 1); // State update - triggers re-render
}, 1000);
};
const stop = () => {
clearInterval(intervalRef.current); // Read ref - no re-render
};
return (
<div>
<p>{time}s</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}
Think of useRef as a class instance variable. It's a box that holds a .current property. React doesn't care when it changes. Use it for timers, DOM references, previous values, and any mutable data that doesn't affect rendering.
18. How do you build a custom hook?
The real interview answer: A custom hook is just a function that uses other hooks. Name it use*Something*. It lets you extract and reuse stateful logic between components.
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const stored = localStorage.getItem(key);
return stored !== null ? JSON.parse(stored) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch {
console.warn(`Failed to save ${key} to localStorage`);
}
}, [key, value]);
return [value, setValue];
}
// Usage - same API as useState, but persisted
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [fontSize, setFontSize] = useLocalStorage('fontSize', 16);
// ...
}
Custom hooks are the primary code reuse mechanism in modern React. If you find yourself copying the same useState/useEffect combo between components, extract it into a hook.
19. What are the Rules of Hooks, and why do they exist?
The real interview answer: Two rules:
- Only call hooks at the top level. No hooks inside loops, conditions, or nested functions.
- Only call hooks from React functions. Either function components or custom hooks.
// WRONG - conditional hook call
function Profile({ userId }) {
if (userId) {
const [user, setUser] = useState(null); // Breaks!
}
}
// RIGHT - always call hooks, handle condition in the effect
function Profile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
if (!userId) return; // Guard inside the effect
fetchUser(userId).then(setUser);
}, [userId]);
}
Why? React tracks hooks by call order. If a hook is called conditionally, the order can change between renders, and React loses track of which hook is which. It's a fundamental constraint of the hooks design - they use an array internally, not named keys.
20. What is useId, and why was it introduced?
The real interview answer: useId generates a unique ID that's stable across server and client rendering. It solves hydration mismatches that happened when you tried to generate IDs yourself.
function FormField({ label }) {
const id = useId();
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} type="text" />
</div>
);
}
// Generates something like ":r1:" on both server and client
// Multiple IDs from one useId call:
function PasswordField() {
const id = useId();
return (
<>
<label htmlFor={`${id}-password`}>Password</label>
<input id={`${id}-password`} type="password" />
<label htmlFor={`${id}-confirm`}>Confirm</label>
<input id={`${id}-confirm`} type="password" />
</>
);
}
Before useId, people used Math.random() or counters, which produced different IDs on server and client. That broke hydration and caused accessibility tools to fail silently.
State Management (Questions 21-26)
21. What is prop drilling, and how do you solve it?
The real interview answer: Prop drilling is passing data through multiple layers of components that don't use the data themselves - they just pass it down. It's not inherently bad for 2-3 levels, but it becomes painful when you're passing props through 5+ components.
// Prop drilling - Header doesn't use 'user' but has to accept it
function App() {
const [user, setUser] = useState(null);
return <Layout user={user} />;
}
function Layout({ user }) {
return <Header user={user} />;
}
function Header({ user }) {
return <UserMenu user={user} />;
}
// Solution 1: Context
const UserContext = createContext(null);
function App() {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={user}>
<Layout />
</UserContext.Provider>
);
}
function UserMenu() {
const user = useContext(UserContext); // Skip the middlemen
}
// Solution 2: Component composition
function App() {
const [user, setUser] = useState(null);
return (
<Layout>
<Header>
<UserMenu user={user} /> {/* Pass directly to consumer */}
</Header>
</Layout>
);
}
Composition is underrated. Before reaching for Context, ask if you can restructure your components so the data goes directly to where it's needed.
22. When should you use Context API vs a state management library?
The real interview answer: Context is great for low-frequency updates that many components need - theme, locale, auth status, feature flags. It's not great for high-frequency updates because every consumer re-renders when the value changes.
Use Context when:
- Data changes infrequently (theme, auth, locale)
- You have a clear provider/consumer hierarchy
- The alternative is just 3-4 levels of prop drilling
Use a state management library when:
- Many components update the same state frequently
- You need computed/derived state
- You want fine-grained subscriptions (only re-render when your slice changes)
- State logic is complex enough to benefit from devtools
23. How does Redux work, and is it still relevant in 2026?
The real interview answer: Redux uses a single store with a predictable state flow: dispatch an action, a reducer produces new state, connected components re-render. Redux Toolkit (RTK) is the modern way to write Redux - it eliminates the boilerplate.
// Redux Toolkit slice
import { createSlice } from '@reduxjs/toolkit';
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [], total: 0 },
reducers: {
addItem(state, action) {
// RTK uses Immer - you can "mutate" directly
state.items.push(action.payload);
state.total += action.payload.price;
},
removeItem(state, action) {
const index = state.items.findIndex(i => i.id === action.payload);
if (index !== -1) {
state.total -= state.items[index].price;
state.items.splice(index, 1);
}
},
},
});
Is it still relevant? Yes, but it's no longer the default choice. Redux is best for large apps with complex, interconnected state. For most apps, lighter alternatives like Zustand work fine. Don't add Redux just because it's Redux.
24. What are Zustand and Jotai, and when would you pick them over Redux?
The real interview answer: Zustand is a minimal state manager with a simple API - create a store, use it in components. No providers, no boilerplate. Jotai takes an atomic approach - each piece of state is an independent atom.
// Zustand - dead simple
import { create } from 'zustand';
const useCartStore = create((set) => ({
items: [],
addItem: (item) => set((state) => ({
items: [...state.items, item]
})),
removeItem: (id) => set((state) => ({
items: state.items.filter(i => i.id !== id)
})),
get total() {
return this.items.reduce((sum, i) => sum + i.price, 0);
}
}));
// Usage - no provider needed
function CartCount() {
const count = useCartStore((state) => state.items.length);
return <span>{count}</span>;
}
// Jotai - atomic state
import { atom, useAtom } from 'jotai';
const countAtom = atom(0);
const doubledAtom = atom((get) => get(countAtom) * 2); // Derived
function Counter() {
const [count, setCount] = useAtom(countAtom);
const [doubled] = useAtom(doubledAtom);
return <div>{count} (doubled: {doubled})</div>;
}
Pick Zustand when you want something simple that "just works." Pick Jotai when you have lots of independent, composable pieces of state. Pick Redux when you need the middleware ecosystem, devtools, and your team already knows it.
25. What is "server state," and how does TanStack Query handle it?
The real interview answer: Server state is data that lives on the server and your app has a cached copy of. It's fundamentally different from client state (UI state like modals, form inputs) because it's shared, async, and can become stale.
TanStack Query (formerly React Query) treats server state as a cache problem, not a state problem.
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function UserProfile({ userId }) {
const { data: user, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000, // Cache is fresh for 5 minutes
});
const queryClient = useQueryClient();
const updateUser = useMutation({
mutationFn: (updates) => patchUser(userId, updates),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['user', userId] });
},
});
if (isLoading) return <Skeleton />;
if (error) return <Error message={error.message} />;
return (
<div>
<h1>{user.name}</h1>
<button onClick={() => updateUser.mutate({ name: 'New Name' })}>
Update
</button>
</div>
);
}
TanStack Query gives you caching, background refetching, stale-while-revalidate, pagination, infinite scroll, and optimistic updates out of the box. It eliminated 80% of the Redux code in most apps because that Redux code was just caching server responses.
26. How do you decide what state management approach to use?
The real interview answer: Use the simplest thing that works.
- Local state (useState/useReducer): Start here. Most state is local.
- Lift state up: When siblings need the same state, lift it to their parent.
- Composition: Restructure components to avoid passing through intermediaries.
- Context: For truly global, low-frequency data (auth, theme).
- TanStack Query: For all server/async data. This isn't optional anymore - it's the standard.
- Zustand/Jotai/Redux: Only for complex client state that multiple unrelated components need.
The mistake most people make is reaching for a global state library immediately. 90% of state in a well-structured React app is local or server state.
Performance (Questions 27-32)
27. How does React.memo work, and when should you use it?
The real interview answer: React.memo is a higher-order component that skips re-rendering a component if its props haven't changed (shallow comparison).
const ExpensiveList = React.memo(function ExpensiveList({ items, onSelect }) {
console.log('Rendering ExpensiveList');
return (
<ul>
{items.map(item => (
<li key={item.id} onClick={() => onSelect(item.id)}>
{item.name}
</li>
))}
</ul>
);
});
// Custom comparison function
const ChartComponent = React.memo(
function ChartComponent({ data, config }) {
return <canvas>{/* expensive chart rendering */}</canvas>;
},
(prevProps, nextProps) => {
// Return true to skip re-render
return (
prevProps.data.length === nextProps.data.length &&
prevProps.config.type === nextProps.config.type
);
}
);
React.memo only works if the props actually stay the same. If the parent creates new objects or functions on every render, memo does nothing. That's where useMemo and useCallback come in.
28. Explain the relationship between React.memo, useMemo, and useCallback.
The real interview answer: They're three tools that work together to prevent unnecessary work:
- React.memo: Skips re-rendering a component if props are the same
- useMemo: Skips recalculating a value if dependencies are the same
- useCallback: Skips recreating a function if dependencies are the same
function Parent({ data }) {
// useMemo: avoid re-sorting on every render
const sortedData = useMemo(() =>
[...data].sort((a, b) => a.name.localeCompare(b.name)),
[data]
);
// useCallback: keep the same function reference
const handleItemClick = useCallback((id) => {
console.log('clicked', id);
}, []);
// React.memo on Child: skip re-render if sortedData and
// handleItemClick haven't changed
return <MemoizedChild items={sortedData} onClick={handleItemClick} />;
}
const MemoizedChild = React.memo(function Child({ items, onClick }) {
return items.map(item => (
<div key={item.id} onClick={() => onClick(item.id)}>
{item.name}
</div>
));
});
The key insight: React.memo without useCallback/useMemo is often useless, because new objects/functions get created every render. And useCallback/useMemo without React.memo is also often useless, because the child renders anyway.
29. What is code splitting, and how do you do it in React?
The real interview answer: Code splitting breaks your bundle into smaller chunks that load on demand. In React, you use React.lazy and Suspense.
import { lazy, Suspense } from 'react';
// This component's code is in a separate chunk
const AdminDashboard = lazy(() => import('./AdminDashboard'));
const UserSettings = lazy(() => import('./UserSettings'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/admin" element={<AdminDashboard />} />
<Route path="/settings" element={<UserSettings />} />
<Route path="/" element={<Home />} />
</Routes>
</Suspense>
);
}
Best practices:
- Split at route boundaries first - that's the biggest win
- Split heavy components (charts, editors, maps) that not all users need
- Don't split tiny components - the network overhead isn't worth it
- Use route prefetching to load chunks before the user navigates
30. What is lazy loading, and how does React.lazy work?
The real interview answer: React.lazy lets you define a component that loads lazily - its code is only fetched when the component is first rendered. It works with dynamic import() and must be wrapped in Suspense.
// Named export pattern
const Chart = lazy(() =>
import('./Chart').then(module => ({ default: module.Chart }))
);
// Preloading strategy
const AdminPage = lazy(() => import('./AdminPage'));
function NavLink() {
const preload = () => {
// Start loading when user hovers the link
import('./AdminPage');
};
return (
<Link to="/admin" onMouseEnter={preload}>
Admin
</Link>
);
}
The preloading pattern is important for interviews. Just lazy loading means the user sees a spinner the first time they visit a page. Preloading on hover (or on viewport intersection) makes the experience feel instant.
31. What is list virtualization, and when do you need it?
The real interview answer: Virtualization means only rendering the items currently visible in the viewport, even if your list has thousands of items. Instead of 10,000 DOM nodes, you have maybe 30.
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50, // estimated row height in px
});
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
{virtualizer.getVirtualItems().map((virtualRow) => (
<div
key={virtualRow.key}
style={{
position: 'absolute',
top: 0,
transform: `translateY(${virtualRow.start}px)`,
height: `${virtualRow.size}px`,
}}
>
{items[virtualRow.index].name}
</div>
))}
</div>
</div>
);
}
Use virtualization when you have 500+ items in a scrollable list. For smaller lists, the added complexity isn't worth it. TanStack Virtual and react-window are the go-to libraries.
32. How do you use the React Profiler to find performance issues?
The real interview answer: The React Profiler (in React DevTools) records renders and shows you which components rendered, why, and how long they took.
// Programmatic Profiler component
import { Profiler } from 'react';
function onRender(id, phase, actualDuration, baseDuration, startTime, commitTime) {
// Log to your analytics
if (actualDuration > 16) { // Longer than one frame
console.warn(`Slow render: ${id} took ${actualDuration}ms`);
}
}
<Profiler id="ProductList" onRender={onRender}>
<ProductList products={products} />
</Profiler>
Practical workflow:
- Open React DevTools Profiler tab
- Click record, perform the slow interaction, stop recording
- Look at the flame chart - tall bars are slow components
- Check "Why did this render?" - often it's unnecessary re-renders from new object/function references
- Fix the top offenders with
React.memo,useMemo, or restructuring
Don't optimize blindly. Profile first, identify the actual bottleneck, then fix it.
React 19 & Server Components (Questions 33-40)
33. What are Server Components, and how are they different from Client Components?
The real interview answer: Server Components render on the server and send HTML (plus a special RSC payload) to the client. They never run in the browser. Client Components are the traditional React components that run in the browser (and can also be server-rendered for initial HTML).
// Server Component (default in Next.js App Router)
// Can access databases, file system, secrets directly
async function ProductPage({ params }) {
const product = await db.query('SELECT * FROM products WHERE id = $1', [params.id]);
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<AddToCartButton productId={product.id} /> {/* Client Component */}
</div>
);
}
// Client Component - needs "use client" directive
'use client';
import { useState } from 'react';
function AddToCartButton({ productId }) {
const [added, setAdded] = useState(false);
return (
<button onClick={() => {
addToCart(productId);
setAdded(true);
}}>
{added ? 'Added!' : 'Add to Cart'}
</button>
);
}
Key differences:
- Server Components have zero JavaScript bundle impact
- Server Components can directly access backend resources
- Server Components cannot use hooks, state, or event handlers
- Client Components can do everything traditional React components can
The mental model: Server Components handle data fetching and static rendering. Client Components handle interactivity. Push 'use client' as far down the tree as possible.
34. What is the use() hook in React 19?
The real interview answer: use() is a new hook that lets you read the value of a Promise or Context. Unlike other hooks, it can be called conditionally. When used with a Promise, it integrates with Suspense to handle loading states.
import { use, Suspense } from 'react';
// Reading a promise - integrates with Suspense
function UserProfile({ userPromise }) {
const user = use(userPromise); // Suspends until resolved
return <h1>{user.name}</h1>;
}
function App() {
const userPromise = fetchUser(123); // Start fetching early
return (
<Suspense fallback={<Skeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
// Conditional context reading
function StatusMessage({ showDetails }) {
if (showDetails) {
const theme = use(ThemeContext); // Can be called conditionally!
return <p style={{ color: theme.primary }}>Details here</p>;
}
return <p>No details</p>;
}
The big deal is that use() breaks the "hooks can't be conditional" rule. It's not technically a hook in the traditional sense - it's a new primitive that the React team designed specifically to be more flexible.
35. What are Actions in React 19?
The real interview answer: Actions are async functions that handle form submissions and data mutations. They integrate with React's transition system to provide pending states, error handling, and optimistic updates automatically.
'use client';
import { useActionState } from 'react';
async function submitForm(previousState, formData) {
const name = formData.get('name');
const email = formData.get('email');
try {
await createUser({ name, email });
return { success: true, message: 'User created!' };
} catch (error) {
return { success: false, message: error.message };
}
}
function SignupForm() {
const [state, formAction, isPending] = useActionState(submitForm, null);
return (
<form action={formAction}>
<input name="name" required />
<input name="email" type="email" required />
<button disabled={isPending}>
{isPending ? 'Submitting...' : 'Sign Up'}
</button>
{state?.message && (
<p className={state.success ? 'text-green' : 'text-red'}>
{state.message}
</p>
)}
</form>
);
}
Actions work with the <form action={}> pattern. The form submits even without JavaScript (progressive enhancement), and React enhances it when JS is available. This is a big deal for Server Components where forms are the primary mutation mechanism.
36. What does useFormStatus do?
The real interview answer: useFormStatus gives you the pending status of the parent form's action. It must be used inside a component that's rendered within a <form>. It lets you build reusable submit buttons that know when the form is submitting.
'use client';
import { useFormStatus } from 'react-dom';
function SubmitButton({ children }) {
const { pending, data, method, action } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? (
<span className="flex items-center gap-2">
<Spinner size="sm" />
Submitting...
</span>
) : (
children
)}
</button>
);
}
// Usage in any form - it's completely reusable
function ContactForm() {
return (
<form action={submitContactForm}>
<input name="message" />
<SubmitButton>Send Message</SubmitButton>
</form>
);
}
The key detail: useFormStatus reads the status of the parent form. It doesn't work if you use it in the same component as the form - it needs to be in a child component. This trips people up in interviews.
37. How does useOptimistic work?
The real interview answer: useOptimistic lets you show the expected result immediately while the async operation completes in the background. If the operation fails, React automatically reverts to the previous state.
'use client';
import { useOptimistic } from 'react';
function MessageList({ messages, sendMessage }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(currentMessages, newMessage) => [
...currentMessages,
{ ...newMessage, sending: true }
]
);
async function handleSend(formData) {
const text = formData.get('message');
const newMessage = { id: crypto.randomUUID(), text, sending: true };
addOptimisticMessage(newMessage); // Instantly show the message
await sendMessage(text); // Actually send it (reverts on failure)
}
return (
<div>
{optimisticMessages.map(msg => (
<div key={msg.id} style={{ opacity: msg.sending ? 0.7 : 1 }}>
{msg.text}
</div>
))}
<form action={handleSend}>
<input name="message" />
<button type="submit">Send</button>
</form>
</div>
);
}
This is the pattern every chat app, social media feed, and todo app uses. Before useOptimistic, you had to manually manage optimistic state with useState and rollback logic. Now React handles the rollback for you.
38. How does Suspense work, and what can trigger it?
The real interview answer: Suspense lets you declaratively specify loading states. When a child component "suspends" (meaning it's not ready to render yet), React shows the nearest Suspense boundary's fallback.
import { Suspense } from 'react';
function App() {
return (
<div>
<h1>My App</h1>
{/* Nested Suspense boundaries for granular loading states */}
<Suspense fallback={<NavSkeleton />}>
<Navigation />
</Suspense>
<div className="content">
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
<Suspense fallback={<MainSkeleton />}>
<MainContent />
</Suspense>
</div>
</div>
);
}
What can trigger Suspense:
React.lazy()- code splittinguse()with a Promise - data fetching in React 19- Server Components that are streaming
- Any library that implements the Suspense protocol (TanStack Query, Relay)
What CANNOT trigger Suspense:
useEffectwithfetch- this is the old pattern and doesn't integrate with Suspense- Regular promises without the
use()hook
39. What is streaming SSR, and why does it matter?
The real interview answer: Traditional SSR generates the complete HTML page on the server before sending anything. Streaming SSR sends HTML in chunks as each part of the page becomes ready.
// In Next.js App Router, this happens automatically
// with Suspense boundaries
async function Page() {
return (
<div>
{/* This renders and streams immediately */}
<Header />
{/* This streams its skeleton first, then the real content */}
<Suspense fallback={<ProductSkeleton />}>
<ProductRecommendations /> {/* Slow API call */}
</Suspense>
{/* This also streams immediately */}
<Footer />
</div>
);
}
Why it matters:
- Time to First Byte (TTFB): User sees content faster because you don't wait for the slowest data fetch
- Progressive rendering: The page fills in piece by piece instead of all-or-nothing
- No waterfalls: Multiple Suspense boundaries stream in parallel
The user sees Header and Footer immediately. ProductRecommendations shows a skeleton that gets replaced when the data arrives. The slow API call doesn't block the rest of the page.
40. What is the RSC payload, and how does server-client communication work?
The real interview answer: When the server renders Server Components, it doesn't send HTML alone. It sends a special format called the RSC payload (also called the RSC wire format or "flight data"). This payload describes the component tree in a way that React on the client can reconstruct and merge with Client Components.
// Simplified RSC payload (actual format is more compressed)
0:["$","div",null,{"children":[
["$","h1",null,{"children":"Product Name"}],
["$","$Lclient-component",null,{"productId":123}]
]}]
The flow:
- Server renders Server Components, producing the RSC payload
- RSC payload streams to the client
- React on the client reconstructs the tree
- Client Component placeholders in the RSC payload get hydrated with actual Client Components
- Client Components become interactive
This is why Server Components can pass serializable props to Client Components (strings, numbers, plain objects) but NOT functions, classes, or other non-serializable values. Everything crosses a serialization boundary.
Routing & Data Fetching (Questions 41-44)
41. How does React Router compare to Next.js App Router?
The real interview answer: React Router is a client-side routing library - all routing happens in the browser. Next.js App Router is a framework-level router that supports server-side rendering, Server Components, and file-based routing.
// React Router (v6+)
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{ path: 'products', element: <Products />, loader: loadProducts },
{ path: 'products/:id', element: <ProductDetail />, loader: loadProduct },
],
},
]);
function App() {
return <RouterProvider router={router} />;
}
// Next.js App Router (file-based)
// app/products/page.tsx
export default async function ProductsPage() {
const products = await db.products.findMany(); // Server Component
return <ProductList products={products} />;
}
// app/products/[id]/page.tsx
export default async function ProductPage({ params }) {
const product = await db.products.findUnique({ where: { id: params.id } });
return <ProductDetail product={product} />;
}
React Router 7 (formerly Remix) has been closing the gap with server-side features, loaders, and actions. The choice often comes down to: do you want a full framework (Next.js) or a library you add to your own setup (React Router)?
42. What are the modern data fetching patterns in React?
The real interview answer: The evolution has been: useEffect fetch (bad) -> TanStack Query (good for client) -> Server Components (best when available).
// Pattern 1: useEffect (avoid for most cases)
function Products() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/products')
.then(r => r.json())
.then(data => { setProducts(data); setLoading(false); });
}, []);
// Problems: loading states, error handling, race conditions, no caching
}
// Pattern 2: TanStack Query (great for client-heavy apps)
function Products() {
const { data: products, isLoading } = useQuery({
queryKey: ['products'],
queryFn: () => fetch('/api/products').then(r => r.json()),
});
// Handles caching, refetching, loading, errors automatically
}
// Pattern 3: Server Components (best DX when framework supports it)
async function Products() {
const products = await db.products.findMany();
return <ProductList products={products} />;
// No loading state needed - streams with Suspense
// No client-side JS for this component
// Direct database access
}
In 2026, if you're using Next.js (or a similar RSC-supporting framework), Server Components are the default for initial data fetching. TanStack Query is still essential for client-side mutations, real-time updates, and interactive data.
43. How does Suspense work for data fetching?
The real interview answer: Suspense for data fetching lets you "suspend" rendering while waiting for async data. Instead of managing isLoading states everywhere, you declare loading boundaries.
import { Suspense, use } from 'react';
// The data fetching starts before rendering
function ProductPage({ productId }) {
const productPromise = fetchProduct(productId);
return (
<div>
<Suspense fallback={<ProductSkeleton />}>
<ProductDetails productPromise={productPromise} />
</Suspense>
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews productId={productId} />
</Suspense>
</div>
);
}
function ProductDetails({ productPromise }) {
const product = use(productPromise);
return (
<div>
<h1>{product.name}</h1>
<p>${product.price}</p>
</div>
);
}
The important nuance: create the Promise outside the suspending component. If you create it inside, you get an infinite loop (component suspends, remounts, creates new Promise, suspends again). The Promise should be created by the parent or by a framework-level mechanism.
44. How do you use Error Boundaries for data fetching errors?
The real interview answer: Error Boundaries catch rendering errors, including errors thrown by Suspense when a Promise rejects. Combining Suspense and Error Boundaries gives you declarative loading and error states.
import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
function Dashboard() {
return (
<div className="grid grid-cols-2 gap-4">
<ErrorBoundary fallback={<ErrorCard message="Failed to load revenue" />}>
<Suspense fallback={<CardSkeleton />}>
<RevenueCard />
</Suspense>
</ErrorBoundary>
<ErrorBoundary fallback={<ErrorCard message="Failed to load users" />}>
<Suspense fallback={<CardSkeleton />}>
<UsersCard />
</Suspense>
</ErrorBoundary>
</div>
);
}
// With react-error-boundary (the library), you get reset functionality
function DataSection() {
return (
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) => (
<div>
<p>Error: {error.message}</p>
<button onClick={resetErrorBoundary}>Retry</button>
</div>
)}
onReset={() => {
// Optionally clear cache or refetch
queryClient.invalidateQueries();
}}
>
<Suspense fallback={<Loading />}>
<DataContent />
</Suspense>
</ErrorBoundary>
);
}
Pattern: ErrorBoundary wraps Suspense wraps your data component. Error Boundary catches failures, Suspense handles loading, and your component just handles the happy path. Clean separation.
Testing (Questions 45-48)
45. How do you test React components with React Testing Library?
The real interview answer: React Testing Library encourages testing components the way users interact with them - by finding elements by role, label, or text, not by component internals.
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
function LoginForm({ onSubmit }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
return (
<form onSubmit={(e) => { e.preventDefault(); onSubmit({ email, password }); }}>
<label htmlFor="email">Email</label>
<input id="email" value={email} onChange={e => setEmail(e.target.value)} />
<label htmlFor="password">Password</label>
<input id="password" type="password" value={password}
onChange={e => setPassword(e.target.value)} />
<button type="submit">Log In</button>
</form>
);
}
// Test
test('submits login form with email and password', async () => {
const handleSubmit = vi.fn();
const user = userEvent.setup();
render(<LoginForm onSubmit={handleSubmit} />);
await user.type(screen.getByLabelText('Email'), 'test@example.com');
await user.type(screen.getByLabelText('Password'), 'secret123');
await user.click(screen.getByRole('button', { name: 'Log In' }));
expect(handleSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'secret123',
});
});
Key principles: query by accessibility roles first (getByRole), then by label text, then by text content. Never query by class name or test ID unless there's no better option. If it's hard to find an element, that often means your HTML isn't accessible.
46. How do you test custom hooks?
The real interview answer: Use renderHook from React Testing Library. It renders your hook inside a test component so you can inspect its return value and call its functions.
import { renderHook, act } from '@testing-library/react';
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(c => c + 1);
const decrement = () => setCount(c => c - 1);
const reset = () => setCount(initialValue);
return { count, increment, decrement, reset };
}
test('useCounter increments and decrements', () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(11);
act(() => {
result.current.decrement();
result.current.decrement();
});
expect(result.current.count).toBe(9);
act(() => {
result.current.reset();
});
expect(result.current.count).toBe(10);
});
// Testing hooks that need providers
test('useAuth returns current user', () => {
const wrapper = ({ children }) => (
<AuthProvider value={{ user: { name: 'Pat' } }}>
{children}
</AuthProvider>
);
const { result } = renderHook(() => useAuth(), { wrapper });
expect(result.current.user.name).toBe('Pat');
});
Wrap state updates in act() so React processes them before your assertions run. For hooks with effects, you may need waitFor from Testing Library.
47. How do you test async components?
The real interview answer: Use findBy queries (which wait for elements to appear) and waitFor for more complex async assertions.
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser).catch(setError);
}, [userId]);
if (error) return <div role="alert">{error.message}</div>;
if (!user) return <div>Loading...</div>;
return <h1>{user.name}</h1>;
}
// Test
test('displays user name after loading', async () => {
// Mock the API
global.fetch = vi.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ name: 'Pat', email: 'pat@test.com' }),
})
);
render(<UserProfile userId="123" />);
// Initially shows loading
expect(screen.getByText('Loading...')).toBeInTheDocument();
// Waits for the user name to appear
const heading = await screen.findByRole('heading', { name: 'Pat' });
expect(heading).toBeInTheDocument();
});
test('shows error message on fetch failure', async () => {
global.fetch = vi.fn(() => Promise.reject(new Error('Network error')));
render(<UserProfile userId="123" />);
const alert = await screen.findByRole('alert');
expect(alert).toHaveTextContent('Network error');
});
Pro tip: findBy queries have a default timeout of 1000ms. For slower operations, pass { timeout: 3000 }. But if your tests regularly need longer timeouts, your component might need a loading state improvement.
48. What is snapshot testing, and why is it mostly useless?
The real interview answer: Snapshot testing renders a component to a serialized string, saves it to a file, and fails if the output changes. In theory, it catches unintended changes. In practice, everyone just runs --updateSnapshot when they see a diff.
// The snapshot test
test('renders correctly', () => {
const { container } = render(<Button variant="primary">Click me</Button>);
expect(container).toMatchSnapshot();
});
// Generates a .snap file:
// exports[`renders correctly 1`] = `
// <div>
// <button class="btn btn-primary">Click me</button>
// </div>
// `;
Why it's mostly useless:
- False positives everywhere. Change a CSS class name? Every snapshot breaks.
- Nobody reviews snapshot diffs. They're huge and meaningless, so developers blindly update them.
- They don't test behavior. A snapshot says "the HTML looks like this" but not "the button actually works."
- They create maintenance burden. Hundreds of snapshot files that need updating on any UI change.
What to do instead: write targeted assertions about specific things you care about. Does the button have the right text? Is the error message visible? Is the link pointing to the right URL? These survive refactors and actually catch real bugs.
The one place snapshots are okay: testing serialized data structures (API responses, config objects) where the exact shape matters and changes should be intentional.
Architecture & Patterns (Questions 49-50)
49. What are Compound Components, and when would you use them?
The real interview answer: Compound Components are a pattern where multiple components work together to form a cohesive unit, sharing implicit state. Think of how <select> and <option> work together in HTML.
// Compound Component pattern
const Accordion = ({ children }) => {
const [openIndex, setOpenIndex] = useState(null);
return (
<AccordionContext.Provider value={{ openIndex, setOpenIndex }}>
<div className="accordion">{children}</div>
</AccordionContext.Provider>
);
};
const AccordionItem = ({ index, children }) => {
const { openIndex, setOpenIndex } = useContext(AccordionContext);
const isOpen = openIndex === index;
return (
<div className="accordion-item">
<button onClick={() => setOpenIndex(isOpen ? null : index)}>
{children[0]} {/* header */}
</button>
{isOpen && <div className="accordion-body">{children[1]}</div>}
</div>
);
};
// Clean, declarative API
<Accordion>
<AccordionItem index={0}>
<span>Section 1</span>
<p>Content for section 1</p>
</AccordionItem>
<AccordionItem index={1}>
<span>Section 2</span>
<p>Content for section 2</p>
</AccordionItem>
</Accordion>
Use compound components when you have a group of related components that share state and need a clean, declarative API. Tabs, accordions, dropdowns, and menus are classic examples. Libraries like Radix UI and Headless UI use this pattern extensively.
50. How did render props evolve into hooks, and what composition patterns matter today?
The real interview answer: Render props solved the same problem custom hooks solve - sharing stateful logic between components. But render props created "wrapper hell" and were harder to read.
// Old render props pattern
<MousePosition>
{({ x, y }) => (
<WindowSize>
{({ width, height }) => (
<div>Mouse: {x},{y} Window: {width}x{height}</div>
)}
</WindowSize>
)}
</MousePosition>
// Modern hooks equivalent - way cleaner
function Dashboard() {
const { x, y } = useMousePosition();
const { width, height } = useWindowSize();
return <div>Mouse: {x},{y} Window: {width}x{height}</div>;
}
Render props aren't dead though. They're still useful for components that need to control what gets rendered, not just data. Libraries like TanStack Table and Downshift use them for maximum flexibility.
Composition patterns that matter in 2026:
// 1. Children as the primary composition mechanism
<Card>
<CardHeader>Title</CardHeader>
<CardBody>Content</CardBody>
</Card>
// 2. Slots pattern for named insertion points
<Dialog>
<Dialog.Trigger>Open</Dialog.Trigger>
<Dialog.Content>
<Dialog.Title>Are you sure?</Dialog.Title>
<Dialog.Actions>
<Button>Cancel</Button>
<Button variant="danger">Delete</Button>
</Dialog.Actions>
</Dialog.Content>
</Dialog>
// 3. Hooks for shared logic, components for shared UI
function useSearch(items) {
const [query, setQuery] = useState('');
const filtered = useMemo(
() => items.filter(i => i.name.toLowerCase().includes(query.toLowerCase())),
[items, query]
);
return { query, setQuery, filtered };
}
The best React code in 2026 follows a simple rule: hooks for logic, components for UI, composition for flexibility. Keep components small, extract custom hooks aggressively, and prefer composition over configuration props.
React interviews aren't really about memorizing API signatures. Interviewers want to see that you understand why things work the way they do, that you've hit real problems and solved them, and that you can reason about tradeoffs. If you can explain when NOT to use useMemo, why Server Components changed the data fetching story, and how to actually debug a performance issue with the Profiler - you'll stand out from candidates who just memorized a list of definitions. Build things, break things, and understand the "why" behind the tools you use every day.