Interview questions

A curated collection of questions designed to help developers prepare for technical interviews, focusing on core concepts and real-world scenarios.

useMemo vs useCallback

useMemo memoizes a computed value and returns it.

useCallback memoizes a function reference.

When to use

HookUse when
useMemoYou want to avoid expensive recalculations
useCallbackYou want to prevent unnecessary re-renders caused by new function references

Example

// useMemo - memoizes a value const sortedList = useMemo(() => { return items.sort((a, b) => a.price - b.price); }, [items]); // useCallback - memoizes a function const handleClick = useCallback(() => { doSomething(id); }, [id]);

Note: don't overuse these hooks - they have their own overhead. Apply only where a profiler shows a real problem.

Virtual DOM

Virtual DOM is a lightweight in-memory representation of the real DOM tree as JavaScript objects.

How the update process works

  1. When state changes, React creates a new virtual tree.
  2. The reconciliation algorithm compares the old and new trees (diffing).
  3. React computes the minimal set of changes.
  4. 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+ uses Concurrent Mode where updates can be interrupted and deferred - this is the next level of optimization on top of Virtual DOM.

useEffect vs useLayoutEffect

Execution order

Render -> DOM mutations -> useLayoutEffect -> Browser paints -> useEffect
useEffectuseLayoutEffect
WhenAfter paint (async)Before paint (sync)
Blocks browserNoYes
SSRWorks correctlyProduces a warning

When to use useLayoutEffect

  • You need to measure DOM element dimensions (getBoundingClientRect).
  • You need to synchronously update the DOM to avoid visual flickering.
useLayoutEffect(() => { const { height } = ref.current.getBoundingClientRect(); setHeight(height); // before paint - user won't see a jump }, []);

Rule: use useEffect by default. Switch to useLayoutEffect only when you see visible visual artifacts.

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 Mode

High priority: user input, animations
Low priority:  data fetching renders, transitions

Summary: Fiber is the architectural foundation for Suspense, Transitions, useDeferredValue, and all of Concurrent React.

React Context

Context lets you pass data through the component tree without prop drilling.

How it works

const ThemeContext = createContext('light'); function App() { return ( <ThemeContext.Provider value="dark"> <Child /> </ThemeContext.Provider> ); } 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 value with useMemo.
  • Use a state manager (Zustand, Jotai) for high-frequency updates.

Context works great for rarely changing data: theme, locale, authentication.

React Server Components (RSC)

RSC are components that render exclusively on the server and send no JavaScript to the client.

Comparison

Server ComponentClient Component
RenderingServerClient + Server (hydration)
JS in bundleNoYes
Hooks (useState etc.)NoYes
Direct DB/FS accessYesNo
InteractivityNoYes

Example

// 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.

Reconciliation and keys

The reconciliation algorithm

React compares the old and new virtual trees using two rules:

  1. Different element types - the old tree is destroyed and a new one is mounted.
  2. Same types - only changed attributes/props are updated.

Why keys are needed

When rendering lists React uses key to identify elements between renders.

// 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.

key is also used as an intentional state reset - changing a component's key fully recreates it.

React Suspense

Suspense lets components wait for something (data, code) and show a fallback in the meantime.

Basic example with lazy loading

const HeavyChart = lazy(() => import('./HeavyChart')); function Dashboard() { return ( <Suspense fallback={<Spinner />}> <HeavyChart /> </Suspense> ); }

How it works with data fetching

A library (React Query, Relay, Next.js) must implement the Suspense protocol: throw a promise during render if data is not ready yet. React catches it and shows the fallback.

// Conceptually inside a library: function useSuspenseQuery(key) { if (!cache.has(key)) throw fetchData(key); // React catches this promise return cache.get(key); }

Error Boundary + Suspense

<ErrorBoundary fallback={<Error />}> <Suspense fallback={<Skeleton />}> <UserProfile /> </Suspense> </ErrorBoundary>

React 19 introduced the use(promise) hook - the official way to integrate promises with Suspense.

Controlled vs Uncontrolled components

Controlled Component

The field value is stored in React state. React fully controls the form.

function 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.

function UncontrolledInput() { const ref = useRef<HTMLInputElement>(null); const handleSubmit = () => { console.log(ref.current?.value); }; return <input ref={ref} defaultValue="default" />; }

When to use which

ControlledUncontrolled
Live validationYesNo
Dependent fieldsYesNo
Simple formsOverkillYes
Non-React integrationHardYes

Most React forms are controlled. Uncontrolled is used for file inputs and integration with third-party DOM libraries.

State update batching

Batching is the merging of multiple setState calls into a single re-render for optimization.

Before React 18

Batching only worked inside React event handlers. Inside setTimeout, Promise, or native events - each setState triggered a separate re-render.

// 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.

// 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

import { flushSync } from 'react-dom'; flushSync(() => setCount(c => c + 1)); // immediate re-render flushSync(() => setFlag(f => !f)); // second immediate re-render

flushSync is rarely used - for example when integrating with animation libraries or when you need to synchronously read the DOM after an update.

useState vs useReducer

Both hooks manage local state, but for different scenarios.

useState

const [count, setCount] = useState(0); setCount(count + 1);

Best for simple, independent values.

useReducer

type 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 + Context is a lightweight alternative to Redux for medium-sized apps.

React Portals

A portal lets you render a child component into a different DOM node that is outside the parent component's hierarchy.

Syntax

import { 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-index and overflow: hidden issues 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 structure --> <div id="app">...</div> <div id="modal-root"> <!-- Portal renders here --> </div>

Code Splitting in React

Code splitting is breaking the bundle into pieces that load on demand (lazy loading).

React.lazy + Suspense

import { lazy, Suspense } from 'react'; const SettingsPage = lazy(() => import('./SettingsPage')); function App() { return ( <Suspense fallback={<PageSkeleton />}> <SettingsPage /> </Suspense> ); }

Route-based splitting (React Router)

const routes = [ { path: '/', element: lazy(() => import('./HomePage')) }, { path: '/dashboard', element: lazy(() => import('./DashboardPage')) }, ];

Tools

ToolMechanism
Webpackimport() - automatic chunks
ViteESM-based splitting out of the box
Next.jsAutomatic per page + dynamic()

Webpack magic comments

const Chart = lazy(() => import(/* webpackChunkName: "chart" */ './Chart') );

Goal: send the user only the code needed for the current screen.

Difficulty

Junior
Middle
Senior

Tags

#hooks
#performance
#core
#state
#ssr
#async
#forms
#dom
#bundling
Soon

Test yourself

Check your knowledge with interactive questions and instant feedback.