React Developer Interview Questions and How to Answer Them (50+ Questions)
Whether you are interviewing for a junior, mid-level, or senior React role, this guide covers every topic an interviewer is likely to raise — from first-principles fundamentals to the architecture decisions that separate experienced engineers from the rest. Each answer is written the way a strong candidate would actually speak it, with working code you can study and adapt.
The questions are grouped by topic so you can drill the areas where you need the most work.
Table of Contents
- 1Core Fundamentals (Q1–Q8)
- 2React Hooks Deep Dive (Q9–Q20)
- 3Performance Optimization (Q21–Q28)
- 4Virtual DOM, Fiber, and Reconciliation (Q29–Q34)
- 5State Management (Q35–Q40)
- 6Testing (Q41–Q45)
- 7TypeScript with React (Q46–Q48)
- 8Concurrent Features and React 18/19 (Q49–Q52)
- 9Architecture and Patterns (Q53–Q55)
Part 1 — Core Fundamentals
Q1. What is React and what problem does it solve?
React is a JavaScript library for building user interfaces. It was created at Facebook to solve a very specific problem: keeping a complex, data-driven UI in sync with application state without manually updating the DOM every time something changes.
The two core ideas that make React work:
Declarative rendering. You describe what the UI should look like for a given state. React figures out how to make the DOM match. You never write "find this node and change its text."
Component model. UIs are composed of small, reusable, self-contained pieces. Each component owns its own logic, markup, and (optionally) its own state. This makes large codebases easier to reason about and test.
// Declarative — describe what you want, not how to achieve it
function Greeting({ name }) {
return <h1>Hello, {name}</h1>;
}Interview tip: Mention that React is a *library*, not a framework. It handles the view layer; routing, data fetching, and state management require separate decisions.
Q2. What is JSX and how does it compile?
JSX is a syntax extension that looks like HTML inside JavaScript. It is not valid JavaScript — the build step (Babel or the React compiler) transforms it into React.createElement calls.
// What you write
const el = <button className="btn">Click me</button>;
// What it compiles to (pre-React 17 transform)
const el = React.createElement("button", { className: "btn" }, "Click me");
// With the new JSX transform (React 17+), no import needed
// The compiler imports from react/jsx-runtime automaticallyJSX is entirely optional but nearly universal because it is more readable than nested createElement calls. It enforces one important rule: adjacent elements must be wrapped in a single parent (or <>...> Fragment) because a function can only return one value.
Q3. What is the difference between a controlled and uncontrolled component?
Controlled component: React state is the single source of truth. Every keystroke updates state, and the input's value is always derived from that state.
function ControlledInput() {
const [value, setValue] = React.useState("");
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}Uncontrolled component: The DOM itself holds the value. You read it via a ref when you need it (e.g., on form submit).
function UncontrolledInput() {
const inputRef = React.useRef(null);
function handleSubmit() {
console.log(inputRef.current.value);
}
return <input ref={inputRef} />;
}When to use which:
- Controlled: form validation on every keystroke, dependent fields, complex form state.
- Uncontrolled: simple file inputs, integrating with non-React libraries, or when you want minimal re-renders on every keystroke (large forms with hundreds of fields).
Q4. Explain the React component lifecycle for both class components and functional components with hooks.
Class component lifecycle (simplified):
| Phase | Methods |
|---|---|
| Mount | constructor → render → componentDidMount |
| Update | render → componentDidUpdate |
| Unmount | componentWillUnmount |
Functional component equivalent with hooks:
function MyComponent({ id }) {
// Runs on every render (like render())
console.log("rendering");
React.useEffect(() => {
// Runs after mount (like componentDidMount)
const subscription = subscribe(id);
return () => {
// Runs before unmount or before next effect (like componentWillUnmount)
subscription.cancel();
};
}, [id]); // Re-runs when id changes (like componentDidUpdate for id)
}Interviewers care about three things here: knowing the cleanup return, understanding the dependency array, and knowing that useLayoutEffect fires synchronously before the browser paints (use it for DOM measurements).
Q5. What are props and how do they differ from state?
| | Props | State |
|---|---|---|
| Owned by | Parent component | The component itself |
| Mutable? | No (read-only from child's perspective) | Yes, via setter |
| Triggers re-render? | Yes, when parent passes new value | Yes, when updated |
| Purpose | Configuration, communication down the tree | Local dynamic data |
// Props — passed in, read-only
function UserCard({ name, role }) {
return <div>{name} — {role}</div>;
}
// State — owned locally, mutable
function Counter() {
const [count, setCount] = React.useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}A common follow-up: "Can a child modify its own props?" — No. The child calls a callback prop provided by the parent, and the parent updates its own state, which flows back down as new props.
Q6. What are React Fragments and why do they exist?
React.Fragment (or the shorthand <>...>) lets you return multiple elements from a component without adding an extra DOM node.
// Without Fragment — adds a meaningless div to the DOM
function TableRow() {
return (
<div>
<td>Name</td>
<td>Age</td>
</div>
);
}
// With Fragment — valid HTML structure preserved
function TableRow() {
return (
<>
<td>Name</td>
<td>Age</td>
</>
);
}Use the explicit form (not the shorthand) when you need to add a key prop to items in a list.
Q7. What is prop drilling and how do you avoid it?
Prop drilling happens when a deeply nested component needs data and you pass it through every intermediate component even though those intermediates don't use the data themselves.
App (has user) → Layout → Sidebar → UserMenu (needs user)
↑ Layout and Sidebar don't need user
but have to pass it alongSolutions in order of complexity:
- 1Context API — good for infrequently changing data (theme, locale, auth user).
- 2Component composition — lift the consumer component up and pass it as a child or prop (often underused).
- 3External state library (Zustand, Redux) — components subscribe directly to the store slice they need.
Q8. What is a React Portal?
Portals render a component's output into a different DOM node than its parent, while keeping it in the React tree (so events still bubble through React's synthetic event system).
import { createPortal } from "react-dom";
function Modal({ children, isOpen }) {
if (!isOpen) return null;
return createPortal(
<div className="modal-overlay">
<div className="modal-content" role="dialog" aria-modal="true">
{children}
</div>
</div>,
document.getElementById("modal-root") // target DOM node outside #root
);
}Use cases: modals, tooltips, dropdown menus that need to break out of overflow: hidden containers, and toast notifications.
Part 2 — React Hooks Deep Dive
Q9. What are the Rules of Hooks and why do they exist?
React tracks hook state by call order, not by name. That means hook #1 always corresponds to the same piece of state across renders. If you call hooks conditionally, the order can change between renders and React loses track of which state belongs to which hook.
Rule 1: Only call hooks at the top level — never inside loops, conditions, or nested functions.
Rule 2: Only call hooks from React function components or other custom hooks.
// WRONG — conditional hook call
function Component({ show }) {
if (show) {
const [value, setValue] = useState(""); // hook #1 only sometimes
}
const [other, setOther] = useState(0); // might be hook #1 or #2 depending on show
}
// CORRECT — the condition is inside the hook, not around it
function Component({ show }) {
const [value, setValue] = useState("");
const [other, setOther] = useState(0);
if (!show) return null;
}The eslint-plugin-react-hooks package enforces these rules automatically at lint time.
Q10. How does useState work? What is lazy initialization?
useState returns a state variable and a setter. React persists the state between renders; calling the setter schedules a re-render with the new value.
Functional updater pattern — use this when the new state depends on the previous value to avoid stale reads:
// Potentially stale (reads count from closure)
setCount(count + 1);
// Safe — always uses the latest state
setCount(prev => prev + 1);Lazy initialization — when initial state is expensive to compute, pass a function instead of a value. The function runs only on the first render:
// Without lazy init — parses JSON on EVERY render
const [settings, setSettings] = useState(JSON.parse(localStorage.getItem("settings")));
// With lazy init — parses JSON once
const [settings, setSettings] = useState(() => JSON.parse(localStorage.getItem("settings")));Q11. Explain the useEffect hook and its dependency array behavior.
useEffect runs a side effect (data fetching, subscriptions, DOM manipulation) after the component renders.
useEffect(() => {
// Side effect code
return () => {
// Cleanup — runs before next effect or unmount
};
}, [dependency1, dependency2]);Dependency array behavior:
| Array | Behavior |
|---|---|
| Not provided | Runs after every render |
| [] (empty) | Runs once after mount |
| [a, b] | Runs after mount and after any render where a or b changed |
Data fetching with cleanup to prevent race conditions:
useEffect(() => {
let active = true;
async function fetchUser() {
const data = await getUser(userId);
if (active) {
setUser(data);
}
}
fetchUser();
return () => {
active = false; // Ignore stale responses if userId changes before this resolves
};
}, [userId]);Modern React with React 19's use hook or a library like TanStack Query handles this race-condition problem for you at a higher level.
Q12. What is useLayoutEffect and when do you use it instead of useEffect?
| | useEffect | useLayoutEffect |
|---|---|---|
| When it runs | Asynchronously after browser paint | Synchronously after DOM mutations, before paint |
| Use case | Most side effects | DOM measurements, preventing visual flicker |
function Tooltip({ anchor }) {
const tooltipRef = useRef(null);
const [position, setPosition] = useState({ top: 0, left: 0 });
useLayoutEffect(() => {
// Read DOM measurements BEFORE the browser paints
// so there's no visible "jump"
const rect = anchor.getBoundingClientRect();
setPosition({ top: rect.bottom, left: rect.left });
}, [anchor]);
return (
<div ref={tooltipRef} style={{ position: "absolute", ...position }}>
Tooltip content
</div>
);
}Rule of thumb: default to useEffect. Only reach for useLayoutEffect when you have a measurable visual flicker caused by layout reads immediately after render.
Q13. What is useRef and what are its two main use cases?
useRef returns a mutable object { current: value } that persists across renders without triggering re-renders when mutated.
Use case 1 — DOM access:
function FocusInput() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>Focus</button>
</>
);
}Use case 2 — Mutable values that should not trigger re-renders (e.g., timers, previous values):
function Interval() {
const timerRef = useRef(null);
function start() {
timerRef.current = setInterval(() => console.log("tick"), 1000);
}
function stop() {
clearInterval(timerRef.current);
}
return (
<>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</>
);
}Q14. Explain useMemo and useCallback. When should you actually use them?
Both are memoization tools that exist to preserve referential identity across renders.
useMemo — memoizes the *result* of a computation:
const filteredList = useMemo(
() => items.filter(item => item.active && item.name.includes(query)),
[items, query] // Recompute only when these change
);useCallback — memoizes the *function itself*:
const handleDelete = useCallback(
(id) => dispatch({ type: "DELETE", id }),
[dispatch] // dispatch from useReducer is stable, so this runs once
);When to actually use them:
- 1
useMemo: expensive computations (O(n log n) or heavier), or when an object/array reference must be stable for a dependency array. - 2
useCallback: the function is passed to aReact.memo-wrapped child component that would otherwise re-render, or the function is a dependency of another hook.
When NOT to use them: wrapping every function and value blindly adds memory and comparison overhead. Profile first with React DevTools Profiler, then memoize only where there is a measured problem.
Q15. What is useReducer and when is it better than useState?
useReducer manages state via a pure function (the reducer) that maps (state, action) => newState. It follows the same pattern as Redux.
const initialState = { count: 0, step: 1 };
function reducer(state, action) {
switch (action.type) {
case "INCREMENT":
return { ...state, count: state.count + state.step };
case "SET_STEP":
return { ...state, step: action.payload };
case "RESET":
return initialState;
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
<p>Count: {state.count} (step: {state.step})</p>
<button onClick={() => dispatch({ type: "INCREMENT" })}>+</button>
<button onClick={() => dispatch({ type: "SET_STEP", payload: 5 })}>Step 5</button>
<button onClick={() => dispatch({ type: "RESET" })}>Reset</button>
</>
);
}Prefer useReducer over useState when:
- Multiple related state values change together.
- Next state depends on previous state in non-trivial ways.
- You want co-located, testable state logic (the reducer is a pure function — easy to unit test with no React needed).
- You find yourself writing many
useStatesetters that always change together.
Q16. How do you write a custom hook? Give a realistic example.
A custom hook is a JavaScript function whose name starts with use and that calls other hooks internally. It lets you extract and share stateful logic without adding extra components to the tree.
// useFetch — data fetching with loading, error, and cancellation
function useFetch(url) {
const [data, setData] = React.useState(null);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState(null);
React.useEffect(() => {
const controller = new AbortController();
async function fetchData() {
try {
setLoading(true);
const res = await fetch(url, { signal: controller.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
setData(json);
} catch (err) {
if (err.name !== "AbortError") {
setError(err.message);
}
} finally {
setLoading(false);
}
}
fetchData();
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
// Usage
function UserProfile({ id }) {
const { data: user, loading, error } = useFetch(`/api/users/${id}`);
if (loading) return <Spinner />;
if (error) return <p>Error: {error}</p>;
return <div>{user.name}</div>;
}Q17. What is a stale closure in React and how do you fix it?
A stale closure happens when an effect (or event handler) captures a variable from the time it was created, and that variable later goes out of date without the closure knowing.
// BUG — stale closure in setInterval
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
// count is always 0 here — captured at mount, never updates
console.log(count);
setCount(count + 1); // will always set to 1
}, 1000);
return () => clearInterval(interval);
}, []); // Empty array means effect never re-runs
return <div>{count}</div>;
}Fix 1 — functional updater (for state):
setCount(prev => prev + 1); // reads current state, not closure valueFix 2 — ref to hold latest value (for anything else):
function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
countRef.current = count; // Always up to date
useEffect(() => {
const interval = setInterval(() => {
console.log(countRef.current); // Never stale
}, 1000);
return () => clearInterval(interval);
}, []);
}Q18. What is useContext and what are its performance pitfalls?
useContext subscribes a component to a context value. When the context value changes, every component that called useContext with that context re-renders — even if they only use part of the value.
const ThemeContext = React.createContext("light");
function App() {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={theme}>
<Page />
<button onClick={() => setTheme(t => t === "light" ? "dark" : "light")}>
Toggle
</button>
</ThemeContext.Provider>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button className={`btn-${theme}`}>Click</button>;
}Performance pitfall: if you put { user, setUser, theme, setTheme } all in one context, every consumer re-renders whenever any of those values change.
Fixes:
- 1Split contexts by concern (one for user, one for theme).
- 2Memoize the context value:
const value = useMemo(() => ({ user }), [user]). - 3For frequently changing values, reach for Zustand's
useStore(selector)— it only re-renders when the selected slice changes.
Q19. Explain useTransition and useDeferredValue (React 18+).
These hooks allow you to mark certain state updates as non-urgent so React can keep the UI responsive during heavy renders.
useTransition — wraps the state setter at the call site:
function SearchPage() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
function handleChange(e) {
setQuery(e.target.value); // Urgent — update the input immediately
startTransition(() => {
setResults(heavyFilter(e.target.value)); // Non-urgent — can be interrupted
});
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<ResultList results={results} />
</>
);
}useDeferredValue — wraps a value at the consumption site (use when the value comes from props you do not control):
function ResultList({ query }) {
const deferredQuery = useDeferredValue(query);
// Renders with a lagging value while React prioritizes urgent updates
const results = useMemo(() => heavyFilter(deferredQuery), [deferredQuery]);
return <List items={results} />;
}Key difference: useTransition is for when you control the setter. useDeferredValue is for when you receive a value as a prop and want to defer downstream computation.
Q20. What is useId and why does it exist?
useId generates a unique, stable ID that is consistent between server and client renders. It exists specifically to prevent hydration mismatches when you need IDs for accessibility (linking to ).
function FormField({ label }) {
const id = useId();
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} />
</div>
);
}Never use this to generate list keys — use the data's own identifier for that. useId is purely for accessibility wiring.
Part 3 — Performance Optimization
Q21. What is React.memo and how does it differ from useMemo?
React.memo is a higher-order component that memoizes an entire functional component. It does a shallow comparison of props before deciding whether to re-render.
const ExpensiveList = React.memo(function ExpensiveList({ items, onSelect }) {
console.log("ExpensiveList rendered");
return (
<ul>
{items.map(item => (
<li key={item.id} onClick={() => onSelect(item.id)}>
{item.name}
</li>
))}
</ul>
);
});
// In the parent — useCallback keeps onSelect stable across renders
function Parent() {
const [selected, setSelected] = useState(null);
const handleSelect = useCallback((id) => {
setSelected(id);
}, []);
return <ExpensiveList items={STATIC_ITEMS} onSelect={handleSelect} />;
}Key distinction:
React.memo— memoizes a component (prevents re-render).useMemo— memoizes a value (prevents recomputation).
React.memo only helps if the component's props are actually stable. If you pass a new object or function literal on every parent render, React.memo does nothing. That is why useCallback and useMemo are often used together with it.
Q22. How does code splitting work with React.lazy and Suspense?
Code splitting breaks your bundle into chunks that load on demand. React.lazy loads a component only when it is first rendered.
import React, { lazy, Suspense } from "react";
// Dashboard is loaded in a separate chunk — not included in initial bundle
const Dashboard = lazy(() => import("./Dashboard"));
const Settings = lazy(() => import("./Settings"));
function App() {
const [page, setPage] = useState("home");
return (
<Suspense fallback={<div>Loading...</div>}>
{page === "dashboard" && <Dashboard />}
{page === "settings" && <Settings />}
</Suspense>
);
}For route-level splitting (the most impactful split point), pair it with React Router:
const routes = [
{ path: "/dashboard", element: lazy(() => import("./pages/Dashboard")) },
{ path: "/settings", element: lazy(() => import("./pages/Settings")) },
];Interview follow-up: "What do you split?" Route-level components first. Then large, rarely-used features like rich text editors, chart libraries, or modals containing heavy third-party dependencies.
Q23. What is virtualization (windowing) and when do you use it?
When you render a list of 10,000 items, React creates 10,000 DOM nodes — most of which are off-screen. Virtualization renders only the items currently visible in the viewport, dramatically reducing DOM nodes.
Libraries: react-window (lightweight), react-virtual (TanStack Virtual — more flexible), @tanstack/react-virtual.
import { FixedSizeList } from "react-window";
function VirtualList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>{items[index].name}</div>
);
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
);
}Rule of thumb: lists over ~200 items are candidates for virtualization. Profile with React DevTools first.
Q24. How do you identify performance bottlenecks in a React app?
Step 1 — React DevTools Profiler. Record a session, look at the flamegraph. Bars that are wide and orange are slow. Look for components that re-render when they should not.
Step 2 — Check why components re-render. Right-click a component in DevTools → "Highlight updates when components render." Unexpected flashes indicate unnecessary re-renders.
Step 3 — Browser performance tab. Look for long tasks (>50ms on the main thread) blocking interaction.
Common culprits:
- Parent re-renders pass new object/array literals as props to memoized children (breaking memoization).
- Large context values causing entire subtrees to re-render on every change.
- Missing keys on lists causing full unmounts and remounts.
- Expensive computations inside render without
useMemo. - Fetching data without caching (re-fetching on every mount).
Q25. What is the difference between shallow and deep comparison? Why does React use shallow comparison?
Shallow comparison compares primitive values by value and objects/arrays by reference:
{ a: 1 } === { a: 1 } → false (different references)
const obj = { a: 1 }; obj === obj → true (same reference)Deep comparison recursively checks every nested value — correct but O(n) in the size of the data structure.
React uses shallow comparison in React.memo, PureComponent, and dependency array checks because it is O(1) and predictable. Deep comparison would be too expensive to run on every render for every prop.
The practical implication: if you pass a new object/array literal as a prop, shallow comparison always says "changed" even if the content is identical. The fix is to memoize the object with useMemo or move it outside the component if it is static.
Q26. How do you optimize a React app's initial load time?
- 1Code splitting at route and feature level (React.lazy + Suspense).
- 2Tree shaking — import only what you use:
import { debounce } from "lodash-es"notimport _ from "lodash". - 3Preload critical resources with
for fonts and key images. - 4Compress assets — Brotli/gzip via CDN or server config.
- 5Defer non-critical JS —
or dynamic imports. - 6Optimize images — WebP format, appropriate dimensions, lazy loading (
loading="lazy"). - 7Bundle analysis — use
webpack-bundle-analyzeror Vite'srollup-plugin-visualizerto find unexpectedly large dependencies.
Q27. Explain React Server Components (RSC). How do they affect performance?
React Server Components run exclusively on the server. They never send their JavaScript to the client — only the rendered output (a serialized React tree). This means:
- Zero bundle impact for server components.
- They can directly access databases, filesystems, and secrets.
- They cannot use state, effects, or browser APIs.
// UserProfile.server.jsx — runs on server, never sent to browser
async function UserProfile({ id }) {
const user = await db.users.findById(id); // Direct DB access — no API needed
return (
<div>
<h1>{user.name}</h1>
<ClientInteractiveSection userId={id} /> {/* Client component for interactivity */}
</div>
);
}The performance win: the HTML arrives with data already baked in. No loading states for the initial data, no waterfall client-side fetches. Client bundle shrinks because server component code never ships to the browser.
Current status: RSC is most mature in the Next.js App Router. It requires a framework — you cannot use RSC with plain Vite/CRA.
Q28. What is the React Compiler (formerly React Forget)?
The React Compiler is a build-time tool that automatically applies memoization equivalent to manually adding React.memo, useMemo, and useCallback everywhere it is safe to do so. It analyzes component code and inserts optimizations without requiring developer annotation.
What this means in practice: you write normal components without explicit memoization, and the compiler produces an optimized build.
Constraint: the compiler strictly enforces the Rules of React. If a component violates the rules (e.g., mutates state directly), the compiler skips that component rather than produce faulty output.
Current status: the compiler shipped as opt-in with React 19. In Next.js 15+, it can be enabled with experimental.reactCompiler: true in the Next.js config.
Part 4 — Virtual DOM, Fiber, and Reconciliation
Q29. What is the Virtual DOM and what problem does it solve?
The Virtual DOM is a lightweight JavaScript representation of the actual DOM. It is a plain object tree — cheap to create and compare.
The problem it solves: direct DOM manipulation is slow. The DOM is a complex API, and browsers must recalculate layout and repaint after many changes. If you re-rendered a full page of HTML on every state change (like early server-rendered apps did on navigation), it would be far too slow for interactive UIs.
How it works:
- 1A state change triggers React to build a new virtual DOM tree.
- 2React diffs the new tree against the previous tree (reconciliation).
- 3Only the minimal set of actual DOM mutations is applied.
This means React touches the real DOM as little as possible and batches changes for efficiency.
Q30. Explain React's diffing algorithm. What are its key heuristics?
React uses an O(n) heuristic algorithm (vs. the theoretically optimal O(n³) for arbitrary trees). It relies on two assumptions:
Heuristic 1 — Element type determines tree identity:
If the root element changes type (e.g., Heuristic 2 — Keys identify list items: Without keys, React compares list items positionally. Inserting at the beginning causes every item to appear "changed." With stable keys, React matches items by identity across renders. Why index as key is often wrong: using array index as key means React thinks item #0 is always the same item. If you delete item #0, all subsequent items shift position and appear "changed" — destroying their local state. The old Stack Reconciler (pre-React 16) used JavaScript's call stack for recursion. Rendering was synchronous and could not be interrupted. A large re-render blocked the main thread until it finished — causing dropped frames and janky UI. React Fiber (React 16+) is a complete rewrite of the reconciliation algorithm. It breaks rendering into small units of work called "fibers" — one fiber per component. The scheduler can pause, resume, prioritize, and discard these units of work at any time. Key capabilities Fiber enables: Render phase (a.k.a. reconciliation phase): Commit phase: Interview answer structure: "The render phase is pure computation — no side effects, interruptible. The commit phase is where the DOM actually changes — synchronous and uninterruptible. This separation is what lets React be concurrent." Keys help React identify which items in a list have changed, been added, or been removed between renders. A key must be unique among siblings and stable across renders. The problem with index keys in mutable lists: When index keys are acceptable: static lists that are never reordered, filtered, or have items deleted — e.g., a read-only FAQ list. Concurrent Mode is React's ability to work on multiple versions of the UI simultaneously, interrupting and prioritizing work. In React 18, it is enabled by using With | Scope | Tool | |---|---| | Single component | | A subtree (sibling components, parent-child) | Lift state / composition | | App-wide infrequent data (theme, auth, locale) | Context API | | Complex app-wide state, high update frequency | Zustand, Jotai, Redux Toolkit | | Server state (async, caching, invalidation) | TanStack Query, SWR | The key interview signal: distinguish between client state (UI state, form values) and server state (data fetched from an API). Using Redux to cache API responses was common before TanStack Query existed; in modern codebases, server state is managed by a dedicated cache library, and Redux/Zustand handle only client state. Redux is a predictable state management library based on the Flux architecture. Three principles: Interview tip: interviewers who still ask about Zustand is a minimal state management library (~1kb gzipped). You create a store as a hook, and components subscribe to slices of that store. Why developers prefer it: TanStack Query is a server-state library. It caches, synchronizes, and updates data from async sources (APIs, databases). It is "state management" in that it manages the lifecycle of remote data — not just local UI state. What it gives you for free: deduplication of concurrent requests, background refetching, cache invalidation, optimistic updates, pagination, and infinite scroll. Every consumer of a context re-renders when the context value changes — even if the consumer only uses part of the value. This happens because React cannot do partial subscriptions to a context. Fix 1 — Split contexts by update frequency: Fix 2 — Memoize the context value: Fix 3 — For high-frequency updates, use Optimistic updates immediately update the UI as if the server request succeeded, then roll back on failure. This makes the UI feel instant. RTL philosophy: "The more your tests resemble the way your software is used, the more confidence they give you." — Kent C. Dodds Tests should query the DOM the way a user does — by role, label, and visible text — not by component internals (state, instance methods, implementation details). This makes tests resilient to refactors. Enzyme (older tool) lets you inspect component state and call lifecycle methods directly. Tests break when you refactor implementation without changing behavior. RTL tests survive refactors because they only care about what the user sees. Query priority (RTL's own hierarchy): Use MSW (Mock Service Worker) is the recommended approach for mocking API calls — it intercepts at the network level, meaning your tests exercise your actual fetch code rather than mocked functions. Use Wrap state updates in Snapshot testing captures a component's rendered output and compares it to a stored snapshot file on subsequent runs. A diff appears if the output changes. When snapshots are useful: simple, stable presentational components (icons, badges, typographic elements) where visual regressions matter and the output rarely changes intentionally. When snapshots hurt: complex components with dynamic data — snapshots become large, frequently change, and developers start blindly updating them. For interactive components with behavior, prefer behavioral assertions with RTL. Wrap the component under test with the necessary providers: Discriminated unions let TypeScript narrow prop types based on a literal field, preventing impossible prop combinations. This pattern is especially powerful for button/link variants, form field modes (view/edit), and modal types. Key rule: the Promise must be created *outside* the component (or cached) — creating it inside the component would make a new Promise on every render, causing an infinite loop. React 19 introduced Actions: functions you pass to form elements via the Before Actions, you needed Batching groups multiple state updates into a single re-render. Before React 18, batching only happened inside React event handlers. Updates in If you ever need to opt out (rare), use Error boundaries catch rendering errors in their child tree and display a fallback UI. They must be class components (as of React 19, there is no hook equivalent yet). What Error Boundaries do NOT catch: In practice, most teams use the Compound components are a group of components that share implicit state through context, giving consumers a flexible, expressive API. Higher-Order Component (HOC): a function that takes a component and returns a new component with additional behavior. Custom hook: encapsulates reusable logic directly — no wrapping component. General rule: custom hooks are preferred in modern React. HOCs are still appropriate when you need to wrap a component you do not own (e.g., a third-party component) or when you need render control (adding a Suspense boundary, error boundary, or DOM wrapper). Avoid HOC nesting chains — they create "wrapper hell" in DevTools. Feature-based structure (recommended for teams): Key principles: Reconciliation: React's process of computing the minimal DOM changes needed to go from the old tree to the new one. Hydration: attaching React's event listeners and making a server-rendered HTML page interactive on the client. forwardRef: lets a parent component pass a ref down to a DOM element inside a child component. Portals: render a component's output to a different DOM node while keeping it in the React event tree. Strict Mode: development-only wrapper that double-invokes renders and effects to surface bugs caused by side effects in the render phase. Synthetic Events: React wraps native browser events in a cross-browser normalized wrapper. In React 17+, events are attached to the root element rather than Lifting State Up: when two sibling components need to share state, move that state to their nearest common ancestor and pass it down via props. React tracks hook state by call order, not by name. Rule 1: only call hooks at the top level — never inside loops, conditions, or nested functions. Rule 2: only call hooks from React function components or custom hooks. Violating Rule 1 corrupts the call-order React depends on, causing state to be assigned to the wrong hook across renders. The eslint-plugin-react-hooks package enforces both rules automatically. useMemo memoizes the *result* of a computation — it runs the function and caches the return value. useCallback memoizes the *function itself* — it returns a stable function reference without calling it. Use useMemo for expensive derived values (filtered lists, formatted data). Use useCallback when passing a function to a React.memo-wrapped child component that would otherwise re-render because the function reference changes on every parent render. The old Stack Reconciler used JavaScript's call stack recursively, making rendering synchronous and uninterruptible. A large re-render blocked the main thread entirely, causing dropped animation frames and unresponsive UI. React Fiber (React 16+) breaks rendering into small 'fiber' units of work. The scheduler can pause, resume, prioritize, and discard these units at any time. This enables time-slicing, Suspense, concurrent mode, and the ability to interrupt low-priority renders when a user interaction arrives. A controlled component derives its value from React state — every keystroke updates state, and the input always reflects that state. An uncontrolled component stores its value in the DOM itself, accessed via a ref when needed. Use controlled components for real-time validation, dependent fields, or complex form logic. Use uncontrolled components for simple cases, file inputs, or when integrating with non-React libraries. Controlled is the default recommendation because state is explicit and predictable. Zustand is preferred for most new projects because it requires zero boilerplate (no providers, no action creators, no reducers files), offers built-in selector subscriptions so components only re-render for the slice they use, works outside React for utility functions, and has a tiny bundle footprint. Redux (via Redux Toolkit) remains the better choice for very large teams who need strict architectural conventions, time-travel debugging via Redux DevTools, or are maintaining an existing Redux codebase. Neither should be used for server/async state — TanStack Query handles that better. A stale closure happens when an effect captures a variable at the time it was created and that variable later changes without the closure knowing. Two fixes: (1) Use the functional updater form for state — setCount(prev => prev + 1) always reads current state regardless of what the closure captured. (2) Use a ref to hold the latest value — update ref.current = value on every render, then read ref.current inside the effect. The effect never goes stale because refs are mutable and always reflect the latest value. Both defer low-priority renders to keep the UI responsive, but they differ in where you apply them. useTransition wraps the state *setter* at the call site — you control which updates are deferred and get an isPending boolean to show a loading indicator. useDeferredValue wraps a *value* at the consumption site — use it when you receive a value as a prop from a parent you don't control and want to defer downstream computation. If you own the setter, use useTransition. If you only receive the value, use useDeferredValue. React.memo is a higher-order component that wraps a functional component. Before rendering, it does a shallow comparison of the current and previous props. If all props are shallowly equal, it skips re-rendering and reuses the last output. It fails when a parent passes a new object or function literal on every render — shallow comparison sees a new reference and re-renders anyway. The fix is to stabilize those props with useMemo (for objects/arrays) and useCallback (for functions) in the parent component. The legacy model (ReactDOM.render) renders synchronously and cannot be interrupted. React 18 with createRoot uses concurrent rendering: React can pause, resume, and prioritize work. It enables automatic batching (multiple state updates across event handlers, timeouts, and Promises are batched into one re-render), startTransition (marks updates as non-urgent so they can be interrupted), useDeferredValue, Suspense on data fetching, and streaming SSR. The upgrade path is simply replacing ReactDOM.render with createRoot — existing code continues working. RTL encourages querying the DOM the way users and assistive technologies do. The priority is: getByRole (most preferred — mirrors screen reader traversal and enforces accessible markup), then getByLabelText (form fields), getByPlaceholderText, getByText, getByDisplayValue, and finally getByTestId (last resort). Using getByRole forces you to write accessible HTML because it relies on ARIA roles. Tests that use getByTestId are coupled to implementation (data-testid attributes) rather than user-visible behavior, making them less meaningful and more brittle. How to Answer Conflict-With-a-Coworker Interview Questions Learn how to answer conflict-with-a-coworker interview questions with real examples and proven techniques. Stand out in tech and remote job interviews. How to Answer 'Why Do You Want to Work Here' in Interviews Discover expert strategies for answering 'why do you want to work here,' tailored for remote tech roles and dollar opportunities. Real, practical interview tips. Frontend Developer Interview Questions and How to Answer Them (50+) Complete SEO article covering 54 frontend developer interview questions with detailed answers, real code snippets across HTML, CSS, JavaScript, React, TypeScript, accessibility, security, build tools, and testing. Full-Stack Developer Interview Questions: How to Answer Like a Pro (45+) Comprehensive full-stack developer interview guide with 46 numbered questions covering JavaScript/TypeScript, React, CSS, REST APIs, databases, Node.js, system design, security, testing, DevOps, and advanced architecture topics. Each answer includes working code examples and production-level context. Paste your job link: we research who's interviewing you and rehearse you live. Have an interview coming up? Install the live copilot →), React tears down the entire subtree and builds a fresh one, including resetting all state.// Before
<div><Counter /></div>
// After — Counter is unmounted and remounted because root type changed
<span><Counter /></span>// Without keys — inefficient (all items re-render on insert at front)
items.map(item => <Item name={item.name} />)
// With keys — React tracks items by ID, only creates the new one
items.map(item => <Item key={item.id} name={item.name} />)Q31. What is React Fiber and how is it different from the old Stack Reconciler?
Q32. Describe Fiber's render phase and commit phase.
before mutation (snapshot effects), mutation (DOM updates), layout (useLayoutEffect fires here).useEffect runs asynchronously after the browser has painted (after the commit phase).Q33. What are keys in React and why is using array index as a key often a mistake?
// Items: [ {id: 1, name: "Alice"}, {id: 2, name: "Bob"} ]
// Rendered with index keys: key=0 → Alice, key=1 → Bob
// After deleting Alice:
// Items: [ {id: 2, name: "Bob"} ]
// React sees: key=0 changed from "Alice" to "Bob" — updates it
// Result: correct text, but Bob's component instance was "Alice's" — local state is wrong
// With stable ID keys: key=1 → Alice (removed), key=2 → Bob (unchanged)
// React unmounts Alice's component, Bob's component is untouchedQ34. What is Concurrent Mode and how does createRoot enable it?
createRoot instead of the legacy ReactDOM.render.// Legacy mode (React 17 and below)
import ReactDOM from "react-dom";
ReactDOM.render(<App />, document.getElementById("root"));
// Concurrent mode (React 18+)
import { createRoot } from "react-dom/client";
const root = createRoot(document.getElementById("root"));
root.render(<App />);createRoot, you get automatic batching (multiple state updates in event handlers, timeouts, and Promises are batched into a single re-render), startTransition, useDeferredValue, Suspense on data fetching, and the streaming SSR improvements.Part 5 — State Management
Q35. When would you use local state vs Context API vs an external library?
useState or useReducer |Q36. What is Redux and what are its three core principles?
(state, action) => newState. No side effects inside a reducer.// Redux Toolkit (modern Redux — the boilerplate-reduced version)
import { createSlice, configureStore } from "@reduxjs/toolkit";
const cartSlice = createSlice({
name: "cart",
initialState: { items: [] },
reducers: {
addItem: (state, action) => {
state.items.push(action.payload); // Immer handles immutability
},
removeItem: (state, action) => {
state.items = state.items.filter(item => item.id !== action.payload);
},
},
});
const store = configureStore({ reducer: { cart: cartSlice.reducer } });
// In a component
const items = useSelector(state => state.cart.items);
dispatch(cartSlice.actions.addItem({ id: 1, name: "Widget" }));combineReducers and connect() are working with legacy code. Mention Redux Toolkit and useSelector/useDispatch as the current standard.Q37. How does Zustand work and why do developers prefer it for new projects?
import { create } from "zustand";
const useCartStore = create((set) => ({
items: [],
total: 0,
addItem: (item) =>
set((state) => ({
items: [...state.items, item],
total: state.total + item.price,
})),
removeItem: (id) =>
set((state) => {
const removed = state.items.find(i => i.id === id);
return {
items: state.items.filter(i => i.id !== id),
total: state.total - (removed?.price ?? 0),
};
}),
}));
// In a component — only re-renders when `items` changes, not `total`
function CartList() {
const items = useCartStore(state => state.items);
return <ul>{items.map(i => <li key={i.id}>{i.name}</li>)}</ul>;
}Q38. What is TanStack Query (React Query) and why is it considered a state management tool?
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
function Users() {
const { data, isLoading, error } = useQuery({
queryKey: ["users"],
queryFn: () => fetch("/api/users").then(r => r.json()),
staleTime: 5 * 60 * 1000, // Consider fresh for 5 minutes
});
if (isLoading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <UserList users={data} />;
}
function CreateUser() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newUser) => fetch("/api/users", { method: "POST", body: JSON.stringify(newUser) }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["users"] }),
});
return <button onClick={() => mutation.mutate({ name: "New User" })}>Add User</button>;
}Q39. Explain the Context API performance problem and how to fix it.
// PROBLEM — one context with everything
const AppContext = createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState("light");
// Every re-render of AppProvider creates a new object reference
// All consumers re-render on every user OR theme change
return (
<AppContext.Provider value={{ user, setUser, theme, setTheme }}>
{children}
</AppContext.Provider>
);
}const UserContext = createContext();
const ThemeContext = createContext();
// Components that only need theme don't re-render when user changesconst value = useMemo(() => ({ user, setUser }), [user]);
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;useSyncExternalStore or reach for Zustand, which handles subscriptions at a per-selector level.Q40. How do you handle optimistic updates in a React application?
function TodoList() {
const queryClient = useQueryClient();
const toggleMutation = useMutation({
mutationFn: (todo) =>
fetch(`/api/todos/${todo.id}`, {
method: "PATCH",
body: JSON.stringify({ done: !todo.done }),
}),
onMutate: async (todo) => {
await queryClient.cancelQueries({ queryKey: ["todos"] });
const previous = queryClient.getQueryData(["todos"]);
queryClient.setQueryData(["todos"], (old) =>
old.map(t => t.id === todo.id ? { ...t, done: !t.done } : t)
);
return { previous };
},
onError: (err, todo, context) => {
queryClient.setQueryData(["todos"], context.previous);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});
}Part 6 — Testing
Q41. What is the React Testing Library philosophy and how does it differ from Enzyme?
getByRole — most accessible, mirrors screen reader traversalgetByLabelText — for form fieldsgetByPlaceholderTextgetByTextgetByDisplayValuegetByTestIdQ42. How do you test async behavior in React components?
findBy* queries (which return a Promise) or waitFor for state changes that happen after async operations.import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { server } from "../mocks/server"; // MSW server
import { rest } from "msw";
import UserProfile from "./UserProfile";
test("displays user name after loading", async () => {
render(<UserProfile id={1} />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
expect(await screen.findByText("Alice Johnson")).toBeInTheDocument();
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});
test("shows error on fetch failure", async () => {
server.use(
rest.get("/api/users/1", (req, res, ctx) => res(ctx.status(500)))
);
render(<UserProfile id={1} />);
expect(await screen.findByText(/something went wrong/i)).toBeInTheDocument();
});Q43. How do you test a custom hook?
renderHook from @testing-library/react:import { renderHook, act } from "@testing-library/react";
import { useCounter } from "./useCounter";
test("increments counter", () => {
const { result } = renderHook(() => useCounter(0));
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
test("useFetch returns data", async () => {
const { result } = renderHook(() => useFetch("/api/users"));
expect(result.current.loading).toBe(true);
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
expect(result.current.data).toHaveLength(3);
});act() to ensure React processes all state updates and effects before assertions.Q44. What is snapshot testing and when should you use it?
import { render } from "@testing-library/react";
import Button from "./Button";
test("matches snapshot", () => {
const { container } = render(<Button variant="primary">Save</Button>);
expect(container.firstChild).toMatchSnapshot();
});Q45. How do you test components that use Context or Redux?
function renderWithProviders(ui, { preloadedState = {}, ...renderOptions } = {}) {
const store = configureStore({
reducer: rootReducer,
preloadedState,
});
function Wrapper({ children }) {
return (
<Provider store={store}>
<ThemeProvider theme="light">
{children}
</ThemeProvider>
</Provider>
);
}
return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) };
}
// In tests
test("shows correct item count from store", () => {
const { getByText } = renderWithProviders(<CartSummary />, {
preloadedState: { cart: { items: [{ id: 1 }, { id: 2 }] } },
});
expect(getByText("2 items")).toBeInTheDocument();
});Part 7 — TypeScript with React
Q46. How do you type component props in TypeScript?
interface ButtonProps {
label: string;
variant?: "primary" | "secondary" | "danger";
disabled?: boolean;
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
}
function Button({ label, variant = "primary", disabled = false, onClick }: ButtonProps) {
return (
<button
className={`btn btn-${variant}`}
disabled={disabled}
onClick={onClick}
>
{label}
</button>
);
}
// Typing children explicitly (React 18+ removed implicit children from FC)
interface CardProps {
title: string;
children: React.ReactNode;
}
// Extending HTML element props
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
error?: string;
}Q47. How do you type useState, useRef, and event handlers in TypeScript?
// useState
const [count, setCount] = useState(0); // inferred: number
const [user, setUser] = useState<User | null>(null);
// useRef for a DOM element
const inputRef = useRef<HTMLInputElement>(null);
// Access: inputRef.current?.focus()
// useRef for a mutable value
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
// Event handlers
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
setValue(event.target.value);
}
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
}
function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
// ...
}Q48. What are discriminated unions and how do they help type React components?
type FieldProps =
| {
mode: "editable";
value: string;
onChange: (v: string) => void;
}
| {
mode: "readonly";
value: string;
};
function Field(props: FieldProps) {
if (props.mode === "editable") {
return <input value={props.value} onChange={(e) => props.onChange(e.target.value)} />;
}
return <span>{props.value}</span>;
}Part 8 — Concurrent Features and React 18/19
Q49. What is Suspense and how does it work for data fetching?
Suspense lets a component "suspend" (pause rendering) while it waits for something to be ready. React shows the nearest fallback until the suspended work completes.// React 19 — use() hook reads a Promise, suspending until resolved
import { use, Suspense } from "react";
function UserProfile({ userPromise }) {
const user = use(userPromise); // Suspends while promise is pending
return <div>{user.name}</div>;
}
function App() {
const userPromise = fetchUser(1); // Kicked off at render time
return (
<Suspense fallback={<Skeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}Q50. What are React 19 Actions and how do they simplify form handling?
action prop. React manages the pending/error state for you.import { useActionState } from "react";
async function submitForm(prevState, formData) {
const name = formData.get("name");
try {
await api.createUser({ name });
return { success: true, error: null };
} catch (err) {
return { success: false, error: err.message };
}
}
function CreateUserForm() {
const [state, action, isPending] = useActionState(submitForm, {
success: false,
error: null,
});
return (
<form action={action}>
<input name="name" required />
<button disabled={isPending}>
{isPending ? "Creating..." : "Create User"}
</button>
{state.error && <p className="error">{state.error}</p>}
{state.success && <p className="success">User created!</p>}
</form>
);
}useState for loading, useState for error, a manual try/catch, and an onSubmit handler. Actions collapse all of that into a single pattern.Q51. What is Automatic Batching in React 18 and why does it matter?
setTimeout, Promises, or native event listeners triggered separate re-renders for each setState call.// React 17 — 2 re-renders (updates inside setTimeout were NOT batched)
setTimeout(() => {
setCount(c => c + 1); // re-render
setFlag(f => !f); // re-render
}, 1000);
// React 18 with createRoot — 1 re-render (automatic batching everywhere)
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React batches these into one re-render automatically
}, 1000);ReactDOM.flushSync():import { flushSync } from "react-dom";
flushSync(() => setCount(c => c + 1)); // Forces immediate re-render
flushSync(() => setFlag(f => !f)); // Forces another immediate re-renderQ52. What is Error Boundary and how do you implement one in 2025?
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, info) {
logError(error, info.componentStack);
}
render() {
if (this.state.hasError) {
return this.props.fallback ?? <p>Something went wrong.</p>;
}
return this.props.children;
}
}
// Usage
<ErrorBoundary fallback={<ErrorPage />}>
<Dashboard />
</ErrorBoundary>react-error-boundary package which provides a cleaner functional API including useErrorBoundary for triggering boundaries from event handlers.Part 9 — Architecture and Patterns
Q53. What is the compound component pattern?
const TabsContext = createContext();
function Tabs({ children, defaultTab }) {
const [active, setActive] = useState(defaultTab);
return (
<TabsContext.Provider value={{ active, setActive }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
function TabList({ children }) {
return <div role="tablist">{children}</div>;
}
function Tab({ id, children }) {
const { active, setActive } = useContext(TabsContext);
return (
<button
role="tab"
aria-selected={active === id}
onClick={() => setActive(id)}
>
{children}
</button>
);
}
function TabPanel({ id, children }) {
const { active } = useContext(TabsContext);
return active === id ? <div role="tabpanel">{children}</div> : null;
}
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;
// Consumer API
<Tabs defaultTab="profile">
<Tabs.List>
<Tabs.Tab id="profile">Profile</Tabs.Tab>
<Tabs.Tab id="settings">Settings</Tabs.Tab>
</Tabs.List>
<Tabs.Panel id="profile"><ProfileForm /></Tabs.Panel>
<Tabs.Panel id="settings"><SettingsForm /></Tabs.Panel>
</Tabs>Q54. When would you use a Higher-Order Component vs a custom hook?
function withAuth(WrappedComponent) {
return function AuthenticatedComponent(props) {
const { user } = useAuth();
if (!user) return <Redirect to="/login" />;
return <WrappedComponent {...props} user={user} />;
};
}function useAuth() {
const user = useContext(AuthContext);
if (!user) throw new Error("Must be used inside AuthProvider");
return user;
}Q55. How do you structure a large React codebase?
src/
features/
auth/
components/
LoginForm.tsx
AuthGuard.tsx
hooks/
useAuth.ts
store/
authSlice.ts
api/
authApi.ts
index.ts ← public interface
shared/
components/ ← Button, Modal, Input
hooks/ ← useDebounce, useLocalStorage
utils/ ← formatDate, validators
types/ ← shared TypeScript interfaces
pages/ ← thin pages that compose features
app/ ← routing, providers, global configindex.ts barrel.shared/ and has no dependencies on any feature.Rapid-Fire Concepts Glossary
document.How to Use This Guide in Your Prep
use hook.FAQ
What are the Rules of Hooks and why does React enforce them?+
What is the difference between useMemo and useCallback?+
What is React Fiber and why was it rewritten from the Stack Reconciler?+
What is the difference between controlled and uncontrolled components?+
When would you choose Zustand over Redux for state management?+
How do you prevent stale closures in useEffect?+
What is the difference between useTransition and useDeferredValue?+
What does React.memo do and when does it fail to prevent re-renders?+
How does Concurrent Mode in React 18 differ from the legacy rendering model?+
What is the React Testing Library's query priority and why does it matter?+
Related articles
Prepare for your real interview
