React Interview Questions and Answers
These questions cover whatβs actually tested in frontend and full-stack developer interviews β from core React fundamentals to React 19βs concurrent features and modern patterns.
Core Concepts
Q1. What is React and what problem does it solve?
React is a JavaScript library by Meta for building component-based user interfaces. It solves the problem of keeping the UI in sync with state changes efficiently.
Core ideas:
- Component model β UI as a tree of composable, reusable components
- Declarative rendering β describe what the UI should look like for a given state; React handles DOM updates
- Virtual DOM β React diffs a lightweight JS representation of the DOM and applies minimal real DOM changes
- Unidirectional data flow β data flows down (props), events flow up (callbacks)
React 18/19 extends this with concurrent rendering, transitions, and Server Components.
Q2. What is the difference between props and state?
// Props β data passed from parent; read-only in the receiving componentfunction Greeting({ name, age }) { return <h1>Hello, {name} ({age})</h1>;}
// State β private data managed within the component; triggers re-render when changedimport { useState } from 'react';
function Counter() { const [count, setCount] = useState(0);
return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(c => c + 1)}>Increment</button> </div> );}Key rules:
- Props flow down (parent β child); state is local
- Never mutate props or state directly β always use the setter (
setCount) - Both props changes and state changes trigger a re-render
Q3. What are React hooks and what are the rules of hooks?
Hooks are functions that let function components use React features (state, lifecycle, context):
Rules of hooks:
- Only call hooks at the top level β not inside conditionals, loops, or nested functions
- Only call hooks from React function components or custom hooks (not regular JS functions)
// CORRECTfunction SearchComponent() { const [query, setQuery] = useState(''); const results = useSearch(query); // Custom hook return <input value={query} onChange={e => setQuery(e.target.value)} />;}
// WRONG β hook inside a condition (breaks rules)function BrokenComponent({ show }) { if (show) { const [count, setCount] = useState(0); // Error! }}Common built-in hooks: useState, useEffect, useContext, useReducer, useCallback, useMemo, useRef, useId, useTransition, useDeferredValue.
Q4. Explain useEffect and the cleanup function.
import { useState, useEffect } from 'react';
function UserProfile({ userId }) { const [user, setUser] = useState(null);
useEffect(() => { let cancelled = false; // Guard against race conditions
async function loadUser() { const data = await fetchUser(userId); if (!cancelled) setUser(data); }
loadUser();
// Cleanup function β runs before next effect OR on unmount return () => { cancelled = true; }; }, [userId]); // Dependency array β re-runs when userId changes
if (!user) return <p>Loading...</p>; return <h1>{user.name}</h1>;}
// Dependency array behavior:// [] (empty) β runs once on mount, cleanup on unmount// [dep1, dep2] β runs when deps change// omitted β runs on every render (usually wrong)State Management
Q5. What is the difference between useState and useReducer?
// useState β best for simple, independent state valuesconst [count, setCount] = useState(0);const [name, setName] = useState('');
// useReducer β best for complex state with multiple related actionsimport { useReducer } from 'react';
const initialState = { count: 0, status: 'idle', error: null };
function reducer(state, action) { switch (action.type) { case 'INCREMENT': return { ...state, count: state.count + 1 }; case 'DECREMENT': return { ...state, count: state.count - 1 }; case 'FETCH_START': return { ...state, status: 'loading', error: null }; case 'FETCH_SUCCESS': return { ...state, status: 'success', count: action.payload }; case 'FETCH_ERROR': return { ...state, status: 'error', error: action.payload }; default: return state; }}
function Counter() { const [state, dispatch] = useReducer(reducer, initialState);
return ( <> <p>Count: {state.count} ({state.status})</p> <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button> </> );}Use useReducer when state transitions are complex, when the next state depends on multiple previous values, or when you want co-located, testable action logic.
Q6. What is the Context API and when would you use Redux instead?
import { createContext, useContext, useState } from 'react';
// Create contextconst ThemeContext = createContext('light');
// Provider wraps the component treefunction App() { const [theme, setTheme] = useState('light');
return ( <ThemeContext.Provider value={{ theme, setTheme }}> <MainLayout /> </ThemeContext.Provider> );}
// Any descendant can consume without prop drillingfunction ThemeToggle() { const { theme, setTheme } = useContext(ThemeContext); return ( <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}> Current: {theme} </button> );}Context works well for: theme, locale, auth user, feature flags β data thatβs global but changes infrequently.
Redux / Zustand / Jotai when you need: complex state with many updates, time-travel debugging, middleware (logging, analytics), or multiple components subscribing to different slices that update very frequently.
Performance
Q7. What are useMemo and useCallback and when do you actually need them?
import { useMemo, useCallback, memo } from 'react';
// useMemo β memoize expensive computed valuefunction ProductList({ products, searchQuery }) { // Only recomputes when products or searchQuery changes const filteredProducts = useMemo( () => products.filter(p => p.name.toLowerCase().includes(searchQuery.toLowerCase())), [products, searchQuery] );
return filteredProducts.map(p => <ProductCard key={p.id} product={p} />);}
// useCallback β memoize a function referencefunction Parent() { const [count, setCount] = useState(0);
// Without useCallback, new function reference every render β // Child re-renders even when not needed (if it's wrapped in memo) const handleSubmit = useCallback((data) => { submitForm(data, count); }, [count]); // Only new function when count changes
return <ExpensiveChild onSubmit={handleSubmit} />;}
// memo β prevent re-render if props haven't changed (shallowly)const ExpensiveChild = memo(function ExpensiveChild({ onSubmit }) { return <button onClick={() => onSubmit({})}>Submit</button>;});Important: these optimizations have a cost (memory, complexity). Only add them when you have a measured performance problem.
Q8. What is Reactβs reconciliation algorithm and what is the key prop for?
Reconciliation is how React determines the minimal set of DOM changes needed when state updates. React diffs the previous and next virtual DOM trees:
- Elements of different types β tear down old tree, build new one
- Same type β update changed attributes
- Lists β compare by position (or
key)
The key prop lets React identify which list items have changed, been added, or removed:
// BAD β key based on index; if items reorder, React sees same positions β wrong updates{items.map((item, index) => ( <TodoItem key={index} todo={item} />))}
// GOOD β stable, unique key{items.map(item => ( <TodoItem key={item.id} todo={item} />))}
// Also: avoid generating keys randomly (Math.random()) β defeats the purposeKeys must be unique among siblings, not globally unique. Keys help React match items across renders, preserving component state for stable items.
Q9. What is lazy loading and Suspense in React?
import { lazy, Suspense } from 'react';
// Code-split a component β bundle for HeavyChart is loaded only when renderedconst HeavyChart = lazy(() => import('./HeavyChart'));
function Dashboard() { return ( <div> <h1>Dashboard</h1> <Suspense fallback={<div className="skeleton">Loading chart...</div>}> <HeavyChart data={data} /> </Suspense> </div> );}
// Nested Suspense boundaries for granular loading statesfunction App() { return ( <Suspense fallback={<AppSkeleton />}> <Header /> <Suspense fallback={<ChartSkeleton />}> <Dashboard /> </Suspense> </Suspense> );}Suspense also works with data fetching when using React 19βs use() hook or frameworks like Next.js that integrate with Reactβs concurrent features.
Modern React
Q10. What is useTransition and when do you use it?
useTransition marks a state update as non-urgent, letting React keep the UI responsive while the update is being processed:
import { useState, useTransition } from 'react';
function SearchPage() { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [isPending, startTransition] = useTransition();
function handleChange(e) { setQuery(e.target.value); // Urgent β update input immediately
startTransition(() => { // Non-urgent β can be interrupted const filtered = heavySearch(e.target.value); setResults(filtered); }); }
return ( <div> <input value={query} onChange={handleChange} /> {isPending && <p>Searching...</p>} <ResultsList results={results} /> </div> );}The input stays responsive while the expensive results re-render happens concurrently. If the user types again before results finish, React discards the old transition and starts a new one.
Q11. What are React Server Components (RSC) and how do they differ from Client Components?
React Server Components render on the server and send only HTML β no JavaScript shipped for them to the client:
// page.tsx (Server Component β runs on server, no 'use client')async function UserProfile({ userId }) { // Direct DB access β no API layer needed! const user = await db.users.findById(userId); const posts = await db.posts.findByUserId(userId);
return ( <div> <h1>{user.name}</h1> {/* InteractiveButton is a Client Component */} <InteractiveButton userId={userId} /> <PostList posts={posts} /> </div> );}'use client'; // This directive makes it a Client Component
import { useState } from 'react';
function InteractiveButton({ userId }) { const [followed, setFollowed] = useState(false);
return ( <button onClick={() => setFollowed(f => !f)}> {followed ? 'Following' : 'Follow'} </button> );}| Server Components | Client Components | |
|---|---|---|
| JavaScript sent to client | No | Yes |
| Can use hooks | No | Yes |
| Can fetch data directly | Yes (async/await) | No (useEffect or SWR) |
| Access to browser APIs | No | Yes |
| Default in Next.js App Router | Yes | Must add 'use client' |
Q12. Whatβs new in React 19 (2024)?
Actions β built-in way to handle async form submissions without extra state:
function ContactForm() { async function submitAction(formData) { 'use server'; // Server Action await sendEmail(formData.get('email')); }
return ( <form action={submitAction}> <input name="email" type="email" /> <button type="submit">Send</button> </form> );}use() hook β read resources (promises, context) inline in render:
const data = use(fetchPromise); // Suspends until resolvedconst theme = use(ThemeContext); // Like useContext but composableuseFormStatus β track pending state of an enclosing form:
function SubmitButton() { const { pending } = useFormStatus(); return <button disabled={pending}>{pending ? 'Sending...' : 'Send'}</button>;}useOptimistic β show optimistic UI while async action is pending:
const [optimisticMessages, addOptimistic] = useOptimistic(messages);Ref as a prop β no longer need forwardRef; just pass ref as a regular prop.
Custom Hooks & Patterns
Q13. Write a custom hook for debouncing a value.
import { useState, useEffect } from 'react';
function useDebounce<T>(value: T, delay: number = 300): T { const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => { const timer = setTimeout(() => { setDebouncedValue(value); }, delay);
return () => clearTimeout(timer); // Clear timer if value changes before delay }, [value, delay]);
return debouncedValue;}
// Usagefunction SearchBox() { const [query, setQuery] = useState(''); const debouncedQuery = useDebounce(query, 300);
useEffect(() => { if (debouncedQuery) { searchAPI(debouncedQuery); // Only fires 300ms after user stops typing } }, [debouncedQuery]);
return <input value={query} onChange={e => setQuery(e.target.value)} />;}Q14. What is the compound component pattern?
Compound components share implicit state through context without explicit prop passing:
import { createContext, useContext, useState } from 'react';
const AccordionContext = createContext(null);
function Accordion({ children, defaultOpen = null }) { const [openId, setOpenId] = useState(defaultOpen);
return ( <AccordionContext.Provider value={{ openId, setOpenId }}> <div className="accordion">{children}</div> </AccordionContext.Provider> );}
function AccordionItem({ id, title, children }) { const { openId, setOpenId } = useContext(AccordionContext); const isOpen = openId === id;
return ( <div> <button onClick={() => setOpenId(isOpen ? null : id)}> {title} </button> {isOpen && <div className="content">{children}</div>} </div> );}
// Clean, expressive usage<Accordion defaultOpen="faq-1"> <AccordionItem id="faq-1" title="What is React?"> React is a UI library... </AccordionItem> <AccordionItem id="faq-2" title="What are hooks?"> Hooks are functions... </AccordionItem></Accordion>