React interview questions
useMemo vs useCallback
useMemomemoizes a computed value and returns it.useCallbackmemoizes a function reference.When to use
Hook Use when useMemoYou want to avoid expensive recalculations useCallbackYou want to prevent unnecessary re-renders caused by new function references Example
tsx// useMemo - memoizes a value const sortedList = useMemo(() => { return items.toSorted((a, b) => a.price - b.price); // sort() would mutate props }, [items]); // useCallback - memoizes a function const handleClick = useCallback(() => { doSomething(id); }, [id]);React Compiler
React Compiler (stable since October 2025) memoizes components, values and callbacks automatically at build time. In a project that uses it, manual
useMemo/useCallbackare rarely needed - mostly as an escape hatch, e.g. for a value used as an effect dependency.Note: without the compiler, don't overuse these hooks - they have their own overhead. Apply only where a profiler shows a real problem.
updated
did you know it?Virtual DOM
Virtual DOM is a lightweight in-memory representation of the real DOM tree as JavaScript objects.
How the update process works
- When state changes, React creates a new virtual tree.
- The reconciliation algorithm compares the old and new trees (diffing).
- React computes the minimal set of changes.
- Only those changes are applied to the real DOM.
Why it matters
Real DOM operations are expensive. Manipulating JS objects is faster, so batching and the diff algorithm provide a noticeable performance boost.
React 18+ has concurrent rendering: updates marked as transitions (
startTransition,useDeferredValue) can be interrupted and deferred so urgent input stays responsive. It's opt-in per update, not a separate "Concurrent Mode".updated
did you know it?useEffect vs useLayoutEffect
Execution order
textRender -> DOM mutations -> useLayoutEffect -> Browser paints -> useEffectuseEffectuseLayoutEffectWhen After paint (async) Before paint (sync) Blocks browser No Yes SSR Doesn't run on the server Doesn't run on the server (the SSR warning was removed in React 19) When to use
useLayoutEffect- You need to measure DOM element dimensions (
getBoundingClientRect). - You need to synchronously update the DOM to avoid visual flickering.
tsxuseLayoutEffect(() => { const { height } = ref.current.getBoundingClientRect(); setHeight(height); // before paint - user won't see a jump }, []);Rule: use
useEffectby default. Switch touseLayoutEffectonly when you see visible visual artifacts.updated
did you know it?- You need to measure DOM element dimensions (
React Fiber
Fiber is a complete rewrite of React's internal rendering engine (introduced in React 16).
Problem with the old engine (Stack Reconciler)
The previous reconciler traversed the component tree recursively and synchronously - it could not be interrupted. With a large tree this blocked the main thread and caused UI jank.
What Fiber changed
- Reconciliation is split into small units of work.
- Work can be paused, cancelled, or resumed.
- Task prioritization: animations matter more than background data updates.
This opened the path to concurrent rendering
textHigh priority: user input, animations Low priority: data fetching renders, transitionsSummary: Fiber is the architectural foundation for
Suspense,Transitions,useDeferredValue, and all of Concurrent React.updated
did you know it?React Context
Contextlets you pass data through the component tree without prop drilling.How it works
tsxconst ThemeContext = createContext('light'); function App() { return ( // React 19: the context itself is the provider (<ThemeContext.Provider> still works) <ThemeContext value="dark"> <Child /> </ThemeContext> ); } function Child() { const theme = useContext(ThemeContext); return <div>{theme}</div>; }Performance problem
Every context consumer re-renders on any change to
value- even if it only uses part of the data.Solutions
- Split contexts - a separate one for frequently changing data.
- Memoize
valuewithuseMemo. - Use a state manager (Zustand, Jotai) for high-frequency updates.
Context works great for rarely changing data: theme, locale, authentication.
updated
did you know it?React Server Components (RSC)
RSC are components that render exclusively on the server and send no JavaScript to the client.
Comparison
Server Component Client Component Rendering Server Client + Server (hydration) JS in bundle No Yes Hooks ( useStateetc.)No Yes Direct DB/FS access Yes No Interactivity No Yes Example
tsx// ServerComponent.tsx - no 'use client' async function ProductPage({ id }: { id: string }) { const product = await db.products.findById(id); // directly on the server return <ProductCard product={product} />; }Key benefits
- Less JavaScript in the bundle - faster load.
- Data fetching close to the data source.
- Sensitive data never reaches the client.
The
'use client'directive turns a component into a Client Component. In Next.js App Router all components are Server Components by default.updated
did you know it?Reconciliation and keys
The reconciliation algorithm
React compares the old and new virtual trees using two rules:
- Different element types - the old tree is destroyed and a new one is mounted.
- Same types - only changed attributes/props are updated.
Why keys are needed
When rendering lists React uses
keyto identify elements between renders.tsx// Bad - React may mix up elements items.map((item, index) => <Item key={index} {...item} />) // Good - stable unique identifier items.map((item) => <Item key={item.id} {...item} />)What happens with wrong keys
- Unnecessary unmount/mount instead of update.
- Loss of component state.
- Broken animations.
keyis also used as an intentional state reset - changing a component'skeyfully recreates it.updated
did you know it?React Suspense
Suspenselets components wait for something (data, code) and show a fallback in the meantime.Basic example with lazy loading
tsxconst HeavyChart = lazy(() => import('./HeavyChart')); function Dashboard() { return ( <Suspense fallback={<Spinner />}> <HeavyChart /> </Suspense> ); }How it works with data fetching
Suspense only works with Suspense-enabled data sources: frameworks (Next.js Server Components), libraries (
useSuspenseQueryin TanStack Query, Relay) or theuse(promise)hook. Under the hood the component suspends during render if data is not ready yet, React catches that and shows the fallback.tsx// React 19: pass a promise (created outside render, e.g. in a Server Component) function Comments({ commentsPromise }) { const comments = use(commentsPromise); // suspends until resolved return comments.map((c) => <Comment key={c.id} {...c} />); }Error Boundary + Suspense
tsx<ErrorBoundary fallback={<Error />}> <Suspense fallback={<Skeleton />}> <UserProfile /> </Suspense> </ErrorBoundary>Throwing a promise from render is an undocumented implementation detail - don't do it yourself. Use
use(promise)or a library's Suspense hooks. Fetching insideuseEffectdoes not trigger Suspense.updated
did you know it?Controlled vs Uncontrolled components
Controlled Component
The field value is stored in React state. React fully controls the form.
tsxfunction ControlledInput() { const [value, setValue] = useState(''); return ( <input value={value} onChange={(e) => setValue(e.target.value)} /> ); }Uncontrolled Component
The value is stored in the DOM. Read via
ref.tsxfunction UncontrolledInput() { const ref = useRef<HTMLInputElement>(null); const handleSubmit = () => { console.log(ref.current?.value); }; return <input ref={ref} defaultValue="default" />; }When to use which
Controlled Uncontrolled Live validation Yes No Dependent fields Yes No Simple forms Overkill Yes Non-React integration Hard Yes Most React forms are controlled. Uncontrolled is used for file inputs and integration with third-party DOM libraries.
updated
did you know it?State update batching
Batching is the merging of multiple
setStatecalls into a single re-render for optimization.Before React 18
Batching only worked inside React event handlers. Inside
setTimeout,Promise, or native events - eachsetStatetriggered a separate re-render.tsx// React 17 - 2 re-renders (inside setTimeout) setTimeout(() => { setCount(c => c + 1); // re-render setFlag(f => !f); // re-render }, 1000);React 18 - Automatic Batching
Now batching works everywhere automatically.
tsx// React 18 - 1 re-render (even inside setTimeout) setTimeout(() => { setCount(c => c + 1); setFlag(f => !f); // one re-render }, 1000);How to opt out of batching
tsximport { flushSync } from 'react-dom'; flushSync(() => setCount(c => c + 1)); // immediate re-render flushSync(() => setFlag(f => !f)); // second immediate re-renderflushSyncis rarely used - for example when integrating with animation libraries or when you need to synchronously read the DOM after an update.updated
did you know it?useState vs useReducer
Both hooks manage local state, but for different scenarios.
useState
tsxconst [count, setCount] = useState(0); setCount(count + 1);Best for simple, independent values.
useReducer
tsxtype Action = { type: 'increment' } | { type: 'reset' }; function reducer(state: number, action: Action): number { switch (action.type) { case 'increment': return state + 1; case 'reset': return 0; } } const [count, dispatch] = useReducer(reducer, 0); dispatch({ type: 'increment' });When to use useReducer
- State is an object with several interrelated fields.
- Next state depends on previous state.
- Complex state transition logic.
- You want to isolate business logic and cover it with tests.
useReducer+Contextis a lightweight alternative to Redux for medium-sized apps.updated
did you know it?React Portals
A portal lets you render a child component into a different DOM node that is outside the parent component's hierarchy.
Syntax
tsximport { createPortal } from 'react-dom'; function Modal({ children }: { children: React.ReactNode }) { return createPortal( <div className="modal">{children}</div>, document.getElementById('modal-root')! ); }When to use
- Modals - to avoid
z-indexandoverflow: hiddenissues from parents. - Tooltips and dropdowns - positioning relative to the viewport.
- Notifications (toasts) - fixed place in the DOM.
Important
Even though a Portal renders its element elsewhere in the DOM:
- Events bubble through the React tree, not the DOM tree.
- Context works normally - a Portal is a child in the React hierarchy.
html<!-- HTML structure --> <div id="app">...</div> <div id="modal-root"> <!-- Portal renders here --> </div>updated
did you know it?- Modals - to avoid
Code Splitting in React
Code splitting is breaking the bundle into pieces that load on demand (lazy loading).
React.lazy + Suspense
tsximport { lazy, Suspense } from 'react'; const SettingsPage = lazy(() => import('./SettingsPage')); function App() { return ( <Suspense fallback={<PageSkeleton />}> <SettingsPage /> </Suspense> ); }Route-based splitting (React Router)
tsxconst router = createBrowserRouter([ { path: '/', lazy: () => import('./HomePage') }, // module exports Component { path: '/dashboard', lazy: () => import('./DashboardPage') }, ]);Tools
Tool Mechanism Webpack import()- automatic chunksVite ESM-based splitting out of the box Next.js Automatic per page + dynamic()Webpack magic comments
tsxconst Chart = lazy(() => import(/* webpackChunkName: "chart" */ './Chart') );Goal: send the user only the code needed for the current screen.
updated
did you know it?