React JS Architecture: React JS Scenario Based questions and answers
System Design, Enterprise State, Advanced Rendering, and Scalable Architectures. Zero code, pure architectural strategy.
Micro-Frontends, Enterprise Testing, Strangler Migrations, and Edge Computing.
1. Enterprise State Architecture
2. Advanced Rendering & Performance Strategy
3. Design Systems & Component Architecture
4. Build, Bundling, and Delivery
5. Resiliency, Security, and Observability
6. Micro-Frontends & Monorepo Scaling
7. Server-Side Rendering (SSR) & Edge Architecture
8. Legacy Migration & The Strangler Pattern
9. Enterprise QA, Security & Observability
React JS Interviw Question: The codeing challenge and machine round
100 Comprehensive React JS Interview Questions & Answers. Master everything from Beginner hooks to Expert-level React JS coding challenge and machine round interview.
Beginner Level
React is an open-source JavaScript library created by Facebook for building user interfaces, particularly single-page applications. Its core features are:
- Component-based architecture — UI is split into reusable, self-contained pieces.
- Virtual DOM — React keeps a lightweight in-memory copy of the real DOM and only updates what changed, boosting performance.
- JSX — A syntax extension that lets you write HTML-like code inside JavaScript.
- Unidirectional data flow — Data flows from parent to child via props, making apps easier to debug.
- Hooks — Functions like
useStateanduseEffectthat add state and lifecycle behavior to functional components.
JSX (JavaScript XML) is a syntax extension that allows you to write HTML-like markup inside JavaScript files. It is not valid JavaScript — Babel transpiles it into React.createElement() calls at build time.
// JSX
const element = <h1 className="title">Hello, World!</h1>;
// What Babel compiles it to
const element = React.createElement('h1', { className: 'title' }, 'Hello, World!');JSX makes component code more readable and easier to reason about compared to chained createElement calls.
// Class Component
class Greeting extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
// Functional Component (preferred)
function Greeting({ name }) {
return <h1>Hello, {name}</h1>;
}Functional components are simpler, use less boilerplate, and since React 16.8 can use Hooks for state and side-effects. Class components require this, lifecycle methods, and are generally more verbose. New code should prefer functional components.
Props (short for properties) are read-only inputs passed from a parent component to a child component. They make components reusable by letting the parent control child behavior/appearance.
function Button({ label, color }) {
return <button style={{ background: color }}>{label}</button>;
}
// Usage
<Button label="Submit" color="blue" />Props are immutable inside the child — the child must never modify them directly.
State is mutable data managed inside a component. When state changes, the component re-renders. Props are immutable data passed from outside.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}Think of props as arguments to a function and state as local variables that persist across renders.
The Virtual DOM is a lightweight JavaScript object tree that mirrors the real DOM. When state or props change, React:
- Renders a new Virtual DOM tree.
- Diffs it against the previous tree (reconciliation).
- Computes the minimal set of real DOM mutations.
- Applies only those changes to the actual browser DOM.
This batched, minimal-update strategy is far faster than naive full-page re-renders.
useState work? Give an example.useState is a Hook that adds local state to a functional component. It returns a tuple: the current value and a setter function.
import { useState } from 'react';
function Toggle() {
const [isOn, setIsOn] = useState(false);
return (
<button onClick={() => setIsOn(prev => !prev)}>
{isOn ? 'ON' : 'OFF'}
</button>
);
}The functional updater form prev => !prev is preferred when the new value depends on the old one, because React may batch state updates.
useEffect and when do you use it?useEffect lets you perform side-effects (data fetching, subscriptions, DOM mutations, timers) after render. It runs after the component renders and optionally cleans up before re-running.
import { useState, useEffect } from 'react';
function UserCard({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(setUser);
// cleanup (runs before next effect or unmount)
return () => setUser(null);
}, [userId]); // re-runs only when userId changes
if (!user) return <p>Loading...</p>;
return <p>{user.name}</p>;
}Hooks are functions that let functional components tap into React features that were previously only available in class components.
useState— local component stateuseEffect— side-effects and lifecycleuseContext— consume a React contextuseRef— mutable ref object / DOM accessuseMemo— memoize expensive computed valuesuseCallback— memoize callback functionsuseReducer— complex state with reducer pattern
Rules of Hooks: only call at the top level, only call inside React functions — never inside conditionals or loops.
key prop in lists?The key prop helps React identify which items in a list have changed, been added, or removed during reconciliation. Keys must be stable, unique among siblings, and ideally come from your data (e.g., database IDs).
const items = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
function List() {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li> // ✅ stable, unique
))}
</ul>
);
}Avoid using array index as key when the list can be reordered — this causes subtle re-render bugs.
Conditional rendering lets you show or hide UI based on state or props. Common patterns:
function Alert({ isError, message }) {
// if/else
if (isError) return <div className="error">{message}</div>;
// ternary
return <div>{message ? message : 'No messages'}</div>;
// short-circuit &&
return <div>{message && <span>{message}</span>}</div>;
}React events use camelCase names and receive a SyntheticEvent — a cross-browser wrapper around the native event.
function Form() {
function handleSubmit(e) {
e.preventDefault(); // prevent page reload
console.log('submitted');
}
return (
<form onSubmit={handleSubmit}>
<input onChange={e => console.log(e.target.value)} />
<button type="submit">Send</button>
</form>
);
}React.Fragment and why is it useful?Fragments let you group multiple elements without adding an extra DOM node. This avoids invalid HTML (e.g., a <tr> inside a <div>) and keeps the DOM clean.
// Short syntax
function Columns() {
return (
<>
<td>Name</td>
<td>Age</td>
</>
);
}
// With key (must use long form)
items.map(item => (
<React.Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.def}</dd>
</React.Fragment>
))useRef? Give two use cases.useRef returns a mutable object { current: value } that persists across renders without causing re-renders when changed.
// Use case 1: access a DOM node
function FocusInput() {
const inputRef = useRef(null);
return (
<>
<input ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>Focus</button>
</>
);
}
// Use case 2: store a mutable value (e.g. previous state)
function Timer() {
const timerIdRef = useRef(null);
const start = () => { timerIdRef.current = setInterval(tick, 1000); };
const stop = () => clearInterval(timerIdRef.current);
// ...
}Prop drilling happens when you must pass data through many intermediate components just to reach a deeply nested consumer that actually needs it.
// theme has to travel A → B → C even though B doesn't use it
<A theme="dark" />
<B theme={theme} /> // B just passes it down
<C theme={theme} /> // C actually uses itProblems: tight coupling, verbose code, hard to refactor. Solutions include React Context, Redux, Zustand, or component composition.
Context provides a way to share values (theme, auth, locale) across the component tree without prop drilling.
const ThemeContext = React.createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
const theme = useContext(ThemeContext);
return <div className={theme}>Toolbar</div>;
}Context should be used for low-frequency global data. For high-frequency updates, prefer a state manager to avoid unnecessary re-renders.
When two sibling components need to share state, you lift the state to their closest common ancestor and pass it down as props.
function Parent() {
const [value, setValue] = useState('');
return (
<>
<Input value={value} onChange={setValue} />
<Display value={value} />
</>
);
}
function Input({ value, onChange }) {
return <input value={value} onChange={e => onChange(e.target.value)} />;
}
function Display({ value }) {
return <p>{value}</p>;
}A controlled component has its form input value driven by React state. A uncontrolled component manages its own state internally via the DOM; you read the value with a ref.
// Controlled
const [text, setText] = useState('');
<input value={text} onChange={e => setText(e.target.value)} />
// Uncontrolled
const inputRef = useRef();
<input ref={inputRef} defaultValue="hello" />
// read: inputRef.current.valueControlled components give you full control over validation and transformations on every keystroke. Uncontrolled components are simpler for basic forms and integrating with non-React code.
React.StrictMode?StrictMode is a developer tool that helps you spot potential problems. It intentionally double-invokes render functions, state initializers, and effects (in development) to surface side-effects written incorrectly. It has no effect in production builds.
<React.StrictMode>
<App />
</React.StrictMode>Warnings it catches: deprecated API usage, impure render side-effects, unexpected re-render issues, and missing cleanup in effects.
State must be treated as immutable. Always return a new object/array instead of mutating the existing one.
// ❌ Wrong — mutates directly
state.user.name = 'Alice'; setState(state);
// ✅ Correct — spread into new object
setState(prev => ({ ...prev, user: { ...prev.user, name: 'Alice' } }));
// ✅ Array: add item
setItems(prev => [...prev, newItem]);
// ✅ Array: remove item
setItems(prev => prev.filter(item => item.id !== targetId));
// ✅ Array: update item
setItems(prev => prev.map(item => item.id === targetId ? { ...item, done: true } : item));null and undefined rendering in JSX?Both null, undefined, and false render nothing — they are valid children that produce no DOM output. This makes them ideal for conditional rendering.
function Component({ show }) {
return (
<div>
{show && <p>Visible!</p>} // nothing rendered when show=false
{null} // nothing rendered
{0} // ⚠️ renders "0"! be careful
</div>
);
}Note: the number 0 does render — a common footgun when using count && <Comp />.
// Modern: destructuring defaults
function Button({ label = 'Click me', color = 'blue' }) {
return <button style={{ color }}>{label}</button>;
}
// Legacy: static property
Button.defaultProps = { label: 'Click me', color: 'blue' };Destructuring defaults are preferred in modern React since defaultProps may be removed in a future major version.
children prop and how is it used?The children prop contains everything placed between the component’s opening and closing tags, enabling composable wrapper components.
function Card({ children, title }) {
return (
<div className="card">
<h2>{title}</h2>
{children}
</div>
);
}
// Usage
<Card title="Hello">
<p>I am a child!</p>
</Card>In React, the style attribute accepts a JavaScript object with camelCased property names and string values (not a CSS string).
const styles = {
backgroundColor: '#0d1117',
fontSize: '16px',
marginTop: 8, // numbers default to px
fontWeight: 'bold',
};
<div style={styles}>Styled</div>
// or inline:
<div style={{ color: 'red' }}>Red</div>setState multiple times in a row?React batches multiple state updates in event handlers (and in React 18+, everywhere including async code) into a single re-render for performance.
function Component() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
function handleClick() {
setA(1); // batched
setB(2); // batched
// → only ONE re-render occurs
}
}If you need the current state value based on the previous update, use the functional updater form: setCount(prev => prev + 1).
Intermediate Level
useReducer and when should you use it over useState?useReducer is ideal when state transitions depend on the previous state and multiple sub-values change together, or when the next state logic is complex.
const initialState = { count: 0, step: 1 };
function reducer(state, action) {
switch (action.type) {
case 'increment': return { ...state, count: state.count + state.step };
case 'setStep': return { ...state, step: action.payload };
default: throw new Error('Unknown action');
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
<p>{state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
</>
);
}useMemo and when should you use it?useMemo memoizes the result of an expensive computation, recomputing it only when dependencies change. It prevents unnecessary recalculations on every render.
import { useMemo } from 'react';
function ProductList({ products, filter }) {
const filtered = useMemo(
() => products.filter(p => p.category === filter),
[products, filter] // only recompute when these change
);
return filtered.map(p => <ProductCard key={p.id} product={p} />);
}Don’t over-optimize — only use useMemo when a profiler shows a real bottleneck. The memoization itself has overhead.
useCallback and how does it differ from useMemo?useCallback(fn, deps) memoizes a function reference. It’s equivalent to useMemo(() => fn, deps). Use it when passing callbacks to memoized child components to prevent unnecessary re-renders.
const handleClick = useCallback(() => {
doSomethingWith(id);
}, [id]); // stable reference unless id changes
// useMemo — memoizes a VALUE
const total = useMemo(() => items.reduce((s, i) => s + i.price, 0), [items]);
// useCallback — memoizes a FUNCTION
const getTotal = useCallback(() => items.reduce((s, i) => s + i.price, 0), [items]);React.memo? How does it work?React.memo is a higher-order component that memoizes a functional component. It skips re-rendering if props haven’t changed (shallow comparison).
const ExpensiveChild = React.memo(function({ value }) {
console.log('rendered');
return <div>{value}</div>;
});
// Custom comparator
const MemoComp = React.memo(Comp, (prev, next) => {
return prev.id === next.id; // return true → skip re-render
});Works best when paired with useCallback/useMemo for stable prop references.
A custom Hook is a function whose name starts with use and that calls other Hooks. It extracts reusable stateful logic from components.
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(url)
.then(r => r.json())
.then(d => { if (!cancelled) setData(d); })
.catch(e => { if (!cancelled) setError(e); })
.finally(() => { if (!cancelled) setLoading(false); });
return () => { cancelled = true; };
}, [url]);
return { data, loading, error };
}
// Usage
const { data, loading } = useFetch('/api/users');Reconciliation is the algorithm React uses to diff the new Virtual DOM tree against the previous one and determine the minimal set of real DOM changes needed.
Key heuristics:
- Elements of different types produce entirely different trees (full subtree rebuild).
- The developer can hint stable identity with the
keyprop. - Same type → React updates props in place, keeping DOM node and children.
React 18 uses the Fiber architecture which makes reconciliation interruptible, enabling concurrent features like Suspense and transitions.
Fiber is React’s internal reconciliation engine (introduced in React 16). It reimplements the reconciler using a linked list of “fiber” units of work, allowing React to pause, resume, abort, and prioritize rendering work.
This enables:
- Concurrent rendering — interruptible renders that keep the UI responsive.
- Suspense & lazy loading — pause rendering until async data or components are ready.
- startTransition — mark non-urgent state updates so urgent updates (typing) stay fast.
React.lazy and Suspense? Write an example.React.lazy lets you code-split a component into a separate bundle loaded on demand. Suspense shows a fallback while it loads.
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./Dashboard'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<Dashboard />
</Suspense>
);
}The browser only downloads the Dashboard bundle when it’s first rendered. Useful for route-level code splitting.
Error Boundaries are class components that catch JavaScript errors in their child tree and display a fallback UI instead of crashing the whole app. They must implement static getDerivedStateFromError or componentDidCatch.
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
logErrorToService(error, info.componentStack);
}
render() {
if (this.state.hasError)
return <h2>Something went wrong.</h2>;
return this.props.children;
}
}Note: Error boundaries don’t catch errors in event handlers or async code — use regular try/catch for those.
useLayoutEffect Hook? How is it different from useEffect?useLayoutEffect fires synchronously after all DOM mutations but before the browser paints. useEffect fires after the paint.
useLayoutEffect(() => {
// Measure DOM, synchronously update layout
const rect = ref.current.getBoundingClientRect();
setWidth(rect.width);
}); // no flicker — runs before browser paintUse useLayoutEffect when you need to read layout from the DOM and synchronously re-render to prevent visual flicker (e.g., tooltips, measuring elements). For everything else, prefer useEffect to avoid blocking the paint.
Portals render children into a DOM node that exists outside the parent component’s DOM hierarchy, while still keeping them in the React component tree (events bubble normally).
import { createPortal } from 'react-dom';
function Modal({ children }) {
return createPortal(
<div className="modal">{children}</div>,
document.getElementById('modal-root') // DOM outside app root
);
}Common use cases: modals, tooltips, dropdowns — anything that needs to visually escape overflow-hidden or z-index constraints of its parent.
forwardRef and why is it needed?By default, ref cannot be passed as a prop to a functional component. forwardRef lets you expose a ref from a parent to a DOM node inside the child.
const Input = React.forwardRef((props, ref) => (
<input ref={ref} {...props} />
));
function Parent() {
const inputRef = useRef();
return <Input ref={inputRef} />; // ref reaches the <input> DOM node
}Commonly used in design system libraries to give consumers direct DOM access while keeping internal implementation details abstracted.
useEffect with no deps, empty array [], and dependencies?// No dependency array → runs after EVERY render
useEffect(() => { console.log('every render'); });
// Empty array [] → runs ONCE after mount
useEffect(() => { console.log('mounted'); }, []);
// With deps → runs on mount AND when any dep changes
useEffect(() => {
console.log('userId changed');
}, [userId]);The cleanup function returned from useEffect runs before the next effect execution or on unmount — in all three cases.
function LoginForm() {
const [fields, setFields] = useState({ email: '', password: '' });
const [errors, setErrors] = useState({});
function validate() {
const e = {};
if (!fields.email.includes('@')) e.email = 'Invalid email';
if (fields.password.length < 8) e.password = 'Min 8 chars';
return e;
}
function handleSubmit(e) {
e.preventDefault();
const e2 = validate();
if (Object.keys(e2).length) { setErrors(e2); return; }
submitToServer(fields);
}
const change = field => e =>
setFields(prev => ({ ...prev, [field]: e.target.value }));
return (
<form onSubmit={handleSubmit}>
<input value={fields.email} onChange={change('email')} />
{errors.email && <span>{errors.email}</span>}
<input type="password" value={fields.password} onChange={change('password')} />
{errors.password && <span>{errors.password}</span>}
<button type="submit">Login</button>
</form>
);
}The render props pattern involves a component that accepts a function as a prop (or as children), and calls it to determine what to render, sharing logic without inheritance or HOCs.
function MouseTracker({ render }) {
const [pos, setPos] = useState({ x: 0, y: 0 });
return (
<div onMouseMove={e => setPos({ x: e.clientX, y: e.clientY })}>
{render(pos)}
</div>
);
}
// Usage
<MouseTracker render={({ x, y }) => <p>{x}, {y}</p>} />Hooks have largely replaced render props for sharing logic, but the pattern is still common in libraries like React Router and Formik.
A HOC is a function that takes a component and returns an enhanced component. It’s a compositional pattern for cross-cutting concerns (auth, logging, theming).
function withAuth(WrappedComponent) {
return function AuthGuard(props) {
const { isLoggedIn } = useAuth();
if (!isLoggedIn) return <Redirect to="/login" />;
return <WrappedComponent {...props} />;
};
}
const ProtectedDashboard = withAuth(Dashboard);HOCs should not mutate the wrapped component. Use a display name (AuthGuard.displayName) for better DevTools debugging.
Combining Context and useReducer gives you a lightweight Redux-like global store without external libraries.
const StoreContext = createContext();
function storeReducer(state, action) {
switch (action.type) {
case 'LOGIN': return { ...state, user: action.payload };
case 'LOGOUT': return { ...state, user: null };
default: return state;
}
}
export function StoreProvider({ children }) {
const [state, dispatch] = useReducer(storeReducer, { user: null });
return (
<StoreContext.Provider value={{ state, dispatch }}>
{children}
</StoreContext.Provider>
);
}
export const useStore = () => useContext(StoreContext);useImperativeHandle and when do you use it?useImperativeHandle customizes the instance value exposed to parent refs via forwardRef, allowing you to expose only a limited API instead of the raw DOM node.
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
clear: () => { inputRef.current.value = ''; },
// DOM node itself is NOT exposed
}));
return <input ref={inputRef} />;
});
// Parent can call: ref.current.focus() or ref.current.clear()import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/user/42">User</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/user/:id" element={<UserProfile />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}function PostList() {
const [posts, setPosts] = useState([]);
const [status, setStatus] = useState('idle'); // idle|loading|success|error
const [error, setError] = useState(null);
useEffect(() => {
setStatus('loading');
fetch('/api/posts')
.then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); })
.then(data => { setPosts(data); setStatus('success'); })
.catch(err => { setError(err.message); setStatus('error'); });
}, []);
if (status === 'loading') return <Spinner />;
if (status === 'error') return <p>Error: {error}</p>;
return posts.map(p => <Post key={p.id} post={p} />);
}React.cloneElement and children props?React.cloneElement lets you clone a React element and inject additional props or override existing ones, useful in compound component patterns.
function Tabs({ children, activeTab }) {
return (
<div>
{React.Children.map(children, child =>
React.cloneElement(child, {
isActive: child.props.id === activeTab
})
)}
</div>
);
}
// Each Tab child now receives isActive without the parent knowing its internalsModern alternative: use Context to share state inside compound components without cloneElement.
function Search() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
if (!query) { setResults([]); return; }
const timer = setTimeout(() => {
fetchResults(query).then(setResults);
}, 300); // 300ms debounce
return () => clearTimeout(timer); // cancel if query changes
}, [query]);
return (
<>
<input value={query} onChange={e => setQuery(e.target.value)} />
{results.map(r => <div key={r.id}>{r.title}</div>)}
</>
);
}React DevTools is a browser extension that adds a Components and Profiler panel to browser DevTools.
- Components panel — inspect the component tree, view props/state/hooks, and highlight re-renders.
- Profiler panel — record a session, then see which components rendered, how long each took (in ms), and why they re-rendered. Flame chart shows the render waterfall.
Workflow: Record → interact with the app → stop recording → look for unexpectedly frequent or slow renders → apply memo, useCallback, or structural fixes as needed.
Compound components are a set of components that work together and share implicit state via Context. The parent manages state; children can access it without explicit props.
const AccordionContext = createContext();
function Accordion({ children }) {
const [open, setOpen] = useState(null);
return (
<AccordionContext.Provider value={{ open, setOpen }}>
<div>{children}</div>
</AccordionContext.Provider>
);
}
function Item({ id, children }) {
const { open, setOpen } = useContext(AccordionContext);
return (
<div>
<button onClick={() => setOpen(open === id ? null : id)}>Toggle</button>
{open === id && children}
</div>
);
}
Accordion.Item = Item;
// Usage: <Accordion><Accordion.Item id="a">...</Accordion.Item></Accordion>function InfiniteList() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const sentinelRef = useRef();
useEffect(() => {
fetchPage(page).then(data => setItems(prev => [...prev, ...data]));
}, [page]);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) setPage(p => p + 1);
});
if (sentinelRef.current) observer.observe(sentinelRef.current);
return () => observer.disconnect();
}, []);
return (
<>
{items.map(i => <Item key={i.id} data={i} />)}
<div ref={sentinelRef} /> // invisible bottom sentinel
</>
);
}Code splitting breaks your bundle into smaller chunks loaded on demand, reducing initial bundle size and TTI (Time to Interactive).
- Component-level:
React.lazy+ dynamicimport() - Route-level: Lazy-load each route component
- Library-level: Webpack/Vite automatically split node_modules
// Route-level splitting
const Home = lazy(() => import('./routes/Home'));
const Profile = lazy(() => import('./routes/Profile'));
<Suspense fallback={<Spinner/>}>
<Routes>
<Route path="/" element={<Home/>} />
<Route path="/profile" element={<Profile/>} />
</Routes>
</Suspense>React supports full HTML accessibility attributes with camelCase naming. Key practices:
// aria-* attributes stay hyphenated
<button aria-label="Close modal" aria-expanded={isOpen}>✕</button>
// for/htmlFor association
<label htmlFor="email">Email</label>
<input id="email" type="email" />
// Focus management for modals
useEffect(() => { if (isOpen) closeButtonRef.current?.focus(); }, [isOpen]);Tools: eslint-plugin-jsx-a11y, React Aria (Adobe), axe-core DevTools extension.
useId Hook?Introduced in React 18, useId generates a stable unique ID that is consistent between server and client renders — avoiding SSR hydration mismatches.
function FormField({ label }) {
const id = useId(); // e.g. ":r1:"
return (
<>
<label htmlFor={id}>{label}</label>
<input id={id} />
</>
);
}Do not use useId to generate keys for lists — use data IDs for that.
startTransition marks a state update as non-urgent. React will defer it and keep the UI responsive for urgent updates (like typing).
import { startTransition, useTransition } from 'react';
function Search() {
const [isPending, startTransition] = useTransition();
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
function handleChange(e) {
setQuery(e.target.value); // urgent — update input immediately
startTransition(() => {
setResults(computeResults(e.target.value)); // non-urgent
});
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending ? <Spinner/> : results.map(...)}
</>
);
}The standard stack: Vitest or Jest (test runner) + React Testing Library (RTL) for DOM-focused tests + Playwright/Cypress for end-to-end tests.
// Example RTL test
import { render, screen, fireEvent } from '@testing-library/react';
test('increments counter', () => {
render(<Counter />);
expect(screen.getByText('Count: 0')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /increment/i }));
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});RTL philosophy: test what the user sees (text, roles) not implementation details (state, refs).
Advanced Level
Concurrent rendering (React 18) lets React prepare multiple versions of the UI simultaneously without blocking the main thread. Renders can be interrupted, paused, and resumed based on priority.
Key APIs:
createRoot— opt into concurrent modestartTransition/useTransition— deprioritize non-urgent updatesuseDeferredValue— defer a value to avoid blocking inputSuspense+ data fetching — show fallbacks while async rendering
Benefit: heavy renders (large lists, complex charts) no longer freeze the UI — React keeps urgent interactions (typing, clicking) buttery smooth.
useDeferredValue? How does it compare to debouncing?useDeferredValue accepts a value and returns a deferred version that “lags behind” to allow more urgent renders to go first.
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
// stale deferredQuery during typing → React renders latest query first
const results = expensiveFilter(deferredQuery);
return results.map(r => <Result key={r.id} {...r} />);
}vs debounce: debounce delays state updates on a fixed timer. useDeferredValue lets React schedule the update based on available CPU time — no artificial delay, and it starts updating as soon as the browser is idle.
With SSR, React renders components to HTML on the server and sends it to the browser. The client then “hydrates” — attaching event listeners to the existing HTML without re-rendering.
// Server (Node.js)
import { renderToString } from 'react-dom/server';
const html = renderToString(<App />);
res.send(`<html><body><div id="root">${html}</div></body></html>`);
// Client
import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document.getElementById('root'), <App />);Benefits: faster FCP, SEO-friendly. Drawbacks: TTFB increases, server load, hydration complexity. Frameworks: Next.js, Remix.
React Server Components (introduced in React 18, popularized by Next.js 13+ App Router) run exclusively on the server. They can access databases and file systems directly, never ship JS to the client, and reduce bundle size.
- Server Components — async, no state/hooks, zero client JS
- Client Components —
'use client'directive, can use hooks and events - Shared Components — can render as either depending on where they’re imported
// app/page.tsx — Server Component (default in Next.js 13+)
async function Page() {
const data = await db.query('SELECT * FROM posts'); // runs on server only
return data.map(post => <PostCard key={post.id} post={post} />);
}Hydration is the process of attaching React’s event system to server-rendered HTML. React walks the existing DOM and matches it against the Virtual DOM tree. If they don’t match, React throws a hydration error and falls back to client rendering.
Common causes of hydration mismatch:
- Rendering
Date.now()orMath.random()differently server vs client - Using
typeof windowto conditionally render - Third-party scripts modifying the DOM before React hydrates
- Invalid HTML nesting (e.g.
<p><div></div></p>)
Fix: use suppressHydrationWarning for intentional mismatches (e.g., timestamps), or defer rendering until client with useEffect.
Suspense data-fetching model (Suspense for Data Fetching)?A component “suspends” by throwing a Promise. React catches it, shows the nearest Suspense fallback, and retries rendering the component when the Promise resolves.
// Library creates a "resource" that throws a Promise
function wrapPromise(promise) {
let status = 'pending', result;
const p = promise.then(d => { status = 'success'; result = d; })
.catch(e => { status = 'error'; result = e; });
return { read() {
if (status === 'pending') throw p;
if (status === 'error') throw result;
return result;
}};
}
// Component using the resource
function UserProfile({ resource }) {
const user = resource.read(); // throws Promise if not ready
return <div>{user.name}</div>;
}In practice, frameworks like Next.js and libraries like TanStack Query implement this for you.
Virtualization renders only the visible rows, keeping DOM nodes constant regardless of list size. Great for lists of 10,000+ items.
const ROW_HEIGHT = 40;
function VirtualList({ items }) {
const [scrollTop, setScrollTop] = useState(0);
const containerHeight = 400;
const totalHeight = items.length * ROW_HEIGHT;
const startIndex = Math.floor(scrollTop / ROW_HEIGHT);
const visibleCount = Math.ceil(containerHeight / ROW_HEIGHT) + 1;
const visibleItems = items.slice(startIndex, startIndex + visibleCount);
return (
<div
style={{ height: containerHeight, overflowY: 'auto', position: 'relative' }}
onScroll={e => setScrollTop(e.target.scrollTop)}
>
<div style={{ height: totalHeight }}>
{visibleItems.map((item, i) => (
<div
key={item.id}
style={{
position: 'absolute',
top: (startIndex + i) * ROW_HEIGHT,
height: ROW_HEIGHT,
}}
>
{item.name}
</div>
))}
</div>
</div>
);
}In production, use react-window or @tanstack/react-virtual.
Flux is a unidirectional data-flow pattern from Facebook: Action → Dispatcher → Store → View → Action. Redux is an opinionated Flux implementation with a single store, pure reducer functions, and a rich middleware ecosystem.
// Redux flow
store.dispatch({ type: 'counter/increment' }); // Action
// Reducer: (state, action) => newState
// Subscribers re-renderRedux Toolkit (RTK) is now the official, recommended way to use Redux — it uses Immer internally so you can “mutate” state in reducers, and createSlice handles action type boilerplate.
Zustand is a minimal, hook-based state manager. It has almost no boilerplate and works outside React components too.
import { create } from 'zustand';
const useStore = create(set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 })),
}));
function Counter() {
const { count, increment } = useStore();
return <button onClick={increment}>{count}</button>;
}vs Redux: Zustand is far less boilerplate, no Provider needed, subscribes components to only the slice of state they use. Redux RTK remains better for large teams needing strict conventions, time-travel debugging, and powerful middleware.
Systematic approach:
- Profile first — use React DevTools Profiler to find offending components before guessing.
- Memoize components —
React.memoskips re-renders when props are reference-equal. - Stable references —
useCallback/useMemoprevent new object/function refs on each render. - Split Context — separate frequently-changing from infrequently-changing context values.
- Colocate state — push state down; only subtrees that need it re-render.
- Virtualize lists —
react-windowrenders only visible rows. - Lazy load — code-split heavy sections, images, data.
- Transitions — wrap non-urgent updates in
startTransition.
flushSync in React 18?React 18 batches all state updates automatically (even in setTimeout and Promises). flushSync forces React to flush pending updates synchronously inside the callback — useful when you need DOM measurements immediately after a state update.
import { flushSync } from 'react-dom';
flushSync(() => {
setItems([..items, newItem]);
});
// DOM is updated HERE, before the next line
listRef.current.lastChild.scrollIntoView();Use sparingly — overuse hurts performance by defeating batching.
function DnDList({ initialItems }) {
const [items, setItems] = useState(initialItems);
const dragIndex = useRef(null);
function handleDragStart(index) { dragIndex.current = index; }
function handleDrop(dropIndex) {
const updated = [...items];
const [removed] = updated.splice(dragIndex.current, 1);
updated.splice(dropIndex, 0, removed);
setItems(updated);
dragIndex.current = null;
}
return (
<ul>
{items.map((item, i) => (
<li
key={item.id}
draggable
onDragStart={() => handleDragStart(i)}
onDragOver={e => e.preventDefault()}
onDrop={() => handleDrop(i)}
>
{item.label}
</li>
))}
</ul>
);
}For production: use @dnd-kit/core or react-beautiful-dnd for accessibility, touch support, and animation.
A stale closure occurs when a callback captures an old value from a previous render and doesn’t see the current state/props.
// ❌ Bug — count is stale inside setInterval callback
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // always reads count=0
}, 1000);
return () => clearInterval(id);
}, []); // empty deps — effect never re-runs
// ✅ Fix — use functional updater
setCount(prev => prev + 1); // prev is always fresh
// ✅ Alternative — use a ref to track latest value
const countRef = useRef(count);
countRef.current = count;
// inside callback: use countRef.currentAn optimistic update applies a change immediately in the UI before the server confirms it, then rolls back if the server returns an error.
async function toggleLike(postId) {
// 1. Optimistically update UI
setPosts(prev => prev.map(p =>
p.id === postId ? { ...p, liked: !p.liked } : p
));
try {
await api.toggleLike(postId); // 2. Persist on server
} catch {
// 3. Roll back on failure
setPosts(prev => prev.map(p =>
p.id === postId ? { ...p, liked: !p.liked } : p // toggle back
));
toast.error('Failed to update like');
}
}React 19 introduced useOptimistic for a built-in, first-class API for this pattern.
TanStack Query is a server-state management library. It handles: caching, background refetching, deduplication of requests, pagination, infinite scroll, optimistic updates, synchronization, and loading/error states — all declaratively.
import { useQuery, useMutation } from '@tanstack/react-query';
function Posts() {
const { data, isLoading, error } = useQuery({
queryKey: ['posts'],
queryFn: () => fetch('/api/posts').then(r => r.json()),
staleTime: 5 * 60 * 1000, // 5 minutes
});
if (isLoading) return <Spinner />;
if (error) return <Error />;
return data.map(p => <Post key={p.id} post={p} />);
}function useNotifications(userId) {
const [notifications, setNotifications] = useState([]);
useEffect(() => {
const ws = new WebSocket(`wss://api.example.com/ws?user=${userId}`);
ws.onmessage = (event) => {
const notif = JSON.parse(event.data);
setNotifications(prev => [notif, ...prev]);
};
ws.onerror = (e) => console.error('WebSocket error', e);
ws.onclose = () => console.log('WebSocket closed');
return () => ws.close(); // cleanup on unmount / userId change
}, [userId]);
return notifications;
}Alternatives: SSE (EventSource), long polling, or libraries like Socket.io / Ably.
The <Profiler> component lets you programmatically measure rendering performance in production builds.
import { Profiler } from 'react';
function onRenderCallback(id, phase, actualDuration, baseDuration) {
sendToAnalytics({ id, phase, actualDuration, baseDuration });
}
<Profiler id="Navigation" onRender={onRenderCallback}>
<Navigation />
</Profiler>Parameters: id (label), phase (mount/update), actualDuration (time for this render), baseDuration (estimated without memo), startTime, commitTime.
function Modal({ isOpen, onClose, title, children }) {
const dialogRef = useRef();
useEffect(() => {
if (!isOpen) return;
dialogRef.current?.focus();
const onKey = e => { if (e.key === 'Escape') onClose(); };
document.addEventListener('keydown', onKey);
return () => document.removeEventListener('keydown', onKey);
}, [isOpen, onClose]);
if (!isOpen) return null;
return createPortal(
<div role="dialog" aria-modal="true" aria-labelledby="modal-title">
<div ref={dialogRef} tabIndex={-1}>
<h2 id="modal-title">{title}</h2>
{children}
<button onClick={onClose} aria-label="Close">✕</button>
</div>
</div>,
document.body
);
}State normalization stores entity data in a flat map (keyed by ID) rather than nested arrays. This avoids data duplication and makes updates O(1) instead of O(n).
// ❌ Denormalized — hard to update a specific post
{ posts: [{ id: 1, author: { id: 5, name: 'Alice' } }, ...] }
// ✅ Normalized — each entity stored once
{
posts: { ids: [1], entities: { 1: { id: 1, authorId: 5 } } },
users: { ids: [5], entities: { 5: { id: 5, name: 'Alice' } } }
}Redux Toolkit’s createEntityAdapter automates this pattern. TanStack Query handles it automatically via query cache keying.
Key CWV metrics and React-specific fixes:
- LCP (Largest Contentful Paint) — SSR or SSG, preload hero image, eliminate render-blocking resources.
- INP (Interaction to Next Paint) — debounce handlers, use
startTransition, avoid long tasks, virtualize large lists. - CLS (Cumulative Layout Shift) — set explicit dimensions on images/iframes, avoid injecting content above existing content.
// Measure with web-vitals library
import { onINP, onLCP, onCLS } from 'web-vitals';
onINP(metric => sendToAnalytics(metric));
onLCP(metric => sendToAnalytics(metric));use Hook (React 19)?The use Hook (React 19) lets you read the value of a resource — a Promise or a Context — inside render. Unlike other hooks, use can be called inside conditionals and loops.
// Reading a Context with use (equivalent to useContext)
import { use } from 'react';
function Heading({ children }) {
const level = use(LevelContext);
return <{`h${level}`}>{children}</{`h${level}`}>;
}
// Reading a Promise (must be wrapped / cache)
function Comments({ commentsPromise }) {
const comments = use(commentsPromise); // suspends until resolved
return comments.map(c => <Comment key={c.id} comment={c} />);
}Key principles for large-scale React apps:
- Feature-based folder structure — group by domain (
features/auth,features/dashboard) not by type. - Clear layer separation — UI components → hooks/services → API layer.
- Strict module boundaries — use barrel exports and enforce with ESLint import rules.
- Micro-frontend or monorepo — NX/Turborepo for team scalability.
- Design system — shared component library (Storybook) consumed by all features.
- Typed contracts — TypeScript + Zod for runtime validation of API responses.
- Testing pyramid — unit (hooks/utils), integration (RTL), e2e (Playwright).
Server Actions let you call server-side functions directly from client components — without manually writing API routes. They’re marked with 'use server' and can be called from form actions or event handlers.
// actions.ts — runs on server
'use server';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
await db.post.create({ data: { title } });
revalidatePath('/posts');
}
// Client component
<form action={createPost}>
<input name="title" />
<button type="submit">Create</button>
</form>Streaming SSR uses renderToPipeableStream (Node.js) or renderToReadableStream (Edge) to stream HTML to the browser in chunks rather than waiting for the full page to render.
import { renderToPipeableStream } from 'react-dom/server';
res.setHeader('Content-Type', 'text/html');
const { pipe } = renderToPipeableStream(<App />, {
onShellReady() { pipe(res); }, // send shell immediately
onError(err) { console.error(err); }
});Wrapped in Suspense, slow components don’t block the initial shell. The client progressively hydrates chunks as they stream in. Result: faster FCP + TTFB without sacrificing dynamic content.
Micro-frontends decompose a large frontend into independently deployable apps owned by different teams. Common approaches with React:
- Module Federation (Webpack 5 / Rspack) — dynamically load remote components at runtime.
- iframes — strong isolation, simple, but limited UX/communication.
- Custom Elements / Web Components — wrap React apps as standards-based elements.
- Single-SPA — orchestrates multiple framework apps on one page.
// webpack.config.js (Host) — Module Federation
new ModuleFederationPlugin({
remotes: {
cart: 'cart@https://cart.example.com/remoteEntry.js',
},
})
// In host app
const CartWidget = lazy(() => import('cart/CartWidget'));Expert Level
React uses a scheduler (the scheduler package) that assigns lanes and priorities to work. Work is queued in a min-heap and processed in priority order using cooperative scheduling (yielding to the browser event loop).
Priority levels (React 18 lanes):
- SyncLane —
flushSync, legacy mode. Always processes before paint. - InputContinuousLane — pointer/scroll events. Processed before next frame.
- DefaultLane — normal setState. Batch and process asap.
- TransitionLane —
startTransition. Can be interrupted by higher-priority work. - OffscreenLane — pre-rendering hidden content.
The scheduler uses MessageChannel to schedule work asynchronously, yielding every ~5ms to let the browser handle input/paint.
React wraps event handlers in batchedUpdates. In React 17 and earlier, this only applied inside React event handlers. In React 18, batching is automatic everywhere via a mechanism called automatic batching.
Internally: each setState call enqueues an update on the fiber’s updateQueue. React defers the re-render by scheduling work asynchronously. Only after the current execution context ends does React flush the queue and process all enqueued updates together in a single render pass.
Calling flushSync forces immediate synchronous flush of the queue.
The React Compiler (previously codenamed “React Forget”) is a Babel plugin developed by the React team that automatically memoizes components, hooks, and JSX expressions at compile time — eliminating the need for manual useMemo, useCallback, and React.memo.
It uses static analysis to identify values whose referential identity needs to be preserved, then inserts the correct memoization. It understands React’s rules of hooks and can prove safety of optimizations.
Released as part of React 19 with Meta running it in production on Instagram.com before public release. Opt in via babel config or Next.js config option.
React’s reconciler (react-reconciler) is decoupled from the host environment. You implement a “host config” that defines how to create, update, and delete nodes in your custom target.
import Reconciler from 'react-reconciler';
const HostConfig = {
createInstance(type, props) { return { type, props, children: [] }; },
appendChildToContainer(container, child) { container.children.push(child); },
commitUpdate(instance, _, __, ___, newProps) { instance.props = newProps; },
removeChildFromContainer(container, child) { /* ... */ },
supportsMutation: true,
// ... ~30 other required methods
};
const MyRenderer = Reconciler.createContainer(HostConfig);
export function render(element, container) {
MyRenderer.updateContainer(element, container);
}Examples in the wild: React Three Fiber (WebGL/Three.js), React PDF, React Native, Ink (terminal).
Internally, a Context object has a $$typeof symbol and stores the current value on the provider’s fiber during reconciliation.
When a Provider renders, React pushes the new value onto a context stack (a linked list of fiber nodes). When a useContext consumer renders, React walks up the fiber tree to find the nearest matching Provider and reads its current value.
When the Provider’s value changes, React propagates the change by marking all consumers as needing re-render (a “context propagation bailout” scan). This is O(n) in the subtree size, which is why splitting contexts and memoizing consumers matters for performance.
React.memo with objects and functions?React.memo uses shallow reference equality. Every render creates new object/array/function references, so memoization is defeated without useMemo/useCallback.
const Child = React.memo(({ style, onClick }) => <div style={style} onClick={onClick}>...</div>);
// ❌ New object on every Parent render — memo is useless
<Child style={{ color: 'red' }} onClick={() => doThing()} />
// ✅ Stable references
const style = useMemo(() => ({ color: 'red' }), []);
const onClick = useCallback(() => doThing(), []);
<Child style={style} onClick={onClick} />Alternative: use the React Compiler which handles this automatically, or design components to accept primitive props.
Race conditions occur when a user triggers multiple requests and the last one resolves before an earlier one, displaying stale data. Solutions:
// Pattern 1: cleanup flag
useEffect(() => {
let active = true;
fetchData(id).then(data => { if (active) setData(data); });
return () => { active = false; };
}, [id]);
// Pattern 2: AbortController
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(r => r.json())
.then(setData)
.catch(e => { if (e.name !== 'AbortError') setError(e); });
return () => controller.abort();
}, [url]);TanStack Query handles race conditions automatically — it cancels in-flight queries when a newer request supersedes them.
// flags.ts
export const flags = {
newDashboard: Boolean(process.env.NEXT_PUBLIC_FLAG_NEW_DASHBOARD),
};
// Hook
function useFlag(key) {
const { user } = useAuth();
const remoteFlags = useQuery({ queryKey: ['flags', user.id], queryFn: fetchFlags });
return remoteFlags.data?.[key] ?? flags[key] ?? false;
}
// Usage
function App() {
const showNewDash = useFlag('newDashboard');
return showNewDash ? <NewDashboard /> : <OldDashboard />;
}Production systems use services like LaunchDarkly, Statsig, or GrowthBook, which add targeting rules, A/B experimentation, kill switches, and analytics.
// eventBus.ts
type Handler = (data: unknown) => void;
const listeners = new Map<string, Set<Handler>>();
export const eventBus = {
on(event: string, handler: Handler) {
if (!listeners.has(event)) listeners.set(event, new Set());
listeners.get(event)!.add(handler);
return () => listeners.get(event)!.delete(handler);
},
emit(event: string, data?: unknown) {
listeners.get(event)?.forEach(h => h(data));
}
};
// Hook
function useEvent<T>(event: string, handler: (data: T) => void) {
const handlerRef = useRef(handler);
handlerRef.current = handler;
useEffect(() => {
return eventBus.on(event, (data) => handlerRef.current(data as T));
}, [event]);
}function useUndoRedo<T>(initial: T) {
const [history, setHistory] = useState<T[]>([initial]);
const [index, setIndex] = useState(0);
const current = history[index];
const set = useCallback((newState: T) => {
const next = history.slice(0, index + 1); // drop future states
setHistory([...next, newState]);
setIndex(next.length);
}, [history, index]);
const undo = () => setIndex(i => Math.max(0, i - 1));
const redo = () => setIndex(i => Math.min(history.length - 1, i + 1));
return { current, set, undo, redo,
canUndo: index > 0,
canRedo: index < history.length - 1
};
}const STEPS = ['info', 'payment', 'confirm'] as const;
function Wizard() {
const [searchParams, setSearchParams] = useSearchParams();
const stepParam = searchParams.get('step');
const stepIndex = STEPS.indexOf((stepParam ?? 'info') as typeof STEPS[0]);
const currentStep = STEPS[Math.max(0, stepIndex)];
const [formData, setFormData] = useState({});
function goTo(step: typeof STEPS[0]) {
setSearchParams({ step });
}
function saveAndNext(data: object) {
setFormData(prev => ({ ...prev, ...data }));
const nextStep = STEPS[stepIndex + 1];
if (nextStep) goTo(nextStep);
}
return (
<>
{currentStep === 'info' && <InfoStep onNext={saveAndNext} />}
{currentStep === 'payment' && <PaymentStep onNext={saveAndNext} />}
{currentStep === 'confirm' && <ConfirmStep data={formData} />}
</>
);
}Web Workers run JS in a background thread, off the main thread. React UI lives on the main thread, but you can offload CPU-heavy work (image processing, search indexing, AI inference) to a worker and communicate via postMessage.
// worker.ts
self.onmessage = ({ data }) => {
const result = heavyComputation(data);
self.postMessage(result);
};
// useWorker.ts
function useWorker(workerPath: string) {
const workerRef = useRef<Worker>();
useEffect(() => {
workerRef.current = new Worker(workerPath, { type: 'module' });
return () => workerRef.current?.terminate();
}, [workerPath]);
const compute = (data: unknown) => new Promise(resolve => {
workerRef.current!.onmessage = ({ data }) => resolve(data);
workerRef.current!.postMessage(data);
});
return { compute };
}act() testing utility and why is it important?act() ensures that all state updates, effects, and re-renders are flushed before you make assertions in tests. Without it, tests may assert on stale DOM state.
import { act } from 'react';
import { render } from '@testing-library/react';
test('loads and displays data', async () => {
await act(async () => {
render(<DataLoader />);
});
// Now state updates + effects have all run
expect(screen.getByText('Data loaded')).toBeInTheDocument();
});React Testing Library wraps all its utilities (render, userEvent, fireEvent) in act automatically, which is why you typically don’t need to call it directly.
A production design system typically combines:
- Design tokens — CSS custom properties or JS objects for color, spacing, typography.
- Primitive components — Box, Text, Stack, Grid with token-based props.
- Compound components — Card, Modal, DataTable built from primitives.
- Storybook — living documentation with interactive playground.
// tokens.ts
export const tokens = {
colors: { primary: '#0066cc', danger: '#dc2626' },
space: [0, 4, 8, 16, 24, 32, 48, 64],
};
// Button with variant system (using vanilla-extract or Tailwind CVA)
const button = cva('rounded font-medium', {
variants: {
intent: {
primary: 'bg-blue-600 text-white',
danger: 'bg-red-600 text-white',
ghost: 'bg-transparent border',
},
size: { sm: 'px-2 py-1 text-sm', lg: 'px-6 py-3 text-lg' },
},
defaultVariants: { intent: 'primary', size: 'sm' },
});Key engineering challenges and solutions:
- Conflict resolution — use Operational Transformation (OT) or CRDTs (Yjs, Automerge) to merge concurrent edits without conflicts.
- Sync — WebSocket for real-time updates; CRDT diffs are small and efficient.
- Awareness — broadcast cursor positions and user presence.
- Offline support — CRDTs can merge divergent offline edits on reconnect.
- React integration — bind Yjs doc changes to React state via
y-reactor custom useSyncExternalStore hook.
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
const ydoc = new Y.Doc();
const provider = new WebsocketProvider('wss://y.example.com', 'room-1', ydoc);
const yText = ydoc.getText('content');
// In component:
useSyncExternalStore(
cb => { yText.observe(cb); return () => yText.unobserve(cb); },
() => yText.toString()
);useSyncExternalStore and when do you need it?useSyncExternalStore (React 18) is the correct way to subscribe to external (non-React) stores inside components. It ensures consistency under concurrent rendering by providing a snapshot mechanism.
import { useSyncExternalStore } from 'react';
function useWindowWidth() {
return useSyncExternalStore(
(callback) => {
window.addEventListener('resize', callback);
return () => window.removeEventListener('resize', callback);
},
() => window.innerWidth, // getSnapshot (client)
() => 1024 // getServerSnapshot (SSR)
);
}Use it when integrating with external state stores (Redux, Zustand, RxJS, browser APIs) to avoid tearing — inconsistent state during concurrent renders.
The Offscreen component (API still stabilizing, called Activity in latest React canary) lets React pre-render trees that are not yet visible, or cache them when they’re hidden — without destroying their state.
<Offscreen mode="hidden">
<ExpensiveTab /> // rendered but not visible, state preserved
</Offscreen>Mode options:
visible— normal renderinghidden— rendered off-screen, state preserved, effects pausedmanual— developer controls visibility transitions
Enables: instant tab switching (pre-rendered), keepalive patterns, background rendering. Replaces the display: none hack that destroys React state.
Tearing occurs when React renders a UI snapshot but an external store updates mid-render, causing different components to see different values of the same state — a visually inconsistent UI.
Scenario: Component A reads store version 1 → store updates to version 2 → Component B reads version 2 → they disagree on the same value.
React’s solution: useSyncExternalStore uses a two-phase “getSnapshot” check. After rendering, React verifies that all snapshots are still consistent. If not, it synchronously re-renders — trading some concurrency for consistency.
Libraries using legacy subscription patterns (e.g. old Redux useSelector) are vulnerable to tearing in concurrent mode until they migrate to useSyncExternalStore. RTK Query and modern Zustand handle this correctly.
A production-grade data grid for 100k rows requires multiple techniques in combination:
- Row virtualization — @tanstack/react-virtual or react-window. Only render ~20-50 visible rows.
- Column virtualization — virtual horizontal scrolling for wide tables.
- Memoized row components —
React.memoper row with stable props. - Immutable data structures — structural sharing for efficient diffing.
- Lazy loading — paginated server requests or cursor-based fetching.
- Web Worker offload — filtering, sorting, and aggregation off the main thread.
- Canvas rendering — for extreme performance (AG Grid’s column virtualizer uses canvas for headers).
Production libraries: AG Grid, TanStack Table (headless), react-data-grid.
React 19 ships several transformative features that change how React apps are built:
- React Compiler — automatic memoization;
useMemo/useCallback/React.memobecome largely unnecessary. - Server Components (stable) — zero-JS server-rendered components, direct data access, smaller bundles.
- Server Actions (stable) — async server functions callable from clients, simplifying API route boilerplate.
use()Hook — read Promises and Contexts in render, even inside conditionals.useOptimistic— first-class API for optimistic UI updates.useFormStatus/useFormState— form state management tied to server actions.- Asset loading APIs —
preload,prefetchDNS,preinitfor resource hints directly from components. - Activity (Offscreen) — keep-alive hidden subtrees with paused effects.
The direction: less client JS, better DX, compiler-driven optimization, and deeper server/client boundary awareness baked into the framework.
React JS Interview Questions and Answers: Expert Level
100 Expert-Level React Questions. Conquer React JS machine rounds and system design interviews by mastering React Architecture, Fiber Internals, RSCs, Security, and Custom Renderers.
1. React Architecture, Fiber & Diffing
Before React 16, the “Stack Reconciler” used synchronous, recursive tree traversal. Once an update started, it could not be interrupted, leading to dropped frames if rendering took longer than 16ms. Fiber is a complete rewrite representing a cooperative scheduling engine. It breaks rendering work into a linked list of mutable “Fiber Nodes.” This allows React to pause rendering, yield control back to the browser to handle user input, and resume or discard the work based on assigned Lane priorities.
import { useState, useTransition, useEffect } from 'react';
export function InventoryDash({ socket }) {
const [searchTerm, setSearchTerm] = useState('');
const [inventory, setInventory] = useState([]);
const [isPending, startTransition] = useTransition();
// High priority UI update
const handleSearch = (e) => setSearchTerm(e.target.value);
useEffect(() => {
socket.on('stock_update', (data) => {
// Yielding to Fiber: Process stock updates in the background
startTransition(() => {
setInventory(data);
});
});
return () => socket.off('stock_update');
}, [socket]);
return (
<div>
<input onChange={handleSearch} value={searchTerm} placeholder="Search SKUs..." />
<List data={inventory} stale={isPending} />
</div>
);
}
A generalized algorithm to find the minimum number of operations to transform one tree into another has a complexity of O(n³). For a UI with 1000 nodes, this means one billion comparisons. React implements a heuristic O(n) algorithm based on two assumptions:
- Two elements of different types will produce different trees. React will tear down the old tree completely and build the new one from scratch.
- Developers can hint at which child elements remain stable across renders using the
keyprop.
// BAD: Array index keys break the O(n) diffing heuristic on deletion/sorting
{zones.map((zone, index) => (
<DeliveryZone key={index} data={zone} />
))}
// GOOD: Stable identities enable efficient Fiber reconciliation
{zones.map((zone) => (
<DeliveryZone key={zone.zoneId} data={zone} />
))}
Double buffering is a technique borrowed from game development to prevent screen tearing. React maintains two Fiber trees: the current tree (reflecting the exact state of the visible DOM) and the workInProgress tree (the draft being calculated in memory).
During the Render phase, all calculations and diffing happen on the workInProgress tree without touching the DOM. Once the Commit phase completes the DOM mutations, React simply swaps the pointers: the workInProgress tree instantly becomes the current tree. If an error or high-priority interruption occurs mid-render, React can safely throw away the workInProgress tree without leaving the user with a broken, half-rendered UI.
Originally, React assigned “expiration times” to updates to determine priority. However, time is linear, making it difficult to express complex concepts like “batch these two background tasks together but preempt them for this specific input.”
React 17+ introduced “Lanes”—a 32-bit bitmask system. Each bit represents a priority level (e.g., SyncLane for inputs, TransitionLane for data fetching). Bitmasks allow the Scheduler to use highly efficient bitwise operations (like & and |) to merge updates, check for overlapping priorities, and decide what task to execute next instantaneously.
When a component’s state or props remain unchanged (verified via shallow equality in React.memo), Fiber “bails out.” It aborts traversing that branch and clones the node directly from the current tree to the workInProgress tree, saving massive CPU cycles.
However, if a Context Provider updates, React scans down the entire tree to find components hooked to that Context. It marks those specific Fiber nodes with a forced update Lane. Even if intermediate parent components trigger a bailout, React will strictly bypass the bailout for the marked Context consumers to ensure they receive the fresh data.
The Render Phase is pure, asynchronous, and interruptible. React calls your component functions, calculates the changes, and flags Fiber nodes with “Effect Tags” (e.g., Placement, Update, Deletion). No DOM mutations happen here.
The Commit Phase is synchronous and uninterruptible. React iterates over the list of Effect Tags and executes the physical DOM mutations (appendChild, removeChild). Afterward, it fires lifecycle hooks like componentDidMount and useLayoutEffect.
useLayoutEffect over useEffect?useEffect fires asynchronously after the browser has painted the screen. useLayoutEffect fires synchronously before the browser paints. If you mutate the DOM in useEffect, the user will see a visual flicker (the first paint, then the mutation).
useEffect causes the tooltip to render at 0,0 and jump to the star. useLayoutEffect allows you to measure the node and apply the precise coordinates before the browser ever paints, preventing the layout shift.
import { useLayoutEffect, useRef, useState } from 'react';
export function StarTooltip({ targetRef }) {
const tooltipRef = useRef(null);
const [coords, setCoords] = useState({ top: 0, left: 0 });
useLayoutEffect(() => {
// Measures and sets coordinates synchronously before paint
const targetRect = targetRef.current.getBoundingClientRect();
const tooltipRect = tooltipRef.current.getBoundingClientRect();
setCoords({
top: targetRect.top - tooltipRect.height,
left: targetRect.left + (targetRect.width / 2) - (tooltipRect.width / 2)
});
}, [targetRef]);
return <div ref={tooltipRef} style={{ ...coords, position: 'absolute' }}>Chart Data</div>;
}
Browsers consume heavy memory if you attach individual event listeners (like onClick) to thousands of DOM nodes. React solves this by ignoring your inline onClick handlers completely at the DOM level.
Instead, React attaches exactly one event listener per event type to the root container (e.g., div#root). When a user clicks a button, the browser event bubbles to the root. React intercepts it, wraps it in a cross-browser compatible “Synthetic Event,” determines which Fiber node was the target, and simulates the capturing and bubbling phases entirely in memory.
If an error is thrown during the Render phase, React catches it and halts the workInProgress traversal. It then walks up the Fiber tree looking for the nearest node tagged as an Error Boundary (a class component implementing static getDerivedStateFromError).
class WidgetErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
// Halts the crash and schedules a fallback render
return { hasError: true };
}
componentDidCatch(error, info) {
// Send to Datadog/Sentry
logToMonitoringService(error, info.componentStack);
}
render() {
if (this.state.hasError) {
return <div className="fallback">AI Prediction temporarily unavailable.</div>;
}
return this.props.children;
}
}
Deep equality requires recursive traversal of nested objects. If a state object has hundreds of keys, executing a deep comparison on every single keystroke or data tick would O(N) throttle the CPU, destroying performance.
Shallow equality (oldProp === newProp) checks memory references, which is an O(1) instantaneous operation. This is why immutability is an architectural requirement in React. By returning a brand new object reference when mutating state, React instantly knows the data changed without having to inspect the internal keys.
2. Advanced Concurrency & React 19
use() API fundamentally differ from await in an async function?Standard await halts the execution of a JavaScript function entirely until a Promise resolves. In React, halting a render function blocks the thread. use() is designed to integrate with React’s Fiber engine and Suspense. When use() encounters an unresolved Promise, it actually throws that Promise up the component tree. React catches it, suspends the component, renders the nearest Suspense fallback, and resumes rendering only when the Promise resolves.
Crucially, because use() hooks into the compiler rather than standard Hook dispatchers, it can be called conditionally inside if statements and loops, breaking the traditional Rules of Hooks.
use(), you can conditionally suspend only if the user is premium.
import { use, Suspense } from 'react';
function AISummary({ isPremium, summaryPromise }) {
// Valid in React 19! We can call `use()` inside a conditional block.
if (!isPremium) {
return <div>Upgrade to view AI Summary</div>;
}
// Throws to Suspense boundary if unresolved, returns data if resolved
const summary = use(summaryPromise);
return <p>{summary}</p>;
}
“Tearing” occurs when a React application’s UI becomes inconsistent because an external data source (like a Redux store) mutates while React is in the middle of a concurrent Render phase. Because concurrent rendering yields to the main thread, an external event could change the store state. Half the UI might render with the old state, and the bottom half with the new state.
React mitigates this using the useSyncExternalStore hook. It forces React to track the external store’s version during the render. If a mutation is detected mid-render, React immediately discards the torn workInProgress tree and triggers a synchronous, high-priority re-render to guarantee UI consistency.
Server Actions abstract away the API layer. When you mark a function with 'use server', the React compiler extracts it from the client bundle and generates a secure, hidden RPC (Remote Procedure Call) endpoint. When a client component invokes the action, React automatically serializes the arguments, sends a POST request to that endpoint, executes the Node.js logic (like database mutations), and streams the updated UI back to the client.
// action.ts
'use server'
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
export async function updateCartQuantity(formData: FormData) {
const itemId = formData.get('itemId');
const qty = parseInt(formData.get('quantity'), 10);
await db.cart.update({ itemId, qty });
// Instructs the server to stream the updated RSC payload to the client
revalidatePath('/cart');
}
// CartItem.tsx (Client or Server Component)
import { updateCartQuantity } from './action';
export function CartItem({ item }) {
return (
<form action={updateCartQuantity}>
<input type="hidden" name="itemId" value={item.id} />
<input type="number" name="quantity" defaultValue={item.qty} />
<button type="submit">Update</button>
</form>
);
}
taint API in React 19 and what architectural security problem does it solve?With Server Components, the boundary between backend and frontend is porous. It becomes incredibly easy to accidentally pass a raw database object as a prop to a Client Component, which serializes sensitive data (like password hashes or internal API keys) directly into the browser’s HTML payload.
React 19 introduces taintObjectReference. If an architect explicitly “taints” a user object at the database layer, React’s serialization engine will monitor it. If a developer ever accidentally passes that tainted object to a Client Component, the React compiler will aggressively throw a fatal error, preventing the data leak.
Previously, form submissions required boilerplate: e.preventDefault(), setting isSubmitting to true, awaiting a fetch, and handling errors. React 19 native forms integrate directly with the transition API via useActionState (formerly useFormState) and useFormStatus.
import { useFormStatus } from 'react-dom';
// This component can be deeply nested inside the <form>
function SubmitButton() {
// Automatically reads the pending state of the nearest parent form action
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Uploading Securely...' : 'Submit Document'}
</button>
);
}
useTransition and useDeferredValue.Both APIs tell React to deprioritize a render, but they operate on different levels of abstraction.
useTransition is Imperative: You wrap the state setter function (the action). You explicitly tell React, “The state update caused by this specific click/typing event is low priority.”
useDeferredValue is Declarative: You wrap the data value itself. You tell React, “I don’t care where this value came from (props, context, external store), if it changes, keep showing the old value for high-priority renders, and schedule a background render to figure out the new UI.”
Historically, injecting CSS files asynchronously caused a Flash of Unstyled Content (FOUC), and React didn’t know when a stylesheet was fully loaded. React 19 introduces native support for Document Metadata. By rendering <link rel="stylesheet" precedence="default"> inside any component, React hoists the link to the <head>.
Crucially, React will now suspend the Commit phase. It holds the DOM update in memory until the browser confirms the stylesheet is downloaded and parsed, guaranteeing the user never sees unstyled markup.
React requires developers to manually declare dependency arrays in useEffect, useMemo, and useCallback to prevent infinite loops and stale closures. The React Compiler is a build-time Babel plugin that performs static code analysis on your component’s Abstract Syntax Tree (AST).
It understands the data flow and automatically injects low-level memoization caches (using arrays) around values that don’t mutate. Architecturally, this means you can delete almost all useMemo and useCallback hooks from your codebase. The compiler guarantees that a component or calculation will only re-execute if its structural, semantic dependencies genuinely change.
Standard Server-Side Rendering (SSR) requires the browser to download the entire JavaScript bundle before the page becomes interactive (the “Uncanny Valley”). Selective Hydration solves this using <Suspense> boundaries.
React sends the HTML immediately. Instead of waiting for all JS, it hydrates the DOM in chunks. If a user clicks or interacts with a specific Suspense boundary before it is hydrated, React’s event delegation system catches the click, bumps that specific chunk to the highest priority Lane, hydrates it instantly, and replays the user’s click event so no interaction is lost.
useOptimistic hook handle race conditions automatically?Optimistic UI (updating the screen before the server confirms success) usually requires complex rollback logic if the network fails. useOptimistic ties the optimistic state directly to the lifecycle of an ongoing async Action.
useOptimistic instantly increments the like counter on the screen. If the Server Action resolves successfully, the optimistic state is quietly swapped for the true server state. If the Server Action throws an error, the Action terminates, and React automatically drops the optimistic state, snapping the UI back to the original value without you writing a single line of manual rollback code.
import { useOptimistic } from 'react';
import { likePostAction } from './actions';
export function LikeButton({ post }) {
// optimisticLikes lives only as long as the action is pending
const [optimisticLikes, addOptimisticLike] = useOptimistic(
post.likeCount,
(state, amount) => state + amount
);
return (
<form action={async () => {
addOptimisticLike(1); // Instantly update UI
await likePostAction(post.id); // True mutation
}}>
<button type="submit">Like ({optimisticLikes})</button>
</form>
);
}
3. Server Components (RSC) & Next.js App Router
Standard SSR sends a finalized, static HTML string to the browser. While great for First Contentful Paint (FCP), it’s a dead end—if you navigate, the server must send a brand new HTML document, destroying client-side state.
The RSC payload is a highly specialized JSON-like stream. It represents the component tree’s Virtual DOM structure, the serialized props fetched from the database, and strict module references (import paths) indicating exactly where Client Component JS chunks should be inserted. React reads this payload on the client and merges it into the existing DOM without destroying client state (like a playing video or an active text input).
Server Components can freely import and render Client Components. However, Client Components cannot import Server Components. If a component with 'use client' imports a Server Component, that Server Component immediately becomes a Client Component, bloats the JS bundle, and causes errors if it uses Node.js modules like `fs`.
To bypass this restriction architecturally, we use the Composition Pattern. The Server Component imports both the Client Component and another Server Component, passing the latter into the former as a children prop.
// Layout.tsx (Server Component)
import { ClientSidebar } from './ClientSidebar';
import { ServerDataFeed } from './ServerDataFeed';
export default function Layout() {
return (
// We pass the Server Component through the Client Component's "hole"
<ClientSidebar>
<ServerDataFeed />
</ClientSidebar>
);
}
Because Next.js blends server and client code in the same directory structure, it is incredibly easy to accidentally import a utility function containing database credentials or Node-specific libraries (like `crypto`) into a Client Component. This leaks secrets to the browser and crashes the client bundle.
Architects use “poisoning” to prevent this. By importing the server-only package at the top of sensitive files, you explicitly poison them against client usage. If a developer mistakenly imports that file into a component with a 'use client' directive, the Webpack/Turbopack build instantly fails, preventing catastrophic security breaches.
// lib/db.ts
import 'server-only'; // POISON: Will break the build if leaked to client
import { Pool } from 'pg';
export const db = new Pool({
connectionString: process.env.DATABASE_URL,
});
Props crossing the network boundary from Server to Client must be strictly serializable. Under the hood, React converts these props into strings inside the RSC payload.
You can pass primitives (strings, numbers, booleans), arrays, Maps, Sets, Dates, and plain objects. You cannot pass class instances (like a custom new User() object), un-serializable APIs (like DOM nodes), or standard functions (event handlers).
'use server' (a Server Action). React intercepts this, serializes it as a hidden API endpoint reference, and passes that reference to the Client Component.
Partial Prerendering (PPR) is a groundbreaking optimization that merges Static Site Generation (SSG) and Server-Side Rendering (SSR). Historically, a page was either fast (static) or personalized (dynamic, but slow TTFB).
PPR allows Next.js to statically generate the outer shell of your layout at build time. The dynamic parts are wrapped in <Suspense>. Upon request, the server instantly sends the static shell from a CDN edge node (TTFB near zero). Simultaneously, the server executes the async database calls for the dynamic parts and streams them into the Suspense boundaries over the same HTTP connection.
import { Suspense } from 'react';
import { Skeleton } from '@/components/ui/Skeleton';
import AIPrediction from '@/components/AIPrediction';
export default function HoroscopePage({ params }) {
return (
<div className="layout">
<!-- STATIC SHELL: Cached and served instantly -->
<h1>Daily Horoscope: {params.sign}</h1>
<nav>Profile | Settings</nav>
<!-- DYNAMIC STREAM: Resolves and patches the DOM seconds later -->
<Suspense fallback={<Skeleton message="Consulting the stars..." />}>
<AIPrediction sign={params.sign} />
</Suspense>
</div>
);
}
The Full Route Cache lives entirely on the server. At build time (or during revalidation), Next.js renders the HTML and RSC payload and stores it on the disk/CDN. Multiple users hitting the same URL are served from this cache.
The Client-Side Router Cache lives entirely in the browser’s memory. When a user navigates between routes, Next.js fetches the RSC payload and stores it locally. If the user clicks “Back” or revisits a previously clicked tab, the UI renders instantly without sending a single network request to the server, preserving a native-app-like feel.
The lifecycle is drastically different from traditional React SPAs:
- The browser makes an HTTP request. The Node server begins executing the Server Components, making direct database queries.
- The server generates a specialized RSC payload and a standard HTML string.
- The HTML string is streamed to the browser to achieve an instant, non-interactive First Paint.
- The browser receives the RSC payload. React reconciles this payload to construct the Virtual DOM in memory without re-fetching data.
- Finally, the Client Component JavaScript bundles are downloaded. React “hydrates” the DOM, attaching event listeners to make it interactive.
When a Server Action completes a database mutation, you can invoke revalidatePath('/route') or revalidateTag('cache-tag'). This instructs the Next.js backend to purge its server-side cache for that specific data.
Because the Server Action was initiated from the client, Next.js seamlessly re-runs the Server Components for the current route and streams the updated RSC payload down in the response of the POST request. React diffs this payload and patches the specific DOM elements that changed, completely avoiding a harsh browser refresh.
You cannot initialize React Context inside a Server Component because RSCs run once on the server and do not hold ongoing state. Attempting to use createContext in a server file will throw an error.
To bypass this, you create a dedicated Client Component wrapper (e.g., <ThemeProvider>) that initializes the Context. You then wrap your Server Components (usually in the layout.tsx file) with this provider. The Server Components can fetch initial data and pass it as a serializable prop to the Provider, which then broadcasts the state to all deeply nested Client Components.
Standard SSR (used by older Next.js or Express apps) acts like a bottleneck: it waits for the absolute slowest API call to resolve before sending the <html> string to the browser. If a database query takes 3 seconds, TTFB is 3 seconds.
Streaming SSR utilizes HTTP chunked transfer encoding. Next.js instantly flushes the static layout (headers, navigation) to the browser. As asynchronous Server Components resolve inside <Suspense> boundaries, Next.js streams HTML fragments and tiny inline script tags into the still-open HTTP connection. These scripts instruct the browser to dynamically insert the resolved HTML into the correct placeholder, drastically lowering TTFB and keeping users engaged.
4. Advanced State & Micro-frontends
Legacy Redux suffered from massive boilerplate (constants, action creators, reducers spread across files) and the high risk of accidental state mutation. RTK solves this primarily through createSlice, which auto-generates action creators and action types simultaneously.
Architecturally, the biggest shift is RTK’s integration of Immer.js. Immer wraps the state in a JavaScript Proxy. This allows developers to write code that *looks* like mutable state updates (e.g., state.push(item)), but Immer intercepts the mutation and safely produces a perfectly immutable next state under the hood.
{ ...state, cart: { ...state.cart, items: [...] } }. With RTK, the mutation is direct and clean, preventing subtle UI tearing bugs caused by mutated references.
import { createSlice } from '@reduxjs/toolkit';
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [], total: 0 },
reducers: {
// RTK + Immer allows "mutating" logic safely
updateQuantity(state, action) {
const item = state.items.find(i => i.id === action.payload.id);
if (item) {
item.quantity = action.payload.quantity; // Looks mutable, is actually immutable!
}
}
}
});
export const { updateQuantity } = cartSlice.actions;
React Query and SWR are fantastic standalone libraries for server-state caching. However, an architect chooses RTK Query when the application already heavily relies on Redux for complex client-side state, and that client state needs to tightly couple with the server state.
Because RTK Query generates Redux reducers and middleware automatically, you can listen to RTK Query cache events (like a successful API fetch) inside your standard Redux slices to trigger synchronous client-side UI workflows without bridging two separate state management systems.
// In a standard Redux slice, listening to an RTK Query endpoint:
extraReducers: (builder) => {
builder.addMatcher(
api.endpoints.purchaseOrder.matchFulfilled,
(state, action) => {
// Synchronously clear the client-side cart when the server purchase succeeds
state.cartItems = [];
state.isCheckoutModalOpen = false;
}
);
}
Atomic State (Jotai): State is broken into tiny, independent pieces (“atoms”). A component only subscribes to the specific atoms it needs. It solves the top-down re-render problem of Context because updating one atom only re-renders the components actively listening to it. It is explicit and declarative.
Proxy State (Valtio/MobX): The entire state object is wrapped in a JS Proxy. The proxy intercepts property access (`get`) to automatically track which component reads which property. When a property mutates (`set`), it precisely triggers a re-render only for the components that read that specific property. It is highly implicit and imperative.
Historically, micro-frontends required building separate apps and mashing them together via IFrames or fragile NGINX routing. Module Federation allows multiple independent Webpack builds to share code and dynamically load chunks from each other at runtime over the network.
Architecturally, the most critical configuration is the shared dependencies array. If the “Host App” and “Remote App” both bundle their own copy of `react` and `react-dom`, React will crash with a “multiple instances of React” invariant violation (Hooks will fail). Module Federation negotiates this, ensuring the Host injects its singleton instance of React into the Remote app.
// webpack.config.js (Remote App)
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'checkoutApp',
filename: 'remoteEntry.js',
exposes: { './CheckoutWidget': './src/CheckoutWidget' },
shared: {
react: { singleton: true, requiredVersion: '^18.2.0' },
'react-dom': { singleton: true },
},
}),
],
};
Micro-frontends should ideally be totally decoupled. If Team A’s Cart app crashes, Team B’s Catalog app should still work. Therefore, tightly coupling them to a single monolithic Redux store defeats the purpose.
- The URL (Best): Pass IDs or search filters via query parameters. It’s universally understood and inherently decoupled.
- Custom Browser Events: Use the native
CustomEventAPI. The Catalog dispatches an ‘ADD_TO_CART’ event; the Cart listens globally and updates itself. - Shared Zustand/Zustand Store: If heavy state sharing is unavoidable, create a tiny, third micro-frontend that purely exposes a Zustand store via Module Federation, and have both apps subscribe to it.
Zustand solves several developer experience and performance issues inherent to Redux:
- No Context Provider: Redux requires wrapping your app in
<Provider store={store}>, tying the store to the React tree. Zustand stores are standard JS modules that live outside the React tree, meaning you can easily read/write state inside non-React files (like an Axios interceptor). - Transient Updates: Zustand allows you to subscribe to state changes without forcing a React component to re-render, which is crucial for 60FPS animations or scroll-syncing.
- Zero Boilerplate: No actions, no reducers, no dispatching required. Just functions that mutate state.
import { create } from 'zustand';
// Store lives entirely outside the React tree
const useBearStore = create((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}));
function BearCounter() {
const bears = useBearStore((state) => state.bears);
return <h1>{bears} around here ...</h1>;
}
React Context is not a state management tool; it is a dependency injection mechanism. When a Context Provider’s value prop changes, React forces every single component that calls useContext() for that provider to re-render, bypassing React.memo entirely.
useSyncExternalStore to subscribe only to the specific slices of data they care about from that injected store.
In deep, nested JSON responses (e.g., a Post with an array of Comments, each with an Author), updating an author’s profile picture requires finding and mutating every instance of that author buried in the tree. This is incredibly slow and complex.
Normalization flattens the state. It stores entities in an object dictionary keyed by ID ({ id1: {...}, id2: {...} }), and uses arrays of IDs to manage relationships. RTK’s createEntityAdapter provides pre-built reducers (like upsertOne, removeMany) and memoized selectors to instantly manage flat, normalized state tables without writing custom mapping logic.
A common anti-pattern is storing search queries, active tabs, and pagination data in Redux or useState. If a user finds a specific dashboard view and sends the link to a coworker, the coworker will see the default layout because the state was trapped in the sender’s local browser memory.
Architects promote the URL (Search Params) to be the Single Source of Truth for navigational state. The React component reads useSearchParams() to render the UI, and to update state, it pushes a new route via the history API. This guarantees that any complex UI view is 100% shareable, reproducible, and indexable by search engines.
Historically, Redux used Thunks (too simple for complex flows) or Sagas (complex Generator function syntax). RTK introduced Listener Middleware, a lightweight alternative to Sagas. It allows you to run imperative, asynchronous logic in response to specific dispatched actions.
If an API call fails due to an expired token, the Listener catches the specific “rejected” action, pauses all other outbound queries, dispatches a token refresh API call, and upon success, re-dispatches the original failed queries.
import { createListenerMiddleware } from '@reduxjs/toolkit';
const listenerMiddleware = createListenerMiddleware();
listenerMiddleware.startListening({
matcher: api.endpoints.getUser.matchRejected,
effect: async (action, listenerApi) => {
if (action.payload.status === 401) {
// Pause further processing, trigger refresh token
const success = await listenerApi.dispatch(refreshTokenRoute());
if (success) {
// Retry original action
listenerApi.dispatch(api.endpoints.getUser.initiate());
}
}
}
});
5. Profiling & Memory Management
An architect approaches memory leaks systematically using the Chrome DevTools “Memory” tab. The core technique is the 3-Snapshot Method. You take Snapshot 1 at baseline. You perform the suspected action (e.g., opening and closing a complex data grid component). You take Snapshot 2. You repeat the action and take Snapshot 3.
You then filter Snapshot 3 for objects allocated between Snapshots 1 and 2. Specifically, you search for “Detached DOM elements” or instances of your React components (like `DataGrid`). If the component is unmounted from the UI but still exists in the heap snapshot, you have successfully identified a leak.
Memory leaks in React rarely originate from the virtual DOM itself; they almost exclusively stem from poorly managed closure scopes inside useEffect or external event subscriptions. When a component registers an interval or listener, the callback function forms a closure over the component’s lexical scope.
If the component unmounts but the listener isn’t cleanly removed, the JavaScript engine’s Garbage Collector (GC) cannot free the memory. The browser’s native API still holds a reference to that callback—and by extension, the entire component state captured in the closure.
import { useEffect, useRef, useState } from 'react';
export function DeliveryCountdown({ orderId }) {
const [timeLeft, setTimeLeft] = useState(1800);
const timerRef = useRef(null);
useEffect(() => {
// Architect tip: Store the interval ID in a ref to guarantee clearing
// even if the component re-renders rapidly before unmounting.
timerRef.current = setInterval(() => {
setTimeLeft((prev) => (prev <= 1 ? 0 : prev - 1));
}, 1000);
// CRITICAL: The cleanup function that prevents the memory leak
return () => clearInterval(timerRef.current);
}, [orderId]);
return <div>{Math.floor(timeLeft / 60)} mins remaining</div>;
}
When analyzing a Heap Snapshot, understanding these two columns is critical for pinpointing the root cause of a leak.
Shallow Size: The memory allocated for the object itself. For a React component, this is usually tiny—just the memory required to hold the primitive values and the memory pointers to its children.
Retained Size: The massive amount of memory that would be freed if that specific object, and all the dependent objects it points to, were deleted. If a tiny 50-byte event listener closure holds a reference to a 50MB Redux data array, its shallow size is 50 bytes, but its retained size is 50MB.
The React Profiler records how long components take to render. It splits this into two metrics to help you judge the effectiveness of your memoization (React.memo, useMemo).
Actual Duration: The literal time spent rendering the component and its children for the current specific update. If children bailed out due to memoization, this number will be low.
Base Duration: The estimated time it would take to completely render the component tree from scratch (like on initial mount) without any memoization or bailout optimizations. If your Actual Duration is consistently near your Base Duration during updates, your memoization strategy is failing.
PerformanceObserver API to track custom React render metrics.Relying purely on React DevTools locally doesn’t tell you how your app performs on a low-end Android phone in production. React outputs marks using the native browser User Timing API. Architects instantiate a PerformanceObserver to listen for specific “mark” and “measure” events programmatically.
// Tracking React rendering bottlenecks in production
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// React tags its internal performance marks with an emoji or specific prefix
if (entry.name.includes('⚛️')) {
console.log(`React Phase: ${entry.name} took ${entry.duration}ms`);
// sendToAnalytics(entry.name, entry.duration);
}
}
});
// Observe custom marks and measures
observer.observe({ entryTypes: ['mark', 'measure'] });
Developers often create overly nested component trees (e.g., wrapping every element in multiple HOCs or context providers). Even if these components are purely presentational and execute quickly, a massive tree has severe hidden costs.
React must allocate a Fiber node object for every single component. A tree with 10,000 nodes means 10,000 objects in the V8 JS heap. During reconciliation, React must recursively traverse this massive linked list. This creates constant CPU overhead, inflates the Javascript memory heap, and triggers aggressive Garbage Collection (GC). GC events are “Stop-The-World”—they freeze the main thread, causing severe scrolling stutters on mobile devices.
Mobile CPUs severely struggle to decompress and parse massive JavaScript bundles. If you ship a 5MB JS bundle, a modern iPhone parses it in 200ms, but a low-end Android device might take 3-4 seconds, paralyzing the UI.
Architectural optimizations require aggressive Route-level code splitting (via React.lazy or Next.js dynamic imports). You must avoid “Barrel Files” (export * from './components') which trick Webpack into bundling the entire component library when only a single button is needed. Finally, migrating heavy dependencies (like Markdown parsers or Date formatters) to React Server Components (RSCs) physically removes them from the mobile device’s parse queue.
V8 compiles JavaScript down to ultra-fast machine code by making assumptions about object shapes (Hidden Classes). If a function always receives an object with { x: number, y: number }, V8 optimizes it. This is called a “Monomorphic” function.
If a React component dynamically deletes keys from its state (delete state.userId) instead of setting them to null, it changes the physical shape of the object. V8 detects this, throws away the optimized machine code, and falls back to slow, interpreted execution (a “Deopt”). To keep React fast, architects mandate that state object shapes must remain completely uniform throughout their lifecycle.
// BAD: Changes object shape, forces V8 engine de-optimization
const handleLogout = () => {
const newState = { ...user };
delete newState.token;
setUser(newState);
};
// GOOD: Preserves object shape, keeps V8 execution lightning fast
const handleLogout = () => {
setUser(prev => ({ ...prev, token: null }));
};
The naive approach to code splitting is lazily loading heavy components right when they render. This results in the user clicking a button, waiting for a network request, and staring at a blank screen or a jerky layout shift.
useLayoutEffect?Layout Thrashing (or Forced Synchronous Layout) happens when you repeatedly alternate between reading DOM measurements (like element.offsetHeight) and writing DOM mutations (like element.style.height = '100px') in a loop or across uncoordinated components.
When you read a measurement, the browser assumes the layout is clean. If you write a mutation, the layout is dirtied. If you read again immediately, the browser is forced to halt JS execution and synchronously recalculate the entire page layout to give you the correct measurement. Architects prevent this by enforcing strict separation: batch all read operations first, store the values, and execute all write operations afterward (often utilizing libraries like fastdom to orchestrate this).
6. Internals & Custom Renderers
react-reconciler package and how do you architect a custom renderer?React is fundamentally broken into two distinct parts: the Reconciler (which handles the Fiber engine, state updates, diffing, and component lifecycles) and the Renderer (which applies those updates to a specific environment, like the DOM or Native iOS).
The react-reconciler is an NPM package that exposes the core Fiber engine. To build a custom renderer, an architect feeds a HostConfig object into this reconciler. The reconciler handles all the complex state management, and whenever the Virtual DOM changes, it calls your HostConfig methods to mutate your specific environment.
ink library) that translates React JSX components (like <Box> and <Text>) into terminal escape sequences.
import ReactReconciler from 'react-reconciler';
// The "translation layer" between React and your custom environment
const HostConfig = {
createInstance(type, props) {
if (type === 'text') return new TerminalTextNode(props.children);
if (type === 'box') return new TerminalBoxNode(props);
},
appendInitialChild(parent, child) {
parent.appendChild(child);
},
// ... dozens of other required mutation methods
};
const CustomRenderer = ReactReconciler(HostConfig);
CustomRenderer.render(<App />, terminalRootContainer);
Unlike react-dom, which knows that <div> maps to document.createElement('div'), a custom renderer must maintain a dictionary or factory mapping string types to specific target classes.
In React Three Fiber (R3F), when you write <mesh />, the HostConfig.createInstance method catches the string “mesh”. It does not create an HTML tag; instead, it instantiates a new THREE.Mesh() object. When React updates a prop like position={[1, 2, 3]}, the renderer intercepts this and executes threeObject.position.set(1, 2, 3).
HostConfig object.The HostConfig is the contract between React’s pure JavaScript logic and the target host environment. It forces the developer to provide concrete implementations for Abstract UI concepts.
It contains lifecycle hooks that map directly to the Commit Phase. For example: createInstance (called when a new element is mounted), commitUpdate (called when props change), removeChild (called on unmount), and createTextInstance. By implementing these specific methods, you teach React how to draw, update, and erase things in an environment it natively knows nothing about.
In the legacy architecture, the JavaScript thread (running Hermes/JSC) and the Native UI thread (Java/Objective-C) were completely isolated. They could only communicate by passing messages over an asynchronous “Bridge”.
Every time React Native wanted to update a `
Fabric represents the total architectural rewrite of React Native. It completely eliminates the asynchronous JSON Bridge. Instead, it utilizes JSI (JavaScript Interface).
JSI allows C++ to expose native UI objects directly to the JavaScript engine’s memory space. JavaScript can now hold direct references to C++ Host Objects and call their methods synchronously. This allows React to mutate Native UI components exactly like it mutates the DOM in a browser—instantly and precisely—enabling flawlessly smooth 120 FPS animations without serialization overhead.
React’s Synthetic Event system (onClick, onChange) does not exist in react-reconciler; it is bundled exclusively within react-dom. If you build a custom renderer for HTML5 Canvas, a `
Architects must build a custom event delegation system. You attach native DOM mouse listeners to the global Canvas element. On click, you calculate the X/Y coordinates. You then use an algorithm (like Raycasting for 3D/WebGL, or geometric hit-testing for 2D) to figure out which of your React-managed instances intersects with those coordinates. Finally, you manually invoke the `onClick` prop stored on that instance.
React does not attach event listeners to individual DOM nodes to save memory. Instead, it uses Event Delegation. Prior to React 17, React attached a single event listener for every event type (click, keypress) directly to the global document object.
The React 17 Architectural Shift: React changed delegation from the document to the specific React Root Container (the div#root you pass to createRoot). This was a massive change for Micro-frontends. Previously, if you embedded a React 16 app inside a React 15 app on the same page, their document-level event listeners would clobber each other. Moving delegation to the root container safely isolates multiple React applications living on the exact same DOM tree.
react-dom?The Commit Phase is broken into three distinct sub-phases: Before Mutation, Mutation, and Layout.
During the Mutation Phase, React physically alters the DOM. It iterates over the Fiber nodes flagged with “Effect Tags” (calculated during the Render phase). It executes appendChild, removeChild, and updates className or style attributes. Crucially, this is also the exact moment React detaches old ref values and attaches new ref instances to the newly mutated DOM nodes.
Native DOM form events are highly fragmented across browsers (e.g., IE/Edge handling input differently than Safari). A native onInput event fires at different times than an onChange event depending on the OS.
React abstracts this chaos. When you use onChange in React, it isn’t simply binding to the native onChange. React’s internal event plugins monitor a combination of native events—input, keydown, keyup, paste, and proprietary browser events. It normalizes this data into a single, predictable SyntheticEvent, guaranteeing that an architect’s form logic behaves identically across all devices and browsers.
Yes. Fundamentally, React is not a UI library; it is an optimized state-machine engine that diffs trees over time. Any system that can be represented as a hierarchical tree of state can be managed by React.
react-hardware to control IoT devices (like Arduino or Raspberry Pi). Instead of writing imperative loops to manage physical hardware state, you represent hardware components as JSX. A change in React state gracefully toggles actual electrical currents on and off.
import { render } from 'react-hardware';
function BlinkingLED({ isBlinking }) {
// Instead of DOM nodes, this renderer maps to physical GPIO pins
return (
<pin pin={13} mode="OUTPUT" value={isBlinking ? 'HIGH' : 'LOW'} />
);
}
// Renders the state tree directly to the serial port connected to the Arduino
render(<BlinkingLED isBlinking={true} />, '/dev/tty.usbmodem1411');
7. Advanced Patterns
When building highly reusable components (like a Select Dropdown for a UI library), hardcoding every possible edge case via boolean props (e.g., closeOnSelect={false}) bloats the component.
The State Reducer pattern solves this by giving the component’s consumer direct access to intercept state transitions. The component manages its own state via an internal useReducer, but accepts a stateReducer prop. Before applying any state change, the component passes the proposed change to the consumer’s reducer, allowing the consumer to modify or completely cancel the update.
// Inside the reusable library component
function useSelect(stateReducer = (state, action) => action.changes) {
const [state, dispatch] = useReducer((state, action) => {
const changes = internalReducer(state, action);
// Inversion of Control: Let the user override our internal logic
return stateReducer(state, { ...action, changes });
}, initialState);
// ...
}
While Context is commonly used for global Dependency Injection (DI), it forces the component to be deeply coupled to the React tree, making isolated Unit Testing difficult. True DI in React is achieved via Component Injection (Render Props) or passing service interfaces directly as props.
By passing the service/component as a prop, the component becomes a pure orchestrator. It knows what to execute, but relies on the parent to define how it executes.
// Generic Dashboard component receives its dependencies as props
export function Dashboard({ LoggerService, AnalyticsAdapter, ChartComponent }) {
const handleLoad = () => {
LoggerService.info('Dashboard mounted');
AnalyticsAdapter.trackEvent('view_dashboard');
};
return (
<div onLoad={handleLoad}>
<!-- We don't care if this is a D3 Chart or a Chart.js Chart -->
<ChartComponent data={data} />
</div>
);
}
Standard state management stores the current state. Event Sourcing never mutates a state object; instead, it stores an append-only array of immutable events (e.g., [{ type: 'ADD_ITEM', id: 1 }, { type: 'CHANGE_QTY', id: 1, qty: 5 }]). The current UI state is derived on the fly by reducing (replaying) these events from the beginning.
Headless UI components provide zero markup and zero CSS. They exclusively encapsulate complex logic, state machinery, and W3C accessibility (ARIA) attributes. They expose this logic via custom hooks or the Render Props pattern.
The consumer uses the hook, receives an object of event handlers and ARIA attributes (like aria-expanded or onKeyDown), and manually spreads (...) them onto their own styled HTML elements, completely decoupling the brains from the beauty.
// The Headless Hook (Library Code)
export function useAccordion() {
const [isOpen, setIsOpen] = useState(false);
return {
isOpen,
triggerProps: {
onClick: () => setIsOpen(!isOpen),
'aria-expanded': isOpen,
'aria-controls': 'accordion-content',
},
contentProps: {
id: 'accordion-content',
hidden: !isOpen,
}
};
}
// The Consumer (App Code)
function MyStyledAccordion() {
const { triggerProps, contentProps } = useAccordion();
return (
<div className="tailwind-wrapper">
<button className="bg-blue-500" {...triggerProps}>Toggle</button>
<div className="p-4" {...contentProps}>Content inside</div>
</div>
);
}
Architects forbid hardcoding z-index: 9999. Z-indexes only work within their local Stacking Context (created by position: relative or transform). If a parent has z-index: 1, a child modal with z-index: 99999 will still be trapped underneath an adjacent sibling with z-index: 2.
The architectural solution is React Portals. You create a sibling <div id="portal-root"> at the absolute bottom of your <body>, completely outside your React app root. Whenever a Component needs to break out (Modals, Tooltips, Toasts), you use ReactDOM.createPortal(child, domNode). The logic stays in your component tree, but the physical HTML is injected at the end of the document, naturally rendering on top of everything without fighting Z-index wars.
Traditional state machines (like `useReducer`) are passive; they only calculate the next state when an event is dispatched. They cannot easily manage asynchronous side effects or time.
The Actor Model allows you to spawn independent “Actors” that run concurrently. An Actor maintains its own state and can send and receive asynchronous messages. If you build a complex flow using XState, the state machine itself can trigger an API call, wait for the response, transition to an ‘error’ state, and send a message back to the React component, completely decoupling orchestration logic from the view.
Heavy libraries (like react-beautiful-dnd) often bloat bundles. For many use cases, the native HTML5 Drag and Drop API is sufficient when orchestrated correctly with React state.
You apply the draggable={true} attribute to the source element. In onDragStart, you store the dragged item’s ID in React state. On the target container, you must call e.preventDefault() inside the onDragOver event; otherwise, the browser forbids dropping. Finally, in the target’s onDrop event, you read the dragged ID from state and trigger your reducer/state mutation to move the item.
If you build an Excel-like data grid with 1,000 cells and apply tabindex="0" to all of them, a user relying on a keyboard will have to press “Tab” 1,000 times to move past the table. This is an accessibility violation.
tabindex="0". Every other cell has tabindex="-1". When the user presses the Arrow Keys, a custom React hook intercepts the keystroke, updates the active coordinate state (X/Y), shifts the `tabindex=”0″` to the new cell, and executes `.focus()` programmatically. The user can traverse the whole grid via arrows, but pressing Tab skips the entire table instantly.
Wizards that rely on const [step, setStep] = useState(1) break immediately if the user accidentally hits the browser’s “Back” button (it navigates them completely away from the app instead of back one step). It also makes deep linking impossible.
Architects treat Wizards as Nested Routes or URL Search Parameters (e.g., /onboarding?step=profile). The central state (the accumulated form data) is held in a higher-order Context or Zustand store. The routing library handles the “Next” and “Back” branch logic by pushing new URL parameters, perfectly aligning the browser history with the Wizard steps.
Isomorphic (or Universal) components execute the exact same JavaScript code on the Node.js server to generate the initial HTML, and then again in the browser to hydrate the DOM.
The danger is Hydration Mismatches. If an isomorphic component renders new Date().getTime(), the Node server evaluates the time at 12:00 PM. The browser downloads the JS and evaluates the time at 12:01 PM. Because the browser’s virtual DOM (12:01) does not match the server’s HTML (12:00), React throws a hydration error and forces a costly synchronous re-render of the entire tree. Architects bypass this using useEffect (which only runs on the client) to set dynamic client-specific data after the initial consistent render.
8. Testing Strategy & CI/CD
Most React Unit tests run in JSDOM (via Jest or Vitest). JSDOM is a headless JavaScript implementation of the DOM, but it completely lacks a rendering engine. Therefore, layout APIs like `IntersectionObserver` and animation APIs like `requestAnimationFrame` literally do not exist or silently fail.
Architects solve this in two ways. The first is to mock the global API in the test setup file, manually triggering the observer callbacks to simulate scrolling. The better architectural shift is to move away from JSDOM entirely for visual components and use Cypress Component Testing or Playwright Component Testing, which mounts the isolated React component inside a real Chrome/Webkit browser engine, executing native layout APIs flawlessly.
// Vitest/Jest setup: Mocking IntersectionObserver for JSDOM
class MockIntersectionObserver {
constructor(callback) {
this.callback = callback;
}
observe(element) {
// Manually trigger the callback to simulate an element entering the viewport
this.callback([{ isIntersecting: true, target: element }]);
}
unobserve() {}
disconnect() {}
}
global.IntersectionObserver = MockIntersectionObserver;
Visual Regression Testing (via tools like Percy or Chromatic) renders your components in a headless browser, takes screenshots, and compares them pixel-by-pixel against a baseline image from the `main` branch. If a CSS change shifts a button 2 pixels, the test fails.
The main pitfall is brittleness and false negatives. Dynamic data (like rendering `new Date()`), slight OS font anti-aliasing differences (Mac vs. Linux CI servers), and CSS animations cause tests to fail constantly even when the UI is correct. Architects mitigate this by mocking all dates/times globally, forcing a specific random seed, and disabling all CSS animations (`* { animation: none !important; }`) injected during the test run.
You cannot test a component that connects to `ws://localhost:8080` in CI without spinning up a real backend server, which makes tests slow and flaky. You must intercept the native `WebSocket` object directly in the testing environment.
React Testing Library (RTL) traditionally mounts components in the browser (JSDOM). Because React Server Components execute strictly in Node.js and often contain raw database queries, rendering them in JSDOM fails instantly.
The Strategy: Do not try to unit test the rendering of an RSC. Instead, test the architecture in three layers:
- Unit Test: Extract the data-fetching logic and Server Actions into isolated pure functions and test them using standard Node/Vitest test runners.
- Component Test: Extract the interactive UI into Client Components and test them using RTL/JSDOM.
- E2E Test: Use Playwright to visit the actual route. This tests the integration—that the Server Component successfully passed the RSC payload to the Client Component.
If your application heavily uses `React.lazy` or Next.js `dynamic`, standard synchronous test runners will unmount the component before the lazy chunk finishes downloading. As a result, Istanbul (the coverage tool) reports 0% coverage on those dynamically imported files, even if they work perfectly.
Architects fix this by wrapping the render call in an asynchronous `act()` block and explicitly awaiting the resolution of the Suspense boundary using `findByText` or `findByTestId`. This forces the JSDOM event loop to wait for the mocked chunk to resolve, ensuring the execution path runs and coverage is accurately recorded.
Traditional coverage only tells you if a line of code was executed, not if the test is actually meaningful. A test without an `expect` assertion provides 100% coverage but catches zero bugs.
Mutation Testing (using tools like Stryker) tests the quality of your test suite. It automatically alters your source code (e.g., changing `amount > 10` to `amount < 10` or swapping `+` for `-`) and runs your test suite. If your tests pass despite the broken code, the mutation "survived," exposing a weak or missing assertion. If the test fails, the mutation was "killed," proving the test is robust.
Unlike REST APIs, where every endpoint has a unique URL (`/api/users`, `/api/orders`), GraphQL routes all traffic through a single POST endpoint (`/graphql`). You cannot simply mock the URL; you must intercept the network request, parse the JSON body, read the `operationName`, and return conditional mock data.
// Playwright GraphQL Mocking
await page.route('**/graphql', async (route) => {
const request = route.request();
const postData = JSON.parse(request.postData());
if (postData.operationName === 'GetProfile') {
// Intercept and return fake profile data
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ data: { user: { name: 'Admin', role: 'architect' } } })
});
} else {
// Let other queries (like analytics) pass through to the real server
await route.continue();
}
});
Manual heap snapshots are great for debugging, but memory leaks will continually regress without automated enforcement. Architects use Playwright connected to Chrome via the CDP (Chrome DevTools Protocol) to automate leak detection in CI.
Manual accessibility audits are slow. Architects enforce baseline a11y compliance by integrating the `jest-axe` library directly into the React Testing Library suite.
During a unit test, you render the component to the JSDOM, pass the resulting HTML container into the `axe()` function, and assert `toHaveNoViolations()`. This instantly catches missing `aria-labels`, invalid ARIA roles, duplicated IDs, and contrast issues before the code is ever merged.
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
it('should have no accessibility violations', async () => {
const { container } = render(<ComplexDataGrid data={mockData} />);
// Scans the DOM output against WCAG standards
const results = await axe(container);
expect(results).toHaveNoViolations();
});
E2E tests in Playwright or Cypress become “flaky” (randomly failing) due to asynchronous unpredictability: network latency, CSS animation delays, or slow React hydration. Architects enforce strict rules to eradicate flakiness:
- Never use hardcoded sleeps: Avoid `await page.waitForTimeout(2000)`. Always wait for explicit visual states, like `await expect(locator).toBeVisible()`.
- Mock third-party scripts: Block requests to Google Analytics, Intercom, or ad networks that mutate the DOM unexpectedly and slow down the page.
- Wait for Hydration: React might render the HTML, but clicks fail if JS hasn’t hydrated. Expose a global `window.__REACT_HYDRATED__ = true` flag on mount, and have the E2E test wait for this flag before interacting.
9. Webpack, Vite & Build Optimization
Webpack is a Bundler. When you start the dev server, it crawls your entire application tree, compiles every module, resolves dependencies, and packs them into a single massive JS file in memory before the browser can render anything. As the app grows, boot time crawls to a halt.
Vite utilizes Native ESM (ECMAScript Modules). It does not bundle the code during development. It serves your source code directly to the browser over HTTP as individual native ES modules. When the browser requests a specific file, Vite compiles only that file on demand using esbuild (written in Rust/Go). This makes local boot times near-instantaneous, regardless of the application’s overall size.
Tree Shaking (Dead Code Elimination) relies exclusively on static ES6 module syntax (import and export). During the build, the bundler statically analyzes the AST (Abstract Syntax Tree) to trace exactly which exports are used. Unused exports are stripped from the final bundle.
Libraries fail to tree-shake for two reasons: First, if they are compiled to CommonJS (require), which is dynamic and cannot be statically analyzed. Second, due to Side Effects. If a library file mutates a global object (e.g., window.MyPolyfill = true) just by being imported, the bundler cannot safely delete that file even if no functions from it are explicitly called. Architects fix this by enforcing the "sideEffects": false flag in the library’s package.json.
Shipping a monolithic 5MB `main.js` kills Time to Interactive (TTI). Architects implement strict chunking boundaries to maximize browser caching.
1. Framework Chunk: `react`, `react-dom`, and routing. This rarely changes and can be cached by the browser for a year.
2. Vendor Chunk: UI libraries (like MUI or Tailwind). Changes occasionally.
3. Feature Chunks: The actual application code, split aggressively by Route, which changes on every deployment.
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: (id) => {
if (id.includes('node_modules')) {
if (id.includes('react') || id.includes('react-dom')) {
return 'vendor-react'; // Framework chunk
}
return 'vendor'; // Other libraries
}
}
}
}
}
});
AST (Abstract Syntax Tree) tools intercept source code and rewrite it before the browser sees it. For React, this means compiling JSX into React.createElement or stripping out TypeScript types.
Historically, Babel (written in JavaScript) handled this. However, Babel is single-threaded and bound by JS execution speeds. Architects are moving to SWC (Speedy Web Compiler) because it is written in Rust. It executes natively on multi-core CPUs, transforming massive React codebases up to 20x faster than Babel, radically reducing CI/CD pipeline deployment times.
An offline-first architecture intercepts all network requests before they leave the browser using a Service Worker (usually generated via Google Workbox in the Webpack/Vite config).
The Service Worker applies specific caching strategies. For static React assets (JS/CSS chunks), it uses Cache First (serving files from the local disk instantly). For critical API data, it uses Network First, falling back to Cache. If the user loses connection on the subway, the API fetch fails, but the Service Worker intercepts the failure and returns the last known JSON payload from the Cache Storage API, allowing the React UI to remain fully functional.
Traditional HMR injected new JavaScript into the browser but often destroyed React component state because it couldn’t map the new code to the existing Virtual DOM nodes. You would save a file, and your filled-out form would suddenly blank out.
React Fast Refresh fixes this. It is deeply integrated with the React Reconciler. When a component file changes, the bundler sends the new function over a WebSocket. Fast Refresh instructs React to re-render that specific component subtree. Crucially, it matches the hook signatures; if the useState order hasn’t changed, React seamlessly maps the old state to the newly injected component logic without losing the user’s data.
WebAssembly is a binary instruction format that runs in the browser at near-native C++ speeds. JavaScript is heavily bottlenecked by garbage collection and JIT compilation during intense math operations.
import { useEffect, useState } from 'react';
export function WasmFilter() {
const [wasmModule, setWasmModule] = useState(null);
useEffect(() => {
// Asynchronously instantiate the WebAssembly binary
WebAssembly.instantiateStreaming(fetch('/imageProcessor.wasm'))
.then(obj => setWasmModule(obj.instance.exports));
}, []);
const applyFilter = (imageData) => {
if (!wasmModule) return;
// Execute C++ logic directly from React
wasmModule.applyBlur(imageData, imageData.length);
};
return <button onClick={applyFilter}>Apply Heavy Filter</button>;
}
Architects regularly run webpack-bundle-analyzer (or rollup-plugin-visualizer). A common massive issue is seeing two different versions of the same library (e.g., `lodash@4.1` and `lodash@4.17`) packed into the final build because two different third-party React components depend on strictly different versions.
To fix this and force a singleton instance, architects use the resolutions field in package.json (for Yarn) or overrides (for NPM). This forcibly overrides the dependency tree, instructing the bundler to resolve all requests for that library to one single, canonical version, stripping megabytes of duplicated code from the chunk.
When using `React.lazy`, the browser won’t even start downloading the chunk until the component is actually required to render, which causes a loading spinner.
Architects use Webpack Magic Comments to control browser priority heuristics. Adding /* webpackPrefetch: true */ to a dynamic import instructs Webpack to inject a <link rel="prefetch"> tag. The browser will silently download the chunk in the background only when it is completely idle. /* webpackPreload: true */ instructs the browser to download it immediately in parallel with the main bundle, reserving it for chunks required milliseconds after the initial load.
// Triggers a background download during browser idle time
const HeavyDashboard = React.lazy(() => import(
/* webpackPrefetch: true */
/* webpackChunkName: "dashboard-view" */
'./HeavyDashboard'
));
React executes in the user’s browser, which is a fundamentally insecure environment. Any environment variable accessed via process.env.API_KEY on the client is string-replaced during the build process and is fully visible to anyone who inspects the compiled JS file.
Architects enforce a strict prefixing strategy (like NEXT_PUBLIC_ in Next.js or VITE_ in Vite). The bundler is configured to only inject variables with this prefix into the client build. True secrets (database passwords, private API keys) are never prefixed, ensuring the bundler completely ignores them, keeping them safely confined to the secure Node.js server executing the Server Components or API routes.
10. Security & Edge Cases
dangerouslySetInnerHTML?React automatically escapes text strings to prevent XSS. However, if you are building a CMS or Markdown parser, you must inject raw HTML using dangerouslySetInnerHTML. This bypasses React’s security, allowing attackers to inject <script> tags or <img onerror="stealToken()"> payloads.
Architects mitigate this by strictly enforcing a Sanitization layer (like DOMPurify). DOMPurify parses the dirty HTML string, builds a DOM tree in memory, and violently strips out any tags, attributes, or event handlers that are not explicitly whitelisted before passing it to React.
import DOMPurify from 'dompurify';
export function MarkdownRenderer({ dirtyHtml }) {
// Never pass raw user input directly to React
const cleanHtml = DOMPurify.sanitize(dirtyHtml, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'h1'],
ALLOWED_ATTR: ['href'] // Only allow safe attributes
});
return <div dangerouslySetInnerHTML={{ __html: cleanHtml }} />;
}
localStorage vs. HttpOnly cookies in a React SPA.Storing a JWT in localStorage makes it incredibly easy for the React app to read the token and attach it to API headers. However, if a single XSS vulnerability exists anywhere in your app (or a third-party dependency), malicious JS can instantly read localStorage and steal the token.
HttpOnly Cookies solve this because the browser explicitly prevents JavaScript from reading the cookie. However, cookies automatically attach to every request, opening the app to CSRF (Cross-Site Request Forgery). An attacker can trick the user into clicking a link, and the browser will blindly attach the authentication cookie to the forged request. Architects solve this by using HttpOnly cookies paired with strict SameSite=Strict attributes and anti-CSRF header tokens.
A strict CSP prevents XSS by forbidding the browser from executing inline scripts (<script>alert(1)</script>) or inline styles.
React Server Components and SSR frameworks (like Next.js) often inject inline scripts to pass the hydration payload or manage styling (e.g., Styled Components). To comply with CSP, an architect must configure the server to generate a cryptographically secure random Nonce on every request. This nonce must be attached to the CSP HTTP header, and then passed into the React application so React can attach `nonce=”random_value”` to the inline script/style tags it generates.
DOM Clobbering is a legacy browser quirk where HTML elements with an id or name attribute are automatically mapped to properties on the global window object.
window.location for routing. An attacker injects <a id="location" href="malicious.com"> into a comment section. Because of DOM clobbering, `window.location` is overwritten. It no longer points to the native browser API; it points to the HTML anchor tag. When the React router attempts to read `window.location.pathname`, the app crashes or behaves unpredictably. Architects prevent this by avoiding global `window` reliance and strictly sanitizing user HTML.
If a modal opens and a user presses “Tab”, the focus will eventually leave the modal and highlight elements in the background UI. This is highly disorienting for screen readers and technically allows interaction with an “inactive” background state.
Architects implement a Focus Trap. You listen to the `keydown` event on the modal. If the user presses “Tab” while focused on the last focusable element in the modal, you execute `e.preventDefault()` and programmatically call `.focus()` on the first element in the modal, creating an infinite loop. Crucially, when the modal closes, you must restore focus to the exact button that originally opened the modal.
In a traditional website, clicking “Submit” reloads the page, and the screen reader reads the new page title (“Success!”). In a React SPA, clicking submit might just render a tiny green checkmark dynamically. A blind user will hear absolutely nothing and assume the form is broken.
Architects use aria-live regions. You place an invisible <div aria-live="polite" aria-atomic="true"> permanently in the DOM. When the React async action succeeds, you update the text inside this div. The browser detects the DOM mutation and instructs the screen reader to interrupt its flow and audibly announce the new text (“Form submitted successfully”) to the user.
When React Router swaps components, the DOM changes completely, but the browser’s focus often remains on the `
` element or gets lost in the void. A keyboard user has to manually tab all the way through the navigation bar again to reach the new page content.Architects enforce a global routing effect. On every route change, React must programmatically call .focus() on the top-level <h1> of the new page (which requires adding tabIndex="-1" to the H1 so it can receive programmatic focus). This instantly drops the screen reader or keyboard user precisely at the beginning of the new content.
target="_blank" attribute and the necessity of rel="noopener noreferrer".Historically, opening a link in a new tab via target="_blank" gave the newly opened page access to the window.opener object of the original page. This created a massive security vulnerability: the new page could maliciously execute window.opener.location = 'phishing-site.com', hijacking the user’s original tab without their knowledge.
React heavily warned developers to append rel="noopener noreferrer" to sever this connection. While modern browsers (Chrome 88+) now implicitly enforce `noopener` on `target=”_blank”`, architects still explicitly require it in React codebases to support legacy enterprise browsers and Safari versions.
React assumes total authority over its Virtual DOM tree. If an external library (like Google Maps, an Ad network, or jQuery) appends or deletes a child node inside a React-managed <div>, React will eventually attempt to update that node. Finding the expected DOM structure broken, React throws a fatal NotFoundError (or Invariant Violation) and crashes.
Architects isolate external mutations. You render a completely empty <div ref={containerRef} />. You use a useEffect to pass that DOM node to the third-party library, and you never pass React children into that div. Furthermore, you can use shouldComponentUpdate (or memoization) returning `false` to guarantee React never attempts to reconcile that specific subtree again.
Prototype Pollution occurs when developers use unsafe recursive merge functions (like deep-merging user payload data into Redux state). If an attacker sends a JSON payload with a key named __proto__, a naive merge function will traverse up to the global JavaScript Object.prototype and inject properties directly into it.
// Unsafe merge function causing Prototype Pollution
function unsafeMerge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object') {
if (!target[key]) target[key] = {};
// DANGER: If key is "__proto__", this alters the global Object prototype!
unsafeMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
React JS Interview question for Intermediate level
Elevate your skills with 100 intermediate-level React questions. Dive deep into Custom Hooks, Performance Optimization, Redux, Next.js SSR, and React 19 Concurrent patterns.
1. Advanced Hooks & Custom Hooks
useEffect runs asynchronously *after* the browser paints the screen. useLayoutEffect runs synchronously *before* the browser paints.The Why & How: React calculates the DOM mutations, runs
useLayoutEffect, and only then hands the final DOM to the browser to paint. Because it blocks the visual update, it should be used sparingly to avoid performance bottlenecks.Real-World Scenario: You are building a custom tooltip that needs to calculate its position based on the dimensions of a target button. If you use
useEffect, the user might see the tooltip render at the top left of the screen for a split second before it “jumps” to the correct spot (a flicker). useLayoutEffect measures the DOM and moves the tooltip before the user ever sees it.
useEffect) references an old state or prop variable because it captured the variable at the time it was created, not its current value.The Why & How: This usually occurs when you omit variables from a hook’s dependency array. To fix it, ensure all dependencies are listed, or use a functional state update (e.g.,
setCount(prev => prev + 1)) so the state setter doesn’t need to capture the current state from the outer scope.Real-World Scenario: You are building a strict 30-minute countdown timer for a hyper-local quick-commerce app. You set up a
setInterval inside a useEffect with an empty dependency array. If you try to do setTime(time - 1), the timer will drop by one second and freeze permanently because time is trapped at its initial value in that closure. Using setTime(prevTime => prevTime - 1) safely bypasses the stale closure.
useReducer is preferable when state logic is complex, involves multiple sub-values, or when the next state depends heavily on the previous state.The Why & How: It centralizes all state transitions into a single, predictable reducer function. This decouples the “how state updates” logic from the component rendering, making it much easier to write unit tests for the state logic independent of the UI.
Real-World Scenario: Managing a complex checkout form for a dark-store delivery app. Instead of having
setLoading, setError, setUserData, and setCartTotal scattered across multiple try/catch blocks, you dispatch a single action: dispatch({ type: 'CHECKOUT_SUCCESS', payload: data }). The reducer cleanly updates all four of those states in one centralized block.
useState (e.g., useState(() => computeHeavyValue())) instead of a direct value.The Why & How: During a re-render, React ignores the initial value passed to
useState. However, if that value is a heavy computation, JavaScript still executes the computation before React discards it. Lazy initialization ensures the function is *only* executed on the very first mount.Real-World Scenario: You need to initialize state by formatting a massive array of AI-generated astrology and numerology predictions. Doing
formatPredictions(hugeArray) directly inside useState causes a heavy synchronous computation on *every single keystroke* or re-render of that component. Lazy initialization prevents this CPU bottleneck.
The Why & How: With the introduction of Concurrent Mode in React 18, React can pause and resume rendering. If an external store updates *while* React is in the middle of a paused render, the UI could render with two different values for the same state (known as “tearing”). This hook forces synchronous updates to prevent tearing.
Real-World Scenario: Subscribing directly to a browser API, like
window.matchMedia for responsive design, or reading a user’s network status (navigator.onLine) for an offline-first delivery rider app. It’s also heavily used under the hood by state libraries like Redux and Zustand.
useMemo caches the result of a calculation. useCallback caches a function definition. They prevent unnecessary re-creations of references on every render.The Why & How: The anti-pattern is using them everywhere. Memoization has a cost (memory allocation and dependency array comparison). If the calculation is trivial, the overhead of
useMemo is worse than just recalculating it.Real-World Scenario: You have a heavily memoized
<ProductTable> component that receives an onDelete prop. If you don’t wrap the parent’s handleDelete function in a useCallback, a new function reference is created every time the parent renders, breaking the memoization of the child table and causing the entire table of 500 items to re-render unnecessarily.
ref.The Why & How: Usually, passing a
ref down (via forwardRef) gives the parent full access to the underlying DOM node (like an <input>). useImperativeHandle intercepts this, allowing you to hide the actual DOM node and only expose specific, controlled functions.Real-World Scenario: You build a complex, custom
<BottomSheet> component for a mobile-first app. The parent shouldn’t care about the internal DOM structure of the sheet. Using this hook, you can expose just two clean methods to the parent: sheetRef.current.open() and sheetRef.current.close().
The Why & How: In Concurrent React, a component might be rendered multiple times before those changes are actually committed to the screen (or the render might be aborted entirely). If you mutate a ref during render, you permanently alter state in a way that React cannot track or roll back.
Real-World Scenario: If you push an event to an analytics array via
ref.current.push(event) right inside the component body, Concurrent mode might run that code three times while preparing the UI, resulting in three duplicate analytics tracking events firing for a single page view.
The Why & How: If Request A is fired, then immediately Request B is fired, but Request B’s API is faster, Request B loads, and then Request A’s delayed response overwrites it. To fix this, you must invalidate previous requests when the effect re-runs.
Real-World Scenario: A search bar with autocomplete. A user types “A”, then “App”, then “Apple”. You fire three API requests. To prevent the “A” result from arriving last and overwriting “Apple”, you instantiate an
AbortController inside the useEffect. In the cleanup function, you call controller.abort(). This instantly cancels any pending, outdated network requests.
The Why & How:
* Component State (useState): Ephemeral UI state (is a modal open, text input values).
* Context/Global State (Zustand/Redux): State shared across wide, unrelated areas of the app (UI themes, user auth status, complex client-side multi-step wizards).
* Server State (React Query/SWR): Asynchronous data that originates from a backend database.
Real-World Scenario: In a delivery app, the typed delivery instructions go in Component State. The user’s JWT auth token goes in Context/Global State. The list of available restaurants is Server State handled by a data-fetching library that manages caching and background refetching.
2. Context API & State Management
The Why & How: React Context does not have a native mechanism to subscribe to a *slice* of the context object. If your context holds
{ theme: 'dark', user: { name: 'Alex' } }, updating the theme will forcefully re-render the <ProfileHeader /> component, even if it only cares about the user object.Real-World Scenario: Placing your entire application state inside a single monolithic
<AppStateContext>. If a user types into a deeply nested search input tied to that context, the entire component tree updates on every single keystroke, causing massive UI lag.
The Why & How: Create logically isolated contexts based on domain (e.g.,
AuthContext, ThemeContext). Furthermore, separate the state from the functions that update it. Create a UserValueContext and a UserDispatchContext.Real-World Scenario: You have a “Logout” button deep in a nested settings menu. By wrapping it in
UserDispatchContext, it only receives the logout() function. When the user’s data changes, the button component does not re-render because it is entirely decoupled from the UserValueContext.
The Why & How: Reach for Redux (or Zustand) when you have high-frequency state updates, complex state transition logic that benefits from middleware, or when you need granular selectors (e.g.,
useSelector(state => state.cart.total)) to prevent unnecessary re-renders natively.Real-World Scenario: Building a dashboard for a quick-commerce dark store with real-time inventory updates streaming in via WebSockets. Context would buckle under the rapid updates, causing continuous full-tree renders. Redux allows specific table rows to subscribe directly to specific slices of the inventory data stream without impacting the rest of the UI.
The Why & How: Legacy Redux required massive boilerplate: separate actions, action creators, constants, and complex immutable reducers. RTK uses “slices” to auto-generate actions and action creators. Most importantly, it integrates
Immer.js under the hood.Real-World Scenario: In legacy Redux, updating a nested user address required agonizing spread operators:
return { ...state, user: { ...state.user, address: { ...state.user.address, city: 'Delhi' } } }. With RTK, you just write state.user.address.city = 'Delhi'. RTK safely translates this mutable syntax into immutable updates behind the scenes.
The Why & How: Traditionally, developers fetched data in
useEffect and dispatched it to a Redux store. This meant writing loading, success, and error states manually. Server-state libraries handle caching, deduplication of duplicate requests, background polling, and cache invalidation automatically.Real-World Scenario: You have a
ProductList and a SidebarSummary that both need the same /api/products data. With React Query, both components can call useQuery('products'). The library is smart enough to only send *one* network request to the backend, sharing the cached response with both components instantly.
The Why & How: Because the store sits entirely outside the React component tree, it circumvents the React Context rendering rules. It has virtually no boilerplate compared to Redux and allows components to subscribe to partial state easily using selectors.
Real-World Scenario: You need a global toggle for a side navigation drawer. Bootstrapping Redux for a simple boolean is overkill. Zustand allows you to create a global store in 5 lines of code, and access it anywhere using a simple hook, keeping the codebase lean and highly performant.
The Why & How: Atoms can be dynamically created and linked together. Components subscribe only to the specific atoms they need. When an atom updates, only the components subscribed to that specific atom re-render.
Real-World Scenario: You are building an interactive canvas application (like Figma or Excalidraw). Each shape on the canvas has its own X/Y coordinates. Storing 10,000 shapes in one Redux object means moving *one* shape recalculates the whole state tree. Making each shape an independent Recoil atom allows you to move one shape at 60fps without touching the others.
The Why & How: It only becomes an anti-pattern when props are passed through many layers of intermediary components that do not need the data, creating a brittle and hard-to-refactor codebase.
Real-World Scenario: Passing a
userId from a <Dashboard> down to a <UserProfile> and then to a <UserAvatar> is perfectly fine. But if you have to pass it through 10 generic layout wrappers that don’t care about the user, you should use Context or Component Composition (passing the Avatar as a children prop) instead.
The Why & How: Hooks like
useContext require a React render cycle to function. To share data with non-React files, you must use an external state manager (like Redux or Zustand stores, which expose getState() outside the tree) or pass the context value as an explicit argument to the utility function when called from within a component.Real-World Scenario: You need to attach a JWT token to every outgoing Axios request. You cannot call
useAuthContext() inside api.js. Instead, you create a Zustand store, and in your api.js file, you call useAuthStore.getState().token to retrieve the token outside of the React lifecycle.
The Why & How: Writing custom
useEffect hooks to read/write to storage is error-prone and can cause hydration mismatches in SSR. It is best to use built-in middlewares like Redux Persist or Zustand’s persist middleware. These handle serializing state to storage on change and hydrating it on initial load automatically.Real-World Scenario: A user adds items to a shopping cart for a 30-minute delivery window but accidentally refreshes the page. By using Zustand’s persist middleware wrapped around the cart store, the state is automatically saved to
localStorage. On refresh, the middleware rehydrates the cart instantly before the first render, preventing data loss.
3. Performance Optimization
React.memo performs a shallow comparison of all current and next props using Object.is().The Why & How: It checks if the memory references of the props have changed. If the parent renders but passes down the exact same prop references, the memoized child skips its render phase and reuses its last DOM output.
Real-World Scenario: You have a heavy
<DataGridRow> component in a financial dashboard. If the specific row’s data hasn’t changed, React.memo stops the row from re-rendering when the global market ticker updates the parent component’s state.
React.memo(Component, arePropsEqual).The Why & How: This function takes
prevProps and nextProps. You return true if they are functionally equivalent (which skips the render) and false if they differ (which triggers the render). This overrides the default shallow comparison.Real-World Scenario: A
<UserProfileCard> takes a massive user object prop, but only physically displays the avatarUrl. You write a custom comparison: (prev, next) => prev.user.avatarUrl === next.user.avatarUrl. Now, if the user’s lastLoginTime updates in the background, the card won’t waste cycles re-rendering.
The Why & How: Because
React.memo uses shallow comparison, it sees that {} !== {}. It interprets the inline object as a completely “new” prop and forcefully re-renders the memoized component anyway, defeating the purpose of the memoization.Real-World Scenario: Passing
style={{ marginTop: '10px' }} directly to a memoized <Button>. On every parent render, a new style object is minted. To fix this, you must extract the object outside the component or wrap it in useMemo.
The Why & How: If a computation is incredibly fast (like basic string concatenation or mapping a 10-item array), the engine overhead of running
useMemo is actually slower and uses more memory than just recalculating the value on every render.Real-World Scenario: Doing
useMemo(() => users.map(u => u.name), [users]) on an array of 5 active users. The optimization overhead is worse than the task. Reserve useMemo for sorting arrays of 5,000+ items or parsing complex datasets.
The Why & How: Instead of the browser natively scrolling thousands of DOM elements, virtualization libraries (like
react-window) calculate the math of the scroll position, absolutely position the visible items, and recycle DOM nodes as they scroll out of view.Real-World Scenario: Building a Slack-like chat application where a channel might have 100,000 messages in history. Rendering 100k DOM nodes will crash a mobile browser. Virtualization keeps exactly 20 message DOM nodes on screen at any given time, maintaining flawless 60fps scrolling.
The Why & How: You wrap parts of your app in
<Profiler id="Name" onRender={callback}>. React calls the callback with data detailing the “commit” time, helping you identify which components render too often or take too long.Real-World Scenario: Users report the “Checkout” page is lagging on older phones. You wrap the form in a Profiler and discover that typing in the “Discount Code” field is causing a heavy
<ShippingMap> component to take 150ms to re-render on every keystroke. You then isolate the input state to fix the lag.
The Why & How: Often spotted by a rising heap size in Chrome DevTools or the classic “Can’t perform a React state update on an unmounted component” warning. You fix them by returning a strict cleanup function inside your
useEffect.Real-World Scenario: A
<StockTicker> sets up a setInterval to fetch prices every 5 seconds. The user navigates to a new page, destroying the component, but the interval keeps running in the background, consuming CPU and trying to call setPrice. Returning () => clearInterval(id) in the effect guarantees the leak is plugged.
The Why & How:
React.lazy dynamically imports a component only when it actually mounts. <Suspense> wraps it to show a fallback UI (like a spinner) while the browser fetches that specific JavaScript chunk over the network, drastically shrinking the initial bundle size.Real-World Scenario: An e-commerce app has a massive 3D
<ProductCustomizer> powered by Three.js. You don’t want the 90% of users who are just browsing to download that 2MB JavaScript payload. React.lazy ensures the 3D engine is only fetched over the network if a user explicitly clicks the “Customize in 3D” button.
The Why & How: The Render Phase is where React calls your component functions to calculate changes (Virtual DOM diffing). This phase is pure and, in Concurrent Mode, can be paused or aborted. The Commit Phase is where React synchronously applies those calculated changes to the actual browser DOM (mutating nodes). This cannot be interrupted.
Real-World Scenario: You trigger an urgent state update (typing) while a slow background list is filtering. React interrupts the list’s Render Phase, completely processes your keystroke through its own Render and Commit phases, and then calmly restarts the list’s Render Phase in the background.
The Why & How: If state lives at the top of a tree, any change forces the entire tree below it to re-render. Moving state exclusively into the child component that needs it physically isolates the render boundary.
Real-World Scenario: You have a
<GlobalLayout> containing a massive sidebar and main content. Putting an isHovered state for a single sidebar button at the layout level re-renders the whole page whenever the user moves their mouse. Moving that useState explicitly inside the individual <SidebarButton> component makes the hover effect perfectly isolated.
4. Component Patterns & Architecture
The Why & How: While custom hooks have largely replaced HOCs for logic sharing, HOCs are still superior for cross-cutting structural concerns that completely wrap or conditionally block UI rendering.
Real-World Scenario: You have 15 different dashboard pages. Instead of putting auth-checking hooks in every single file, you wrap them:
export default withRoleGating(FinanceDashboard, ['ADMIN']). The HOC automatically intercepts the render, checking permissions, and returning a 403 redirect if they fail, keeping the Dashboard component perfectly clean.
children prop), allowing the parent to manage state and the child to dictate rendering.The Why & How: It solves the same logic-sharing problem as hooks but operates entirely within the JSX tree. While custom hooks are cleaner for raw data, Render Props are excellent when complex internal logic is heavily coupled to highly customizable UI layouts.
Real-World Scenario: Building a
<VirtualList>. The parent component calculates the complex scroll math and visible indices, but it leaves the visual UI completely up to the consumer: <VirtualList data={items}>{ (item) => <MyCustomRow data={item} /> }</VirtualList>.
The Why & How: It provides a highly flexible, declarative API. The parent component manages the state, and child components read it, freeing developers from passing a dozen messy config props to a single monolithic component.
Real-World Scenario: A generic
<Tabs> component. Instead of writing <Tabs config={[{title: 'A', content: '...'}]} />, you architect it as: <Tabs> <Tabs.List> <Tabs.Tab>A</Tabs.Tab> </Tabs.List> </Tabs>. This allows consumers to inject custom icons or rearrange layouts without touching the internal state logic.
The Why & How: You initialize internal state, but on every render, you check if the parent passed a specific controlled prop. If the prop exists, it overrides the internal state, giving the consumer maximum flexibility.
Real-World Scenario: Building a reusable
<Accordion> for a design system. Team A just drops it in and lets it handle its own open/close clicks. Team B wants to sync the Accordion with a URL hash so users can bookmark it. Team B passes <Accordion isOpen={urlHash === 'details'} onChange={updateUrl} />, cleanly hijacking the internal state.
The Why & How: Hooks allowed us to easily co-locate data fetching inside UI components. However, tightly coupling complex API fetching directly inside complex CSS grid layouts makes the UI nearly impossible to unit test or view in isolation.
Real-World Scenario: You are writing a
<UserProfile>. Instead of putting Axios calls right inside the complex layout, you create a <UserProfileContainer> that calls useQuery, handles loading states, and passes the raw data to a pure, “dumb” <UserProfileView>, making the view easily renderable in Storybook.
ReactDOM.createPortal(child, domNode) allows you to render a component’s HTML into a completely different part of the actual browser DOM tree.The Why & How: It solves critical CSS stacking context (z-index) and
overflow: hidden issues while keeping the component logically attached to the React tree (so context and events still bubble up normally).Real-World Scenario: A “Delete Account” confirmation Modal is triggered deep inside a
<SettingsCard> that has overflow: hidden styling. Without a portal, the modal gets cut off by the card’s boundaries. Portaling the modal directly to document.body ensures it renders cleanly as an overlay across the entire application.
children prop.The Why & How: It is highly effective for strict layout components where you need specific content injected into designated, non-sequential template areas.
Real-World Scenario: An enterprise
<DashboardLayout> needs a sidebar, a header, and main content. Instead of confusing children logic and mapping, you design it with slots: <DashboardLayout leftSidebar={<AdminNav />} header={<AppHeader />}> <MainChart /> </DashboardLayout>.
The Why & How: Just like a recursive mathematical function, you MUST define a base case (a stop condition) to prevent infinite loops, usually by checking if a
children array is empty or undefined before rendering the next layer.Real-World Scenario: Building a VS Code style file explorer sidebar. You build a
<FileTree node={folderData} />. If folderData has a children array, the component maps over it and returns <FileTree node={child} /> for each one, dynamically drilling down until it hits a simple file node.
as or component), while maintaining its unified styling and behavior.The Why & How: Built using a dynamic tag element (e.g.,
const Component = as || 'div'). This pattern is absolutely crucial for maintaining semantic HTML and accessibility in design systems.Real-World Scenario: A standardized
<Button> component. Sometimes it submits a form, sometimes it navigates routes. Using <Button as="a" href="/home">Home</Button> renders a proper anchor tag for SEO and screen readers, but applies the exact visual CSS of the standard application button.
The Why & How: Every time the parent component renders, a brand new function (and therefore a brand new component reference) is created in memory. React sees it as a completely different component type. It unmounts the old DOM, destroys all local state, and heavily hits the CPU to mount a new one.
Real-World Scenario: You casually define
const UserRow = () => <tr>...</tr> inside a <Table> component. When the table sorts, the parent renders. Instantly, all inputs inside UserRow lose focus, and all local state (like a checked “select” box) is permanently wiped out because the DOM nodes were entirely destroyed and rebuilt from scratch.
5. React Router & Navigation
useNavigate triggers navigation actions, while useLocation reads the current routing state.The Why & How:
useNavigate returns a function allowing you to programmatically mutate the browser’s history stack (pushing or replacing routes). useLocation returns an object representing the current URL, which is often used inside `useEffect` dependency arrays to trigger logic when the route changes.Real-World Scenario: You use
navigate('/success') to redirect a user after a successful Stripe payment completes. You use useLocation() to track page views in Google Analytics every time the location.pathname updates.
The Why & How: URL Parameters are dynamic path segments defined in your route config (e.g.,
/users/:id) and accessed via useParams(). Query Parameters are key-value pairs appended after a question mark (e.g., ?sort=asc) and accessed via useSearchParams().Real-World Scenario: In an e-commerce app,
/products/9876 (URL Param) tells the app *which* exact shoe to fetch from the database. /products?color=red&size=10 (Query Params) tells the app to fetch a list of products and *filter* them.
The Why & How: You create a wrapper component (e.g.,
<RequireAuth>) that checks the user context. If authenticated, it renders <Outlet />. If not, it returns a <Navigate to="/login" replace /> component to bounce them away without leaving a broken history entry.Real-World Scenario: A user tries to access
/admin/billing while logged out. The <RequireAuth> wrapper intercepts them, saves /admin/billing in the routing state, and redirects them to /login. After logging in, the auth flow reads the saved state and seamlessly redirects them back to their intended destination.
<Outlet /> is a placeholder inside a parent route layout where matched child routes are rendered.The Why & How: It allows you to build complex, persistent UI shells (like headers and sidebars) that do not unmount or re-render when a user navigates between sub-pages, vastly improving performance and state retention.
Real-World Scenario: You have a
<DashboardLayout> with a heavy, collapsible sidebar. Inside the layout, you put an <Outlet />. When the user clicks from “Settings” to “Profile”, only the component inside the Outlet is swapped out. The sidebar’s open/close state is perfectly preserved because the parent layout never unmounted.
The Why & How: Traditionally, a component mounts, shows a spinner, runs a
useEffect, and then shows data (a waterfall). `loader` functions execute in parallel with the routing transition. React Router pauses the UI, fetches the data, and only renders the new page once the data is fully ready.Real-World Scenario: Clicking a link to a blog post. With loaders, the URL changes, but the page doesn’t tear down immediately to show a blank loading screen. The router fetches the post in the background, and the UI snaps instantly to the fully populated blog post.
The Why & How: When a user submits a `
The Why & How: You define a route with a splat wildcard path:
<Route path="*" element={<NotFound />} />. It must be placed at the very bottom of your routing configuration so it only triggers if all other specific route matches fail.Real-World Scenario: A user gets a link in an email but accidentally deletes the last letter (
/campaign/sale-202 instead of 2024). Instead of a blank white screen, the splat route catches them and renders a branded 404 page featuring a search bar and a “Return Home” button to prevent user drop-off.
The Why & How: You should *always* default to declarative
<Link> components for navigation because they generate valid HTML <a> tags, which are crucial for screen readers, SEO, and allowing users to “right-click -> open in new tab”. Programmatic navigate() should be strictly reserved for side-effects of business logic.Real-World Scenario: An architect will reject a PR that uses
onClick={() => navigate('/about')} on a `div` styled as a button. navigate() should only be used inside a .then() block after an API call, like successfully submitting a multi-step wizard and pushing the user to a dashboard.
<NavLink> is a specialized version of <Link> that has implicit awareness of the current URL state.The Why & How:
NavLink automatically provides an isActive boolean to its `className` or `style` props if its `to` path matches the browser’s current URL. This eliminates the need for you to manually compare location.pathname against strings.Real-World Scenario: Building a site header. Using
<NavLink to="/dashboard"> allows you to trivially apply a bold font and blue underline to the “Dashboard” menu item exclusively when the user is actually viewing the dashboard.
BrowserRouter relies on the server; HashRouter relies entirely on the client.The Why & How:
BrowserRouter uses the modern HTML5 History API for clean URLs (/about), but requires a backend configured to redirect all requests to `index.html`. HashRouter injects a hash (/#/about), ensuring the server ignores everything after the hash, treating it as a single page request.Real-World Scenario: Use
BrowserRouter for all standard production deployments (Vercel, AWS, Netlify). You only fallback to HashRouter when deploying to environments where you cannot configure server rewrite rules, like a raw S3 bucket without CloudFront, or packaging the app via Electron.
6. Error Handling & Boundaries
The Why & How: Before boundaries, a single syntax error in a deeply nested component would unmount the entire React tree, leaving the user with a blank white screen. Boundaries act like a massive `try/catch` block for declarative rendering.
Real-World Scenario: A third-party mapping widget receives a malformed JSON payload and throws a `TypeError` while rendering. The Error Boundary catches it, replacing the broken map with a “Map Unavailable” graphic, while keeping the rest of the dashboard (nav bars, metrics) perfectly functional.
The Why & How: To build a boundary from scratch, you must use
static getDerivedStateFromError and componentDidCatch, which only exist on `React.Component` classes. Real-World Scenario: While you must use a class under the hood, modern architectures rarely write them from scratch anymore. Teams rely on standardized wrappers like the
react-error-boundary library so they can stay entirely within functional paradigm workflows.
The Why & How: Boundaries only catch errors thrown during the render phase, inside lifecycle methods, and in constructors. They do *not* catch errors inside event handlers (like button clicks), asynchronous code (like `setTimeout` or `fetch`), server-side rendering, or errors thrown within the boundary component itself.
Real-World Scenario: If a user clicks “Submit” and your
onClick handler tries to read user.address.zipcode when address is null, the handler will throw an error. The Error Boundary will completely ignore this. The UI won’t crash, but the button will silently fail to do anything.
The Why & How: Because Error Boundaries ignore event handlers, you must proactively wrap risky logic in
try/catch blocks and manually update component state to reflect the failure.Real-World Scenario: Inside a checkout button’s
handlePayment async function, you wrap the API call in `try/catch`. In the `catch` block, you call setPaymentError(err.message), which triggers the component to conditionally render a red alert box above the form.
The Why & How: It is invoked synchronously after an error is thrown in a descendant. It receives the error object and its sole purpose is to return a new state object (e.g.,
{ hasError: true }). This state update forces the boundary to re-render, displaying the fallback UI instead of the crashed children.Real-World Scenario: As soon as the child tree panics, this static method intercepts the crash, flips the internal `hasError` switch, and the component’s `render()` method immediately returns the “Oops, something went wrong” component to the screen.
The Why & How: It is invoked *after* the fallback UI has been rendered. It receives the error and, crucially, the React component stack trace (showing exactly where in the JSX tree the error occurred). It should never be used to update state.
Real-World Scenario: In an enterprise app, you use this method to quietly format the stack trace and
POST it to Datadog or Sentry. This allows the engineering team to receive an instant slack alert detailing exactly which component blew up in production without relying on user bug reports.
The Why & How: It provides a simple
<ErrorBoundary> component that accepts a `FallbackComponent` prop. More importantly, it provides the useErrorBoundary hook, allowing functional components to imperatively throw errors to the nearest boundary.Real-World Scenario: Instead of writing a 50-line class component, you just wrap a feature:
<ErrorBoundary FallbackComponent={ChartErrorState}> <HeavyChart /> </ErrorBoundary>.
The Why & How: An error boundary is stuck in its fallback state until it unmounts. To clear it, you must provide a mechanism to reset its internal state (setting `hasError` to false) and simultaneously reset the conditions that caused the error.
Real-World Scenario: A data grid crashes due to a temporary network timeout. The boundary catches it and displays a “Failed to load data. [Try Again]” button. Clicking the button calls the boundary’s reset function, which clears the error state and forcefully re-mounts the data grid, giving the network request a second chance to succeed.
The Why & How: Relying on a single top-level boundary means any minor crash takes down the entire application. Architecturally, you should have a global boundary for catastrophic routing failures, but wrap distinct features in localized boundaries to isolate crashes.
Real-World Scenario: Building a complex layout like Figma. The toolbars, the canvas, and the layer sidebar should all have independent boundaries. If a user installs a buggy plugin that crashes the layer sidebar, that sidebar shows an error state, but the user can still safely save their work on the main canvas.
The Why & How: Since boundaries ignore async `fetch` errors, you can catch the error in your promise chain, and then intentionally pass it to a React state setter function configured to throw.
Real-World Scenario:
const [_, setError] = useState(). In your fetch catch block, you execute: setError(() => { throw new Error(err) }). Because state setter callbacks are executed during React’s render phase, throwing inside it successfully trips the Error Boundary layout higher up the tree.
7. Data Fetching & Asynchronous UI
The Why & How: Writing a
fetch inside a useEffect means you have to manually code loading states, error states, and cache storage. Libraries like React Query handle deduplication of identical requests, background revalidation, retry logic, and pagination automatically out of the box.Real-World Scenario: A user clicks between the “Home” tab and “Profile” tab rapidly. With
useEffect, the app makes a new API call every single time they click “Profile”, showing a loading spinner. With React Query, the data is cached; clicking “Profile” shows the UI instantly, while the library quietly checks the server in the background for updates.
The Why & How: Network latency is unpredictable. If Request A (slow) is fired, and then Request B (fast) is fired, B might arrive and set the component state first. Then A arrives late and overwrites B, leaving the UI displaying stale or incorrect data.
Real-World Scenario: You have a dropdown to filter users by department: “Sales” then “Engineering”. You click Sales, the API is slow. You quickly click Engineering, the API is fast, and the UI populates with Engineers. A second later, the delayed “Sales” data arrives and overwrites the UI, even though the dropdown still visibly says “Engineering”.
AbortController.The Why & How: You instantiate an
AbortController inside the effect and pass its `signal` to the fetch request. Crucially, you call controller.abort() inside the useEffect cleanup function. When the dependency array changes (e.g., the user changes the filter), the cleanup function runs, immediately killing the pending network request before the new one fires.Real-World Scenario: Going back to the department filter example: When the user clicks “Engineering”, the effect’s cleanup function runs, which instantly cancels the hanging “Sales” network request at the browser level, ensuring it can never resolve and corrupt your UI state.
The Why & How: It eliminates the need for `if (isLoading) return
<Suspense fallback={<SkeletonLoader/>}> catches this suspension and displays the UI.Real-World Scenario: A dashboard has three distinct widgets: Sales, Traffic, and Alerts. Instead of three separate spinners jumping around, you wrap all three in a single
<Suspense> boundary. The dashboard shows one clean, unified loading skeleton until *all* the widgets have fetched their data.
The Why & How: It makes the app feel instantly responsive. You mutate the local cache immediately. If the subsequent `POST` or `PUT` request to the server fails, you catch the error and cleanly “roll back” the UI cache to its previous state.
Real-World Scenario: Clicking the “Like” heart on a tweet. The heart turns red instantly. In the background, an API request is fired. If you lose internet and the request fails, the app quietly rolls back the heart to gray and shows a tiny “Failed to like” toast, rather than making you wait 500ms for the heart to turn red.
The Why & How: When a component mounts, SWR checks its memory cache. If data exists, it returns it instantly (the UI renders). Concurrently, it sends a network request to the server (revalidate). Once the fresh data arrives, it silently updates the cache and triggers a re-render only if the data actually changed.
Real-World Scenario: Opening a cryptocurrency portfolio app. You instantly see your balances from your last session 2 hours ago (Stale). Half a second later, the numbers tick up or down to reflect the live market prices (Revalidate). The user never sees a loading spinner.
IntersectionObserver API with pagination state.The Why & How: Listening to the `window.onscroll` event is terrible for CPU performance. Instead, you place an empty
<div id="load-more-trigger"> at the bottom of your list. You use an Intersection Observer to watch that div. When it enters the viewport, it triggers a fetch for `page + 1` and appends the results to your state array.Real-World Scenario: Building an Instagram-style feed. You render 10 posts. At the bottom, you have an invisible trigger div. As the user scrolls past post 8, the trigger enters the screen, firing the API for the next 10 posts. They append seamlessly, creating a frictionless scrolling experience.
The Why & How: You initialize the `new WebSocket(url)` inside a
useEffect with an empty dependency array to ensure it only connects once. You bind the `onmessage` event to update your state. Crucially, the cleanup function *must* call `socket.close()`.Real-World Scenario: A live chat widget. If you don’t close the socket in the cleanup function, and the user navigates away from the chat page and comes back, a second WebSocket connection is opened. Now, every time someone sends a message, your app receives it twice, causing duplicate messages in the UI.
The Why & How: REST libraries cache the exact JSON string returned by a specific URL. Apollo Client unpacks GraphQL responses, looks at the
__typename and id of every object, and stores them in a flat, normalized lookup table. This ensures extreme data consistency across the entire app.Real-World Scenario: Your
<Header> queries for User { id: 1, name: "Alex" }. Your <SettingsPage> fires a mutation changing the name to “Alexander”. Because Apollo normalizes by ID, the moment the mutation succeeds, the Header automatically re-renders with the new name without needing to be manually refetched.
The Why & How: If you fetch data `onChange`, a fast typist spelling “React” triggers 5 distinct API calls in 200ms. Debouncing uses a timeout to delay the API call until the user has stopped typing for a specific duration (e.g., 300ms).
Real-World Scenario: You implement a custom hook `useDebounce(searchTerm, 300)`. The hook returns a delayed version of the search term. You put that *debounced* term in your
useEffect dependency array. The user types “React”. The state updates instantly, but the network request only fires once, 300ms after they hit the “t”.
8. React 19 Intermediate Features
The Why & How: Normally, React state updates are urgent and lock the UI until finished. By wrapping a heavy state update inside
startTransition(() => setList(heavyData)), React will process it in the background. If the user clicks a button while it’s processing, React pauses the heavy task, handles the click, and resumes the task.Real-World Scenario: A search page with a massive map. Typing in the search input is urgent (you want to see the letters instantly). Filtering the 10,000 map pins is non-urgent. You wrap the map filtering state update in a transition. The user can type fluidly without the map calculation causing the keyboard to freeze.
The Why & How:
useTransition wraps the actual state setter function, meaning you use it when you *own* the state update. useDeferredValue wraps a value passed down as a prop, meaning you use it when you are receiving data from a parent and cannot control how the state was originally set.Real-World Scenario: If you own the search input and the map, use
useTransition on the input change handler. If you are just building the <MapDisplay pins={data} /> component and receiving data from a generic layout wrapper, you use const deferredPins = useDeferredValue(pins) to protect your component from blocking the main thread.
The Why & How: When a non-urgent task (a transition) starts, React begins building the new DOM tree in memory (the “work-in-progress” tree). If an urgent task (like a click) occurs, React completely throws away the half-finished background tree, instantly builds and commits a tree for the urgent click, and then starts the background task over from scratch.
Real-World Scenario: Hovering over a complex tooltip triggers a massive graph to render in the background. If the user moves their mouse away before the graph finishes rendering, React simply aborts the calculation. The main thread is never blocked, and no wasted DOM mutations occur.
use() is a special API that allows you to directly read the value of a Promise (or Context) during the render phase.The Why & How: Unlike hooks,
use() can be called conditionally (inside `if` statements). When you pass a Promise to it, it hooks directly into Suspense, pausing the component until the promise resolves. It completely replaces the need for basic useEffect fetching and `isLoading` state variables.Real-World Scenario: A parent Server Component passes a pending
userPromise down to a Client Component. Instead of setting up effects, the client simply calls const user = use(userPromise). The component suspends, the nearest boundary shows a spinner, and when the promise resolves, the component renders the user data.
<title>, <meta>, and <link> tags from anywhere deep within the component tree.The Why & How: Previously, you needed third-party libraries like `react-helmet` to manage head tags. Now, if a deeply nested
<BlogPost> component renders a <title>My Post</title>, React automatically intercepts it and “hoists” it up into the document’s actual <head> tag.Real-World Scenario: You are building a dynamic e-commerce catalog. Inside the
<ProductDetail> component, you directly render <meta name="description" content={product.summary} />. React handles moving it to the document head for perfect SEO, without requiring you to pass that data up 10 levels to a layout component.
<form action={...}> props.The Why & How: You can pass a server-side function directly to a form’s action prop. React handles the API layer, the serialization, and the network request automatically. If JavaScript is disabled or still loading (Progressive Enhancement), the browser falls back to a standard form POST, ensuring the feature always works.
Real-World Scenario: A “Subscribe to Newsletter” form at the bottom of a heavy landing page. A user scrolls down and hits submit before the heavy 2MB React JS bundle finishes executing. Because it’s a Server Action, the browser intercepts the submit and sends the data to the server anyway, never losing a conversion.
The Why & How: Calling
preload('https://fonts.../style.css', { as: 'style' }) or prefetchDNS('https://api.backend.com') during a render phase injects the proper resource hints into the document head, allowing the browser to resolve domains or fetch assets during idle time.Real-World Scenario: A user hovers their mouse over a “View 3D Model” button. You trigger a
preload() for the massive Three.js script. By the time they actually click the button a half-second later, the script is already halfway downloaded, drastically reducing the perceived loading time.
forwardRef Higher-Order Component has been deprecated. Refs are now just standard props.The Why & How: Previously, passing a ref from a parent to a child required wrapping the child in
React.forwardRef(), which created messy code and complicated generic typings. In React 19, you simply destructure it: function CustomInput({ ref, placeholder }) { return <input ref={ref} /> }.Real-World Scenario: Building a design system
<Button> that needs to expose its DOM node for tooltip positioning. You no longer have to wrap the entire component file in a forwardRef HOC; you just pass ref down the prop chain exactly like you would an onClick handler.
The Why & How: You pass the “true” base state into
useOptimistic. While a Server Action or async task is pending, the hook returns an optimistic value that you render. Once the action resolves (or fails), the hook automatically discards the optimistic value and snaps back to the source of truth, handling rollbacks inherently.Real-World Scenario: A todo list app. The user clicks “Delete Task”. You pass the action to
useOptimistic, which instantly removes the item from the array in the UI. If the server throws a 500 error, the hook automatically restores the deleted item to the list, requiring zero manual error-catching logic from you.
The Why & How: Instead of developers deciding when to use
useMemo, useCallback, or React.memo, the compiler analyzes your code’s data flow during the build step. It automatically inserts memoization logic precisely where needed to prevent unnecessary re-renders, based strictly on the rules of React.Real-World Scenario: You have a deeply nested component tree suffering from performance issues due to inline object creation. Instead of spending two days refactoring the codebase with `useMemo` dependency arrays, you run the project through the React Compiler. It automatically memoizes the inline objects, instantly fixing the performance drops with zero code changes.
9. Testing React Apps
The Why & How: Enzyme allowed developers to assert “does this component have a state variable named `isOpen` set to true?” RTL forces you to assert “is the text ‘Welcome’ currently visible on the screen?” If you refactor a component to use
useReducer instead of useState, RTL tests will perfectly survive the refactor, whereas Enzyme tests would completely break.Real-World Scenario: A user doesn’t care if you use Redux, Context, or local state. They care that when they click the button labeled “Submit”, a success message appears. RTL guarantees that your tests verify this user-centric outcome.
The Why & How:
*
getBy: Throws an error instantly if the element is not found. Use it as the default for elements that *must* be present.*
queryBy: Returns `null` instead of throwing. Use it strictly to assert that an element does *not* exist on the page.*
findBy: Returns a Promise and retries the query over time. Use it when waiting for an element to appear after an asynchronous event.Real-World Scenario: When a page loads, you use
getByRole('button') to click submit. You use queryByText('Error') and expect it to be null. Finally, you await findByText('Success') to catch the toast notification that renders half a second later.
The Why & How: Writing `jest.mock(‘axios’)` couples your test tightly to the Axios library. If you migrate your app to the native `fetch` API tomorrow, every single one of your tests will break, even though the UI logic didn’t change. MSW spins up a mock server that intercepts the outbound HTTP request regardless of the client library used.
Real-World Scenario: You are testing a complex checkout component that uses React Query, Axios, and custom retry logic. MSW perfectly simulates network latency, 500 server errors, and successful JSON responses, testing the *entire* async infrastructure organically.
useContext will return undefined and crash the test.The Why & How: You do not wrap every individual test manually. Instead, you create a custom
render utility function (e.g., `customRender`) that inherently wraps the tested component inside a standardized <AllTheProviders> component (including Theme, Redux, and Auth).Real-World Scenario: You write a test for
<ProfileAvatar />. In your test file, you just call render(<ProfileAvatar />). Behind the scenes, your custom render intercepts it and actually mounts <ThemeProvider><AuthProvider><ProfileAvatar /></AuthProvider></ThemeProvider>, ensuring the avatar has the correct data to display.
act() is a helper that ensures all React updates (rendering, state updates, effects) triggered by an event are fully completed and flushed to the DOM before your test makes assertions.The Why & How: RTL’s built-in functions (like
userEvent.click or render) already have act() built into them. You only need to use it manually if you have custom asynchronous logic (like a stray setTimeout or a mocked timer) updating React state outside the purview of the standard testing library tools.Real-World Scenario: You see the dreaded warning: “An update to Component inside a test was not wrapped in act(…)”. This usually means your component fired a `fetch` request, the test finished and unmounted, and *then* the fetch resolved and tried to set state. You fix this by awaiting the proper DOM changes (using `findBy`) before letting the test end.
The Why & How:
* **Pros:** Very fast to write; alerts you to unexpected side-effects when changing shared CSS classes or layout wrappers.
* **Cons:** Highly brittle. Changing a typo from “Teh” to “The” fails the test. Because they fail so often on valid changes, developers develop “snapshot fatigue” and blindly run `jest -u` to update them without actually checking the diff, rendering the tests useless.
Real-World Scenario: Architects rarely use snapshots for raw UI components anymore. They are much better suited for testing complex, non-UI data transformations, like verifying the massive nested JSON configuration object generated by a custom form builder.
fireEvent dispatches a single, raw DOM event. userEvent fully simulates the holistic browser interaction exactly as a human would execute it.The Why & How: If you use
fireEvent.change() on an input, it simply updates the value attribute. If you use userEvent.type(), it simulates the mouse hovering, the element focusing, and the sequence of `keydown`, `keypress`, and `keyup` for every single letter typed.Real-World Scenario: You build a custom credit card input that auto-formats numbers. If you use `fireEvent`, your test might pass. But in reality, a bug in your custom `onKeyDown` handler might prevent users from using the Backspace key. `userEvent` will catch this bug because it rigorously fires the full event lifecycle.
The Why & How: Because state updates and `useEffects` take time, an assertion directly after a click will fail (checking for data while the spinner is still on screen). You must use RTL’s async utilities like
waitFor() (to retry an assertion until it passes) or findBy* (which combines `getBy` with `waitFor`).Real-World Scenario: You test a login form. You `userEvent.click(submitButton)`. You cannot immediately assert the dashboard is visible. You must write:
expect(await screen.findByText('Welcome to your Dashboard')).toBeInTheDocument(). RTL will poll the DOM every 50ms until the API resolves and the text appears.
The Why & How: While coverage is a useful metric to find glaring blind spots, mandating 100% is an anti-pattern. It forces developers to write meaningless, brittle tests just to hit the number (like testing native getter/setters or static constants), slowing down feature velocity without actually improving software quality.
Real-World Scenario: A pragmatic architect aims for 70-80% coverage. They mandate 100% coverage on the complex Redux billing reducer that processes money, but perfectly accept 20% coverage on a static UI component that just renders a marketing banner.
The Why & How: RTL runs in Node using JSDOM, a mocked version of a browser. JSDOM does not paint pixels, calculate CSS layouts, or have a real network stack. E2E tests spin up an actual Chromium/WebKit browser, click real pixels, and hit real staging databases.
Real-World Scenario: You write an RTL test to ensure a `
10. SSR, Next.js & Ecosystem
The Why & How: In CSR (standard React), the server sends a blank HTML shell containing a
<script> tag. The browser downloads the JS bundle and uses it to construct the DOM from scratch. In SSR, a Node.js server executes the React components, generates the fully populated HTML string, and sends that complete document to the browser.Real-World Scenario: A user on a slow 3G mobile connection visits a news site. With CSR, they stare at a blank white screen for 4 seconds while the massive JS bundle downloads. With SSR, they see the fully formatted text of the article in 400 milliseconds, even before the interactive JavaScript has finished downloading.
The Why & How: During the CI/CD pipeline (e.g.,
next build), React fetches the required data, renders all the pages to static HTML files, and uploads them to a global CDN. It completely eliminates database hits and server compute time for end-users, offering the fastest possible Time To First Byte (TTFB).Real-World Scenario: A corporate marketing site or a developer blog. The content only changes when a new article is published. Instead of running an expensive Node server to SSR the same article 10,000 times a day, SSG builds it once. Users globally pull the static HTML file instantly from a CDN node in their city.
The Why & How: You configure a
revalidate time (e.g., 60 seconds). A user requests a page, getting the lightning-fast static CDN cache. If 60 seconds have passed since the last cache, Next.js triggers a background serverless function to quietly rebuild that specific page. The *next* user gets the newly generated page.Real-World Scenario: An e-commerce store with 2 million products. Running SSG for 2 million pages would take hours. Using SSR for every page view would crash the database. With ISR, popular product pages are instantly served statically, but if a marketing team changes a price, the cache automatically refreshes itself a minute later without a full site deploy.
The Why & How: The server sends HTML to show the user the UI quickly, but HTML cannot handle
onClick events or useState. The browser downloads the React JS bundle in the background. Once loaded, React traverses the existing DOM, “wakes it up”, and binds all the event listeners, turning the static document into a fully functional Single Page Application.Real-World Scenario: The server renders a fancy “Add to Cart” button. It appears instantly on the screen. However, for a brief fraction of a second (before hydration completes), clicking the button does nothing. Once React finishes hydrating the tree, clicking it triggers the complex state logic.
The Why & How: If React detects a difference, it abandons the server HTML and forcefully re-renders the entire component tree from scratch, destroying performance. This usually happens when relying on browser-specific APIs (like
window.innerWidth) or dynamic data (like new Date()) during the initial render.Real-World Scenario: You render a greeting:
<h1>Good {new Date().getHours() > 12 ? 'Evening' : 'Morning'}</h1>. The server in London renders “Morning”. The client opens it in Tokyo where it’s night, so React renders “Evening”. The trees mismatch, and React throws an error. You fix this by only executing dynamic logic inside a useEffect, which only runs *after* hydration is complete.
The Why & How: In the Pages router, data fetching was route-level (
getServerSideProps). You fetched everything at the top and prop-drilled it down. In the App Router, every component is a Server Component by default. You can fetch database queries directly inside deeply nested components (like a Sidebar), bypassing API layers entirely.Real-World Scenario: Building a layout with a user profile and a shopping cart. In Pages, the top
layout.js had to fetch both pieces of data and pass them down. In App Router, the <UserProfile> component connects to the DB itself, and the <Cart> component connects to Redis itself. They are decoupled and stream to the client independently.
The Why & How: Because App Router components run on the server by default, they cannot use interactivity. You add
"use client" *only* to components that require React hooks (useState, useEffect), DOM event listeners (onClick), or browser-exclusive APIs (window.localStorage).Real-World Scenario: A blog post page. The layout, the article text, and the heavy Markdown parser should remain Server Components (0kb JS sent to the client). You only add
"use client" to the tiny LikeButton.tsx component at the bottom, so it can manage its own isLiked state and onClick animation.
The Why & How: SSR is a technique to generate initial HTML, but the component still mounts on the client for hydration. An RSC executes *exclusively* on the server. Its code is completely stripped from the final client bundle, drastically reducing the amount of JavaScript the user has to download, parse, and execute.
Real-World Scenario: You install the massive
date-fns library to format timestamps in a list of comments. In traditional SSR, the user downloads date-fns.js just in case the component re-renders. With an RSC, the server runs date-fns, formats the string, and sends *only* the raw HTML string <span>Jan 1st</span>. The user never downloads the library.
The Why & How: When a crawler hits a standard React app, it sees
<div id="root"></div> and no meaningful metadata. While Googlebot eventually spins up a headless browser to execute the JS and read the rendered content, it takes significantly longer, is deprioritized, and many social media scrapers (like Slack or iMessage) simply give up and show a blank preview card.Real-World Scenario: If you paste an Amazon product link in Discord, you see the product image and price instantly. This is because the server sent that metadata natively in the HTML
<head>. A raw CSR React app cannot do this effectively because the router hasn’t loaded to figure out which product you are looking at yet.
localStorage, so tokens must be stored in secure HTTP cookies.The Why & How: In a CSR app, you can save a JWT in
localStorage. But when a user requests an SSR page, the Node server needs to know *before* it generates the HTML if the user is an admin or not. Browsers automatically attach cookies to every network request, allowing the server to read the cookie, validate the session, and conditionally render the correct HTML shell.Real-World Scenario: A user navigates to
/admin/dashboard. The browser sends the request along with an HttpOnly, Secure cookie containing their session ID. Next.js middleware intercepts the request, verifies the cookie against the database, and either allows the server to render the sensitive dashboard HTML, or immediately redirects them to /login with a 302 response.
React 100: The Field Guide
100 crucial beginner-level React JS interview questions and answers. Read through full-width, mobile-optimized topics covering Hooks, Rendering, and the new React 19 updates.
1. React Basics & JSX
React.createElement() calls before running in the browser.React.createElement() (or the newer jsx() runtime) calls that browsers can execute.react contains the core library: components, hooks, and the logic for describing UI. react-dom is the renderer that knows how to take that description and mount it into a web page’s DOM (a separate package, react-native, renders to native mobile views instead).<p>{user.name}</p> or <p>{2 + 2}</p>. Only expressions are allowed, not statements, so you can’t put an if block directly inside braces, though ternaries and logical operators work fine.<React.Fragment>...</React.Fragment> or the shorthand <>...</>. Fragments satisfy JSX’s one-root-element rule without adding any extra node to the actual DOM.npm create vite@latest) for plain React apps, or a framework like Next.js or Remix when server rendering and routing are needed.2. Components & Props
React.Component, manage state with this.state, and use lifecycle methods like componentDidMount instead of hooks.this keyword. Hooks also let you share stateful logic between components more easily than the older patterns class components required.function Button({ size = 'medium' }) {...}. The older Component.defaultProps = {...} pattern still works but is being phased out.children is a special prop containing whatever is nested between a component’s opening and closing tags, e.g. <Card>Hello</Card> gives Card a children prop equal to “Hello”. It’s commonly used to build wrapper or layout components.null (or false, or undefined) tells React to render nothing for that component, while the component itself stays mounted and can still respond to future prop or state changes.children prop, rather than extending a base component class. React’s team recommends composition because it’s more flexible and avoids fragile class hierarchies.3. State, Events & Forms
useState, and only that component (or what it explicitly passes down) can change it.useState, e.g. const [count, setCount] = useState(0); setCount(count + 1);. Calling the setter, rather than reassigning the variable directly, is what tells React to schedule a re-render.useEffect that runs after the re-render.value and onChange wired together so React is the single source of truth. An uncontrolled component lets the DOM manage its own value internally, read later via a ref when needed.onChange handler that reads event.target.value and stores it in state, e.g. <input value={name} onChange={(e) => setName(e.target.value)} />. This keeps the input controlled by React state.onSubmit handler to the <form> element and call event.preventDefault() before running your own submission logic. Without that call, the browser would try to reload the page or navigate, which isn’t usually what you want.target and preventDefault(), while integrating with React’s event system.<button onClick={() => handleDelete(item.id)}>Delete</button>. Writing onClick={handleDelete(item.id)} without the arrow function would call it during render instead of on click.action prop (or a button’s formAction prop); React calls it automatically on submission and can handle pending and error states for you. This is often paired with the new useActionState hook to simplify form-handling boilerplate.4. Hooks
useState and useEffect, that let function components use features such as state and side effects that used to only be available in class components. They always start with the word “use” by convention.useState call.const [count, setCount] = useState(0).useState calls, instead of one big state object, usually makes components easier to read and update.useEffect lets you run side effects, code that interacts with something outside of rendering, like fetching data, subscribing to events, or manually updating the document title, after the component renders.useEffect, an array of values the effect depends on. React re-runs the effect only when one of those values has changed since the last render, instead of on every render.useContext lets a component read a value from a Context Provider higher up the tree without it being passed down manually through every intermediate component. It’s the main way to avoid props drilling for things like themes or authenticated user info.useRef returns a mutable object with a .current property that persists across renders, but changing it does not trigger a re-render the way a state update does. It’s commonly used to reference a DOM node directly or store a value without re-rendering.useRef when you need to access a DOM element directly, such as focusing an input, or store a mutable value, like a timer ID or previous value, that shouldn’t cause the component to re-render whenever it changes.useMemo caches the result of an expensive calculation and only recomputes it when its dependencies change, instead of on every render. It’s best used when profiling shows a calculation is actually slow enough to matter, not by default on every computed value.useCallback memoizes a function itself so the same reference is reused across renders unless its dependencies change, while useMemo memoizes the return value of a computation. They’re often used together to avoid unnecessary re-renders of memoized child components.function useWindowWidth() {...} returning the current width.useFetch or useAuth. This convention lets React’s linter and rules-of-hooks checks correctly identify which functions are hooks so they can be checked for proper usage.use() lets a component read the value of a Promise or a Context, and unlike other hooks, it can be called conditionally or inside loops. It’s often used to read data from a Promise passed down from a Server Component, letting the component wait for that value as part of rendering.5. Lifecycle & Effects
componentDidMount and componentDidUpdate running on every single update.componentDidMount in class components, since an empty array means there are no dependencies that could ever change.useEffect. React calls that returned function when the component is about to unmount, or before re-running the effect again, which is the functional equivalent of componentWillUnmount.useEffect runs asynchronously after the browser has painted the screen, while useLayoutEffect runs synchronously after DOM mutations but before the browser paints. useLayoutEffect is reserved for rare cases like measuring layout and adjusting it before the user sees a flicker.StrictMode, React intentionally mounts, unmounts, and remounts components once to help you catch effects that aren’t cleaning up properly. This double-invoking only happens in development, not in production builds.StrictMode is a wrapper component that renders no visible UI but activates extra checks, like double-invoking effects and detecting unsafe lifecycle usage, to help surface bugs in development before they reach production.6. Lists, Keys & Conditional Rendering
.map() to transform an array of data into an array of JSX elements, e.g. {items.map(item => <li key={item.id}>{item.name}</li>)}.crypto.randomUUID(), a database-assigned id once it’s saved, or a stable combination of fields guaranteed not to repeat, rather than relying on array position.condition ? <A /> : <B />, the && operator for an either-render-or-nothing case, condition && <A />, or an early if/return before the main JSX in the component function.{count && <p>{count} items</p>} renders the JSX only if count is truthy. The pitfall is that if count is 0, JavaScript’s && still evaluates to 0, so React renders a literal 0 on the page instead of nothing.{items.length === 0 ? <EmptyState /> : items.map(...)}, so the user sees a helpful message instead of a blank section.{users.map(u => <UserCard key={u.id} name={u.name} email={u.email} />)}.7. Context, Refs & Performance
const ThemeContext = createContext(defaultValue), wrap the part of your tree that needs it with <ThemeContext.Provider value={theme}>, and read it in any descendant with useContext(ThemeContext).<ThemeContext value={theme}>, instead of writing out <ThemeContext.Provider value={theme}> every time, slightly reducing boilerplate.useContext directly and read the value from the nearest matching Provider above it.React.memo wraps a component so React skips re-rendering it if its props haven’t changed, using a shallow comparison. It’s best applied to components that render often with the same props and are expensive enough that skipping the re-render is worth it.forwardRef lets a parent pass a ref through a custom component down to one of its underlying DOM nodes, since refs aren’t a regular prop by default in older React versions. In React 19, function components can accept ref as a normal prop directly, so wrapping them in forwardRef is no longer required in most cases.React.lazy(() => import('./Component')) and render it inside a <Suspense fallback={<Spinner />}> boundary that shows a fallback while the code loads.8. Routing & Ecosystem Basics
<Routes> and <Route path="..." element={...} /> to define which component renders for a given URL, and <Link to="..."> to navigate between routes without a full page reload.<a> tag triggers a full browser page reload when clicked. <Link> intercepts the click and updates the route using JavaScript instead, preserving the SPA’s in-memory state and avoiding the cost of reloading the page.package.json lists a project’s dependencies, like react and react-dom, and scripts, like npm run dev. Package managers such as npm, yarn, or pnpm read that file to install the exact library versions a project needs and to run those scripts.9. React 19 — What’s New
<form action={...}> or a button’s formAction, and React automatically manages pending states, errors, and optimistic updates around it, removing a lot of manual onSubmit and loading-state boilerplate.useActionState takes an action function and an initial state, and returns the current state, a wrapped action to pass to a form, and a pending flag. It removes the need to manually wire up useState plus an onSubmit handler just to track a form’s result and loading status.useFormStatus returns the pending status, and submitted data, of the nearest parent <form>, but it must be called inside a component rendered as a descendant of that form, not the form’s own component. It’s handy for a reusable submit button that automatically disables itself while submitting.useOptimistic lets you show a temporary, optimistic version of state immediately while an async update is still in flight, then reconciles it with the real result once the server responds. It solves the lag between a user’s action, like liking a post, and the UI visibly reflecting it.ref directly as a regular prop, similar to any other prop, so forwardRef is no longer required for most simple ref-forwarding use cases, though it’s still supported for backward compatibility.<title>, <meta>, and <link> directly inside any component, even deep in the tree, and React automatically hoists them up into the document’s <head> rather than rendering them where they appear in the JSX.preload, preinit, preconnect, and prefetchDNS, from react-dom, let you hint to the browser to start fetching a stylesheet, font, script, or domain connection earlier, before the resource is actually needed, to speed up loading.useMemo or useCallback, so components skip unnecessary re-renders without manual optimization.useMemo or useCallback by hand for edge cases it doesn’t catch, but the goal is that beginners increasingly won’t need to reach for them manually.use server directive in frameworks like Next.js, that a client component can call directly. It’s often wired up through the same action prop used by React 19’s form Actions, letting submissions trigger server-side logic without writing a separate API route by hand.10. Tooling, Best Practices & Misc
npm create vite@latest with the React template, is the common recommendation. If routing, server rendering, or a fuller app structure is needed, a framework like Next.js or Remix is usually a better starting point..push(), .splice(), or .sort() directly on a state array. Since these mutate in place and don’t create a new array reference, React won’t detect the change; the fix is non-mutating approaches like [...array, newItem] or .filter()/.map() to build a new array.useState over useRef, or why a particular key is safe to use, since interviewers are usually evaluating your understanding of why, not just whether the final code runs.