Zomato Frontend Interview Questions
React and JavaScript basics, with SEO and page speed.
Zomato's web product includes restaurant and city pages that many people reach from search, alongside ordering and dining flows. That makes server rendering, page speed and SEO more relevant here than at app-only companies.
Interviews are usually described as practical: JavaScript and React fundamentals, a coding exercise, and questions about how you would make a page load faster.
- Questions
- 15, 5 with free answers
- Interview rounds
- 4
- Last reviewed
- October 2026
Zomato frontend interview process
The usual shape of the loop. A guide to what to prepare, not a promise of what you will get.
- 1
Screening
JavaScript and React basics, often with output-based questions.
- 2
Coding round
A live build or a take-home assignment. The format varies by team.
- 3
Technical deep dive
React internals, state management, performance and how search engines see a page.
- 4
Hiring manager
Past projects and how you make decisions.
What interviewers tend to look for
- React fundamentals explained clearly: keys, state, re-renders.
- Loading performance: bundle size, script loading and images.
- Server rendering and the SEO basics that go with it.
- Solid CSS, including responsive layout without a framework.
Your Zomato mock interview
15 questions. Type an answer under each one and an AI reviewer scores it and tells you what you missed. Stuck on one? Ask for a hint. A free account gets 3 AI replies a day.
Your mock interview
0 of 15 answered
Answer a question and your score shows up here.
JavaScript
5 questions- Question 1 of 15JavaScript
Zomato interviewer
What is a closure? Give a practical use for one.
Model answer
A closure is a function together with the scope it was created in. The function keeps access to the variables of that scope even after the outer function has returned.
function createCounter() { let count = 0; // private: nothing outside can touch it return { increment: () => ++count, value: () => count, }; } const counter = createCounter(); counter.increment(); counter.value(); // 1Practical uses: private state as above, function factories, and utilities such as
debounceandmemoize, which keep a timer or a cache between calls.The cost to mention: a closure keeps its captured variables alive for as long as the function is reachable, so a long-lived callback holding a large object is a memory leak.
- Question 2 of 15JavaScript
Zomato interviewer
Explain hoisting and the temporal dead zone.
Model answer
Declarations are registered when a scope is created, before any of its code runs. What differs is the starting value:
varis hoisted and set toundefined, so reading it early givesundefined.- Function declarations are hoisted with their body, so you can call them above where they are written.
let,constandclassare hoisted but left uninitialised. Reading them before the declaration line throws aReferenceError.
The stretch between the start of the scope and the declaration line is the temporal dead zone.
console.log(a); // undefined console.log(b); // ReferenceError var a = 1; let b = 2;Function expressions follow their variable:
var fn = function () {}isundefineduntil that line runs. - Question 3 of 15JavaScript
Zomato interviewer
What is the difference between ==, === and Object.is?
Model answer
===compares without converting types.==converts first, which gives results such as'1' == 1andnull == undefinedbeing true. The one widely accepted use of==isvalue == null, which checks for bothnullandundefined.Object.isbehaves like===with two differences:Object.is(NaN, NaN)is true, andObject.is(+0, -0)is false.All three compare objects by reference, so
{} === {}is false.Worth adding in a React interview: React uses
Object.isto decide whether state or a dependency changed, which is why mutating an object in place does not trigger a re-render. - Question 4 of 15JavaScript
Zomato interviewer
Implement a memoize function.
Model answer
function memoize(fn, resolver = (...args) => JSON.stringify(args)) { const cache = new Map(); return function (...args) { const key = resolver(...args); if (!cache.has(key)) { cache.set(key, fn.apply(this, args)); } return cache.get(key); }; }The discussion is about the cache key and the cache size:
JSON.stringifyfails on functions and circular objects, and treats{a:1,b:2}and{b:2,a:1}as different. That is why the resolver is a parameter.- The cache grows without limit. Cap it with an LRU policy for long-running pages.
- For a single object argument, a
WeakMaplets the entry be garbage collected with the object. - Only memoize pure functions. Memoizing an async function caches the promise, including a rejected one.
- Question 5 of 15JavaScript
Zomato interviewer
What causes memory leaks in a frontend app, and how do you find one?
Model answer
A leak is memory that is no longer needed but is still reachable, so the garbage collector cannot free it. Usual causes:
- Event listeners, timers and subscriptions that are never removed.
- DOM nodes removed from the page but still referenced from JavaScript.
- Caches, arrays and maps that only ever grow.
- Closures that keep a large object alive.
- In React, an effect with no cleanup function.
To find one, open the Memory panel in Chrome DevTools. Take a heap snapshot, repeat the suspect action several times (open and close a modal, change route and come back), take another, and compare. Objects whose count keeps growing are the leak; the retainers view shows what holds them.
Fixes: always return a cleanup from effects, cancel requests with
AbortController, and useWeakMaporWeakReffor caches keyed by objects.
TypeScript
1 question- Question 6 of 15TypeScript
Zomato interviewer
What are generics, and how do you constrain one?
Model answer
A generic is a type parameter. It lets a function or type work with many types while keeping the link between its inputs and its output, which
anythrows away.function first<T>(items: T[]): T | undefined { return items[0]; } first([1, 2, 3]); // number | undefined first(['a', 'b']); // string | undefinedA constraint with
extendslimits what the parameter can be, and lets you use the properties that the constraint guarantees:function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] { return items.map((item) => item[key]); } pluck([{ id: 1, name: 'Asha' }], 'name'); // string[] pluck([{ id: 1, name: 'Asha' }], 'age'); // compile errorGood to mention: type arguments are usually inferred, generics can have defaults (
<T = string>), and one that is used only once in a signature is usually not needed.
React
3 questions- Question 7 of 15React
Zomato interviewer
How does React decide what to update in the DOM? Why do list items need keys?
Model answer
Rendering calls your components and produces a tree of elements. React compares that tree with the previous one (reconciliation) and then applies only the differences to the DOM, in a separate commit step.
Two rules keep the comparison fast:
- A different element type at the same position (a
divbecoming asection, orLoginbecomingDashboard) means React throws that subtree away and builds a new one. Its state is lost. - The same type at the same position means React keeps the instance and its state and updates the props.
In a list, position is not enough, because items move. A
keytells React which item is which between renders. With the array index as the key, inserting at the top makes every item look changed, and state such as a typed input value ends up attached to the wrong row. Use a stable id from the data.A useful trick follows from the same rule: changing a component's
keyon purpose resets its state. - A different element type at the same position (a
- Question 8 of 15React
Zomato interviewer
Is setState asynchronous? What is batching?
Model answer
Calling a state setter does not change the variable in the code that is running. It asks React to render again with the new value. Each render sees its own fixed snapshot of state.
const [count, setCount] = useState(0); function addThreeWrong() { setCount(count + 1); setCount(count + 1); setCount(count + 1); // count is 0 in all three: result is 1 } function addThree() { setCount((c) => c + 1); setCount((c) => c + 1); setCount((c) => c + 1); // each gets the pending value: result is 3 }Batching means React collects all the updates made in one tick and renders once. Since React 18 this happens everywhere, including inside timeouts, promises and native event handlers. Before 18 it only happened in React event handlers.
flushSyncforces an immediate render when you need the DOM updated straight away, for example to measure it. It is rarely needed. - Question 9 of 15React
Zomato interviewer
How do you decide where state should live, and when would you add a state management library?
Model answer
Sort the state by kind first, because each kind has a natural home:
- Local UI state (is this menu open):
useStateoruseReducerin the component. - State shared by a few siblings: lift it to their nearest common parent.
- App-wide client state that changes rarely (theme, auth): Context.
- Server state, meaning data fetched from an API: a cache library such as TanStack Query, SWR or RTK Query, or the framework's own data layer. It handles caching, deduplication, retries and refetching.
- State that should survive a refresh or be shareable (filters, page number, selected tab): the URL.
Most of what teams once put in a global store was server state. With that handled by a cache, the remaining shared client state is often small.
Reach for Redux Toolkit or Zustand when many distant components read and write the same client state, when updates need to be traceable, or when Context re-renders become a measured problem.
- Local UI state (is this menu open):
Next.js
2 questions- Question 10 of 15Next.js
Zomato interviewer
How does routing work in the App Router? Explain dynamic routes and generateStaticParams.
Model answer
Routes come from folders inside
app. A folder becomes a URL segment when it contains apage.tsx.[slug]is a dynamic segment.[...parts]catches any number of segments.(group)organises files or shares a layout without adding to the URL.@slotfolders are parallel routes, rendered side by side in one layout. Intercepting routes show another route inside the current one, which is how a photo opens in a modal but has its own URL.layout.tsxwraps everything below it and is not re-rendered on navigation.
// app/blog/[slug]/page.tsx export async function generateStaticParams() { const posts = await getPosts(); return posts.map((post) => ({ slug: post.slug })); } export const dynamicParams = false; // any other slug is a 404 export default async function Post({ params }) { const { slug } = await params; // a Promise since Next.js 15 const post = await getPost(slug); return <article>{post.title}</article>; }generateStaticParamslists the values to build ahead of time. WithoutdynamicParams = false, other values are rendered on first request and then cached.For navigation use
Link, which prefetches routes as they come into view, andredirectandnotFoundon the server. - Question 11 of 15Next.js
Zomato interviewer
How do you handle SEO in a Next.js app?
Model answer
Start with the content being in the HTML. Pages rendered on the server or at build time are readable by any crawler without running JavaScript.
export async function generateMetadata({ params }) { const { slug } = await params; const product = await getProduct(slug); return { title: product.name, description: product.summary, alternates: { canonical: 'https://example.com/products/' + slug }, openGraph: { title: product.name, images: [product.image] }, }; }- Give every page a unique title and description through
metadataorgenerateMetadata. - Set a canonical URL so that filtered and paginated versions do not compete with the main page.
- Add
sitemap.tsandrobots.ts. - Add structured data as JSON-LD in a script tag for products, articles and breadcrumbs.
- Mark thin or private pages
noindex. - Use real links (
Link), oneh1, and headings in order.
Core Web Vitals are also a ranking signal, so image sizes, layout shift and JavaScript weight are part of the answer.
- Give every page a unique title and description through
CSS
2 questions- Question 12 of 15CSS
Zomato interviewer
What is the difference between px, em, rem, %, vw and vh?
Model answer
px: a fixed CSS pixel. Fine for borders and small details.rem: relative to the root font size. If a user raises their browser's default font size, everything inremscales with it. Use it for type and spacing.em: relative to the element's own font size. It compounds when nested. Useful for padding that should scale with a button's text.%: relative to the parent. Which dimension depends on the property, and vertical padding in percent is based on the parent's width.vwandvh: one percent of the viewport's width or height.dvh,svh,lvh: viewport height that accounts for mobile browser toolbars appearing and disappearing.100vhis often taller than the visible area on a phone;100dvhis not.ch: the width of the0character.max-width: 65chgives a comfortable line length.fr: a share of the free space in a grid.
A good one-line summary:
remfor sizes that should follow the user's settings,pxfor things that should not scale, and viewport or container units for layout. - Question 13 of 15CSS
Zomato interviewer
How do you build a responsive layout?
Model answer
Start mobile-first: the base styles are for the smallest screen and
min-widthmedia queries add to them as space allows..title { font-size: clamp(1.5rem, 1rem + 2vw, 2.5rem); /* fluid, with limits */ } .card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); } .card { container-type: inline-size; } @container (min-width: 28rem) { .card-body { display: flex; /* reacts to the card's width, not the screen's */ } }- Prefer layouts that adapt by themselves, such as wrapping flex rows and
auto-fitgrids, over a breakpoint for every device. - Container queries style a component by the width of its container, so the same card works in a sidebar and in a wide page.
- Serve images with
srcsetandsizes, and give them an aspect ratio. - Include the viewport meta tag. Use
dvhinstead ofvhfor full-height sections on phones. - Keep touch targets large, and respect
prefers-reduced-motionandprefers-color-scheme.
- Prefer layouts that adapt by themselves, such as wrapping flex rows and
Browser, performance and system design
1 question- Question 14 of 15Browser, performance and system design
Zomato interviewer
What is the difference between async and defer on a script tag? What are preload and prefetch?
Model answer
- A plain
scriptstops the HTML parser while the file downloads and runs. asyncdownloads in parallel and runs as soon as the file arrives, pausing the parser at that moment. Order is not guaranteed. Use it for independent scripts such as analytics.deferdownloads in parallel and runs after the document has been parsed, in the order the scripts appear, just beforeDOMContentLoaded. Use it for application code.type="module"scripts are deferred by default.
Resource hints tell the browser about things it has not discovered yet:
preload: this page needs the file soon, so fetch it now at high priority. Used for a font, or a hero image referenced from CSS.prefetch: a later navigation will probably need this, so fetch it when idle.preconnect: open the connection to another origin early.fetchpriority="high"on an image raises its priority, which helps LCP.
Preloading too much is a common mistake. Everything marked urgent competes with what really is.
- A plain
Machine coding
1 question- Question 15 of 15Machine coding
Zomato interviewer
Build an infinite scrolling list.
Model answer
Put an empty sentinel element after the list and watch it with an
IntersectionObserver. When it comes into view, load the next page.const sentinelRef = useRef(null); useEffect(() => { const node = sentinelRef.current; if (!node || !hasMore) return; const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting && !loading) loadNextPage(); }, { rootMargin: '200px' } // start a little before the end ); observer.observe(node); return () => observer.disconnect(); }, [hasMore, loading, loadNextPage]);- Track
items,cursor(or page),loading,hasMoreanderror. - Never send the same page request twice. The
loadingcheck above does that. - Append to the list; do not replace it. Use stable ids as keys.
- Show a loading row, an end-of-list message, and a retry button when a page fails.
- Stop observing when there is nothing more to load.
Follow-ups: why not a scroll listener (it fires constantly and needs throttling and manual maths), how to handle a list that grows to thousands of rows (virtualise), and how to restore the scroll position when the user comes back.
- Track
Practise for Zomato
Reading answers is the easy half. These problems run in the browser against real tests.
