Groww Frontend Interview Questions
Investing app. React, live data and performance.
Groww is an investing platform for stocks, mutual funds and more. Its web app shows prices that change every second, charts and long tables, so frontend interviews tend to touch on real-time data, rendering performance and getting numbers right.
Candidates commonly describe a React-focused machine coding round, a JavaScript fundamentals round and a hiring manager discussion, with a design round for senior roles.
- Questions
- 15, 5 with free answers
- Interview rounds
- 4
- Last reviewed
- October 2026
Groww frontend interview process
The usual shape of the loop. A guide to what to prepare, not a promise of what you will get.
- 1
JavaScript fundamentals
The event loop, closures, promises and small utilities such as debounce or memoize.
- 2
Machine coding
Build a feature in React, such as a table, a filtered list or a timer, in about an hour.
- 3
Frontend depth
React rendering, state management, Next.js and performance.
- 4
Hiring manager
Past work and how you approach product decisions.
What interviewers tend to look for
- Keeping re-renders under control when data updates many times a second.
- Choosing a real-time transport and handling reconnects.
- Large lists and tables that stay responsive.
- Typed props and state, since money values must not be mixed up.
Your Groww 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
4 questions- Question 1 of 15JavaScript
Groww interviewer
Explain the event loop. In what order do a setTimeout, a promise callback and synchronous logs run?
Model answer
JavaScript runs on one call stack. When the stack is empty, the event loop first runs every queued microtask (promise callbacks,
queueMicrotask,MutationObserver), then takes one macrotask (a timer, an I/O callback, a UI event), and the browser may render in between.console.log('A'); setTimeout(() => console.log('B'), 0); Promise.resolve().then(() => console.log('C')); console.log('D'); // A, D, C, BSynchronous code finishes first (A, D). The promise callback is a microtask, so it runs before the timer, which is a macrotask, even with a 0 ms delay.
With
asyncfunctions, everything after anawaitis scheduled as a microtask. A common follow-up is a longer snippet mixing both; work through it queue by queue instead of guessing. - Question 2 of 15JavaScript
Groww interviewer
What is the difference between debounce and throttle? Implement debounce.
Model answer
Debounce waits until the calls stop for a set time, then runs once. Use it for a search box or for saving after the user stops typing. Throttle runs at most once per interval while the calls keep coming. Use it for scroll, resize and drag handlers.
function debounce(fn, wait) { let timer; return function (...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), wait); }; }Common follow-ups: keep
thisand the arguments (done above), add acancelmethod, add aleadingoption that fires on the first call, and then write throttle. - Question 3 of 15JavaScript
Groww 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 4 of 15JavaScript
Groww interviewer
Write a function that retries a failing async call with exponential backoff.
Model answer
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function retry(fn, { retries = 3, delay = 300 } = {}) { for (let attempt = 0; ; attempt++) { try { return await fn(); } catch (error) { if (attempt >= retries) throw error; await wait(delay * 2 ** attempt); // 300, 600, 1200 ms } } }What a strong answer adds without being asked:
- Retry only what can succeed later: network errors, 5xx and 429. A 400 or a 401 will fail the same way again.
- Retry only idempotent requests, or use an idempotency key, so a payment is not made twice.
- Add random jitter to the delay so many clients do not retry at the same instant.
- Cap the delay and accept an
AbortSignalso the caller can cancel.
TypeScript
1 question- Question 5 of 15TypeScript
Groww interviewer
How do you type React components, props, events and hooks?
Model answer
type ButtonProps = React.ComponentProps<'button'> & { variant?: 'primary' | 'ghost'; }; function Button({ variant = 'primary', children, ...rest }: ButtonProps) { return ( <button data-variant={variant} {...rest}> {children} </button> ); } function Search() { const [query, setQuery] = useState(''); // inferred as string const [user, setUser] = useState<User | null>(null); const inputRef = useRef<HTMLInputElement>(null); const onChange = (event: React.ChangeEvent<HTMLInputElement>) => { setQuery(event.target.value); }; return <input ref={inputRef} value={query} onChange={onChange} />; }Points worth saying out loud:
React.ComponentProps<'button'>gives you every native button prop, so you do not retypeonClickanddisabled.childrenisReact.ReactNode.- Let
useStateinfer simple types, and pass a type argument when the initial value isnullor an empty array. - Components can be generic:
function List<T>({ items, renderItem }: ListProps<T>)keeps the item type through torenderItem.
React
4 questions- Question 6 of 15React
Groww interviewer
What makes a component re-render, and how do you cut unnecessary re-renders?
Model answer
A component re-renders when its own state changes, when its parent re-renders, or when a context it reads changes. A changed prop is not a separate cause: props change because the parent rendered.
A re-render is React calling the function again. It is not a DOM update, and most are cheap. Measure with the React DevTools Profiler before changing anything.
When one is a real problem, in rough order of preference:
- Move state down into the component that uses it, so fewer components sit below it.
- Pass expensive content as
children. Elements created by a parent that did not re-render are reused as they are. - Wrap a costly child in
React.memoand give it stable props. - Split a large context, or use a store that lets components subscribe to a slice.
- Virtualise long lists.
- Question 7 of 15React
Groww interviewer
When should you use useMemo and useCallback?
Model answer
useMemocaches a computed value anduseCallbackcaches a function, both until their dependencies change. They only pay off in three cases:- The calculation is slow enough to notice, such as filtering or sorting a large list.
- The value is passed to a child wrapped in
React.memo, which needs the same reference to skip rendering. - The value is a dependency of an effect or of another memoised value.
const visible = useMemo( () => products.filter((p) => p.name.includes(query)), [products, query] ); const handleSelect = useCallback((id) => setSelected(id), []); return <ProductList items={visible} onSelect={handleSelect} />; // ProductList is memo()Everywhere else they add code and a dependency comparison for nothing. An inline arrow function passed to a plain
buttoncosts almost nothing.The React Compiler adds this memoisation at build time, so codebases that use it need far fewer of these hooks written by hand. Knowing why they exist still matters for the interview.
- Question 8 of 15React
Groww interviewer
Why does a setInterval inside useEffect keep seeing the old state? How do you fix it?
Model answer
function Counter() { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => { setCount(count + 1); // count is always 0 here }, 1000); return () => clearInterval(id); }, []); return <p>{count}</p>; // goes to 1 and stays there }Every render has its own
count. The effect runs once, so its callback closes over thecountfrom the first render, which is 0, and keeps setting the state to 1. This is a stale closure.Fixes:
- Use the updater form,
setCount((c) => c + 1). It does not readcountfrom the closure at all. This is the right fix here. - Add
countto the dependency array. That is correct, but it tears down and recreates the interval every second. - Keep the latest value in a ref when the callback needs to read state without restarting.
The
react-hooks/exhaustive-depslint rule catches this. Explain why it warns instead of silencing it. - Use the updater form,
- Question 9 of 15React
Groww interviewer
How would you render a list of 10,000 rows without the page slowing down?
Model answer
Render only the rows that are on screen. This is called windowing or virtualisation.
The container gets the full scroll height (row count times row height). From the scroll position you work out which rows are visible, render those plus a few extra above and below, and position them with a transform. Scrolling changes which twenty or so rows exist.
In practice use a library such as TanStack Virtual or react-window. Writing a fixed-height version by hand is a common interview task.
Things to raise:
- Rows of unknown height have to be measured after they render.
- Fetch the data in pages too. Virtualising 10,000 rows you downloaded in one request only solves half the problem.
- Use stable keys and memoised rows.
- The browser's find-in-page cannot see rows that are not rendered, and screen readers need
aria-rowcountandaria-rowindex. content-visibility: autoin CSS is a cheaper option for moderately long pages.
Next.js
2 questions- Question 10 of 15Next.js
Groww interviewer
How does caching work in the Next.js App Router, and how do you invalidate it?
Model answer
There are several layers, and naming them is most of the answer:
- Request memoisation: the same
fetchmade twice during one render is sent once. - Data cache: results of
fetchcalls stored on the server across requests and deployments. - Full route cache: the rendered output of static routes.
- Client router cache: pages the browser has already visited or prefetched.
// Refetch at most once an hour, and tag it for manual invalidation const res = await fetch('https://api.example.com/products', { next: { revalidate: 3600, tags: ['products'] }, }); // Inside a Server Action or route handler, after a change: revalidateTag('products'); revalidatePath('/products');Be careful with defaults, because they changed. In Next.js 14 a
fetchwas cached unless you opted out. From Next.js 15 it is not cached unless you opt in, and newer releases add an explicituse cachedirective. Say which version you are describing, and check the docs for the one you use. - Request memoisation: the same
- Question 11 of 15Next.js
Groww interviewer
When do you add 'use client', and where should the boundary go?
Model answer
Add it when a component needs state, effects, event handlers, browser APIs, or a library that uses any of those.
The directive marks a boundary. That file and everything it imports become part of the JavaScript sent to the browser. So the goal is to put the boundary as low in the tree as possible.
// app/products/page.tsx: a Server Component export default async function Page() { const products = await getProducts(); return ( <main> <h1>Products</h1> <SearchBox /> {/* small client component */} <ProductGrid products={products} /> {/* stays on the server */} </main> ); }- The common mistake is putting
'use client'at the top of a page or layout because one button needsonClick. That ships the whole page. - Props that cross the boundary must be serialisable. You cannot pass a function, apart from a Server Action.
- A Client Component can still wrap server-rendered content if it receives it as
children. A context provider is written this way. - Client Components are still rendered to HTML on the server for the first load. The name is about where they also run, not where they only run.
- The common mistake is putting
CSS
1 question- Question 12 of 15CSS
Groww interviewer
What are CSS custom properties, and how would you build a dark theme with them?
Model answer
A custom property is a value you name with two dashes and read with
var(). Unlike a Sass variable, it exists at runtime: it cascades, it is inherited, and it can be changed by a class, a media query or JavaScript without rebuilding anything.:root { --bg: #ffffff; --text: #0f172a; --accent: #4f46e5; } [data-theme='dark'] { --bg: #0b1020; --text: #e2e8f0; --accent: #818cf8; } body { background: var(--bg); color: var(--text); }Components only use the variables, so switching the theme means changing one attribute on the root element.
- Default to the system setting with
@media (prefers-color-scheme: dark)and let the user override it. - Save the choice, and apply it with a small inline script in the head before the page paints. Otherwise the wrong theme flashes on load.
- Set
color-scheme: light darkso that form controls and scrollbars match. var(--gap, 8px)takes a fallback, andelement.style.setProperty('--x', value)sets one from JavaScript.
- Default to the system setting with
Browser, performance and system design
1 question- Question 13 of 15Browser, performance and system design
Groww interviewer
Compare polling, Server-Sent Events and WebSockets for real-time updates.
Model answer
- Short polling: ask the server every few seconds. Simple and works everywhere. It wastes requests when nothing has changed, and updates are late by up to one interval.
- Long polling: the server holds each request open until it has something to send, then the client asks again. Closer to real time, more connection handling.
- Server-Sent Events: one long-lived HTTP response over which the server pushes text events. One direction only, server to client. The browser's
EventSourcereconnects by itself. Good for notifications, live scores, order status and streamed AI output. - WebSocket: a two-way connection with very little overhead per message. Use it when the client also sends often: chat, collaborative editing, multiplayer, trading screens.
Choose by direction and frequency. If only the server pushes, SSE is simpler to run. If both sides talk frequently, use a WebSocket. If updates are rare, polling is enough.
Operational points that show experience: reconnect with backoff, send heartbeats to detect dead connections, fetch missed events after a reconnect, pause updates when the tab is hidden, and remember that many open connections change how the backend has to scale.
Machine coding
2 questions- Question 14 of 15Machine coding
Groww interviewer
Build a data table with sorting, search and pagination.
Model answer
Keep the raw rows and the controls in state:
sort(keyanddirection),queryandpage. Everything shown is derived from those on each render.const visibleRows = useMemo(() => { const filtered = rows.filter((row) => row.name.toLowerCase().includes(query.toLowerCase()) ); const sorted = sort.key ? [...filtered].sort((a, b) => { const result = a[sort.key] > b[sort.key] ? 1 : a[sort.key] < b[sort.key] ? -1 : 0; return sort.direction === 'asc' ? result : -result; }) : filtered; const start = (page - 1) * pageSize; return { total: sorted.length, rows: sorted.slice(start, start + pageSize) }; }, [rows, query, sort, page, pageSize]);- Copy before sorting.
sortchanges the array in place. - Go back to page 1 when the search or sort changes.
- Clicking a header cycles ascending, descending, none, and shows an arrow.
- Drive columns from a config array (
key,label,sortable, optionalrender). - Use real
tablemarkup, witharia-sorton the sorted header. - Show an empty state when nothing matches.
Follow-ups: row selection with a select-all checkbox that has an indeterminate state, doing the sorting and paging on the server, a sticky header, and virtualising for very large data sets.
- Copy before sorting.
- Question 15 of 15Machine coding
Groww interviewer
Build a countdown timer or a stopwatch with start, pause and reset.
Model answer
The trap is counting ticks.
setIntervalis not exact, and browsers slow timers down in background tabs, so a counter that subtracts one every second drifts. Store timestamps and compute the time from the clock.function useCountdown(durationMs) { const [remaining, setRemaining] = useState(durationMs); const [running, setRunning] = useState(false); const endTime = useRef(0); useEffect(() => { if (!running) return; const id = setInterval(() => { const left = Math.max(0, endTime.current - Date.now()); setRemaining(left); if (left === 0) setRunning(false); }, 250); return () => clearInterval(id); }, [running]); const start = () => { endTime.current = Date.now() + remaining; // resume from where it paused setRunning(true); }; const pause = () => setRunning(false); const reset = () => { setRunning(false); setRemaining(durationMs); }; return { remaining, running, start, pause, reset }; }- The interval only refreshes the display. The remaining time always comes from
Date.now(). - Pause keeps the remaining time. Start computes a new end time from it.
- The interval is cleared on pause, on finish and on unmount.
- Format as
mm:sswith leading zeros, and disable Start while running.
Follow-ups: several timers on one page, laps for a stopwatch, and surviving a page refresh by saving the end time.
- The interval only refreshes the display. The remaining time always comes from
Practise for Groww
Reading answers is the easy half. These problems run in the browser against real tests.
