Requirements exploration
In this first step, the interviewer wants to see if you truly understand the problem before jumping into solutions. Act like someone who thinks with clarity: restate the goal, identify the core challenge, and ask a few smart clarifying questions.
**What you're solving:**
A scrolling list that keeps loading more items when the user reaches the bottom. You need to make it feel smooth, predictable, and easy to recover from errors.
**What the interviewer looks for:**
- You don't rush into coding.
- You understand the user experience first.
- You’re aware of performance concerns early.
- You identify states: loading, success, empty, end, error.
- You already think about reuse and component boundaries.
How you should act here
Slow down and frame the problem. Show that you understand: "We need a list that loads progressively and stays smooth even after many items. Let me clarify a few things first." This immediately signals senior-level thinking.
The user flow you should describe
1
1. First load
Show skeletons → fetch first items → render them.
→
2
2. Scroll near bottom
A sentinel triggers a 'load more' action.
→
3
3. Append new items
Show loading placeholders → fetch → append.
→
4
4. End or error
"End of list" message or an inline retry button.
Smart clarifying questions you should ask
- How many items can exist in total?
- Does the API use page numbers or cursors?
- Do items have fixed or variable height?
- Should scroll position be restored?
- Can items update while the user scrolls?
What you should explicitly confirm
- Scrolling should stay smooth at all times.
- Loading states should not jump the layout.
- The DOM shouldn't grow endlessly.
- Accessibility must still work.
- The component should be reusable with different item UIs.
Key UX goal
Smooth scrolling
Your design must avoid jank while loading new items.
Key performance idea
Limit DOM size
Too many nodes = lag. Plan ahead.
Key FE decision
When to virtualize?
Introduce it only if the list grows very large.
Critical trade-off you should highlight
If you keep too many items in the DOM, scroll slows down. If you virtualize too aggressively, managing scroll position becomes tricky. Good candidates acknowledge this tension early.
Architecture / High-level design
In this step, the interviewer wants to see if you can turn a vague idea ("infinite scroll list") into a clean set of components and data flows. You should talk in terms of boxes and arrows: where state lives, who fetches data, and how scrolling triggers new loads.
**How you should frame it:**
"At a high level, I’d split this into a page container, a reusable `InfiniteScrollList` component, a data-fetching layer, and a pluggable item renderer. The list manages scroll + pagination state, the data layer talks to the API, and the page decides what to render inside each row."
**What the interviewer listens for:**
- You don’t stuff everything into one giant component.
- You put state and side-effects in the right place.
- You can describe data flow clearly: "event → state update → re-render".
- You think about reuse (same list used on multiple pages).
How you should act here
Draw or describe 3–4 boxes max. Keep it simple: Page → InfiniteScrollList → DataFetcher + ItemRenderer. Show that you can separate responsibilities without over-engineering.
Core building blocks you should mention
| Piece | Responsibility | What you say out loud |
|---|---|---|
| Page / Screen | Owns filters, layout and passes props into the list. | "This page decides which API to call and how each item should look. It passes those down to the list as props." |
| InfiniteScrollList component | Tracks pages, items, loading, error, end-of-list; wires IntersectionObserver. | "The list component owns pagination state and observes a sentinel at the bottom to know when to load more." |
| Data fetching layer (hook/service) | Wraps the HTTP calls and hides pagination details (page vs cursor). | "I’d hide API details behind a small helper so the list just calls `loadNextPage()` and gets items back." |
| Item renderer | Pure visual component for a single item (row/card). | "Rendering of each item is separate, so the list can be reused with different UIs: rows today, cards tomorrow." |
| State store (optional) | Keeps items/filters across navigation, if needed. | "If we need to preserve scroll + filters between visits, I’d lift state into a store or higher-level container." |
Things you should explicitly say
- The list component owns pagination state (current page, items, `hasMore`, loading, error).
- IntersectionObserver lives inside the list and observes a sentinel element at the bottom.
- The data layer exposes a simple API like `loadInitial()` and `loadMore()`.
- The item renderer is stateless and only receives props.
- If needed, global state can keep filters and scroll position.
Things that are red flags
- Mixing fetching logic and heavy layout in one huge component.
- Letting many components manage their own pagination state.
- Triggering network calls directly from scroll events everywhere.
- Tightly coupling the list to one specific item layout.
- No clear place where errors and loading state are handled.
Data flow you should walk through
1
1. Page mounts
Page renders `InfiniteScrollList` and passes a `fetchPage` function and an `ItemComponent`.
→
2
2. Initial fetch
List calls `fetchPage(0)` via the data layer, sets `loading=true`, then stores the first batch of items.
→
3
3. Observe sentinel
List attaches IntersectionObserver to a sentinel at the bottom. When it becomes visible and `hasMore` is true, it triggers `fetchPage(nextPage)`.
→
4
4. Append items
New items are merged into the existing array; list updates `hasMore`, `loading`, and `error` accordingly.
State ownership
Centered in list
One clear place tracks pages, items, and loading.
Side-effects
Isolated
Fetching + IntersectionObserver live in well-defined components/hooks.
Reusability
High
Same list works with different item UIs and different APIs.
Key message to land
Good architecture here is not about fancy patterns. It’s about clear responsibilities: one place for scroll + pagination logic, one place for data fetching, and simple, reusable item components.
Data model
In this step, the interviewer wants to see if you can describe the shape of the data your components work with. You should show that you understand: what an item looks like, how pagination is represented, and which UI states must be part of the model (loading, error, end-of-list, filters, etc.).
**How you should talk about data:**
"I’ll keep the data model simple: one type for each list item, one for pagination/meta info, and one for the list’s UI state. The page can also have filters that affect which items we load."
**What the interviewer listens for:**
- You keep entities small and focused.
- You treat loading/error/end-of-list as first-class data.
- You don’t mix view-only details (like DOM nodes) into the data model.
- You can explain where each piece of data lives and who owns it.
How you should act here
Name the main types out loud. Keep them simple. Show that you’re modeling the data your components actually need, not designing a full backend schema.
Core entities you should define
| Entity | Fields (example) | What you say out loud |
|---|---|---|
| ListItem | id, title, description, thumbnailUrl, createdAt | "Each row in the list is a `ListItem`: it has an `id` plus whatever fields the UI needs like title, description and maybe an image." |
| PaginationMeta | currentPage, pageSize, hasMore, totalCount? | "`PaginationMeta` tracks where we are: current page, page size, whether there are more items, maybe a total count if the API provides it." |
| ListUiState | items[], status, errorMessage, isInitialLoad, isLoadingMore | "`ListUiState` includes the array of items plus UI flags: `status` (idle/loading/success/error), optional `errorMessage`, and booleans like `isInitialLoad` vs `isLoadingMore`." |
| Filters (optional) | searchQuery, sortBy, selectedTag, etc. | "If the list is filterable, I’d keep a small `Filters` object with things like search query, sort option or selected tag and pass it into the fetch calls." |
| Cursor (optional alternative) | cursorToken, hasMore | "If the API is cursor-based, instead of `currentPage` I’d keep a `cursorToken` from the last response and a `hasMore` flag." |
Things you should explicitly name
- A clear type for each list item.
- A structure that tracks pagination (page or cursor).
- Flags for `isInitialLoad` and `isLoadingMore`.
- `hasMore` to know when to stop fetching.
- An error field for showing inline error messages.
Things you should avoid
- Designing a huge database schema instead of a UI-oriented model.
- Storing raw DOM elements in state.
- Hiding loading/error logic in local variables instead of the model.
- Relying only on implicit states (e.g. `items.length === 0` means loading).
- Using many separate booleans that are hard to reason about.
How data changes over time
1
1. Initial load
`ListUiState` starts with `items = []`, `status = 'loading'`, `isInitialLoad = true`.
→
2
2. First response
Items are filled, `PaginationMeta` is updated, `status = 'success'`, `isInitialLoad = false`.
→
3
3. Load more
When sentinel fires, `isLoadingMore = true`; on success, new items are appended and `hasMore` is updated.
→
4
4. Error or end of list
On error, `status = 'error'` and `errorMessage` is set. When `hasMore` is false, the model represents that we reached the end.
Must-have entities
Item + Pagination + UI state
These three cover 90% of the interview discussion.
Key idea
Model UI states
Loading, error, and end-of-list are part of the data model.
Good signal
Small, composable types
If you can explain each type in one sentence, you’re on the right track.
Key message to land
A strong frontend data model is not just about the items. It also captures pagination and UI states explicitly, so the rendering logic stays simple and easy to reason about.
Interface definition (API)
In this step, the interviewer wants to see if you can design clean boundaries between pieces of your UI. You should show that you can define simple, focused interfaces: what props a list component takes, what a fetch function returns, and which callbacks are exposed.
**How you should talk about APIs:**
"I’d like to keep a very small surface area: the page passes a `fetchPage` function and an item renderer into the list. The list calls `fetchPage` with pagination info and receives items plus a `hasMore` flag. Optionally, we expose callbacks like `onItemsChange` or `onEndReached`."
**What the interviewer listens for:**
- Your interfaces are easy to read and reason about.
- You separate concerns: fetching vs rendering vs state.
- You think about error and end-of-list in the API, not as afterthoughts.
- You avoid leaking implementation details (e.g. IntersectionObserver) into external props.
How you should act here
Speak in terms of function signatures and props, but keep them human: "The list takes a `fetchPage(params)` function and an `ItemComponent`. `fetchPage` returns `{ items, hasMore, nextCursor? }`." You don’t need full TypeScript, just clear contracts.
Core interfaces you should define out loud
| Interface | Shape (example) | What you say out loud |
|---|---|---|
| FetchPageParams | { page?: number; pageSize: number; cursor?: string; filters?: Filters; } | "`FetchPageParams` carries pagination info (page or cursor) plus optional filters like search or sort." |
| FetchPageResult | { items: ListItem[]; hasMore: boolean; nextCursor?: string; } | "`FetchPageResult` returns an array of items, a `hasMore` flag, and optionally a `nextCursor` if we use cursor-based pagination." |
| FetchPageFn | fetchPage(params: FetchPageParams) → Promise<FetchPageResult> | "The list only knows about a `fetchPage(params)` function that returns a promise with items and `hasMore`." |
| InfiniteScrollListProps | { fetchPage, ItemComponent, pageSize?, initialFilters?, onEndReached?, onError?, onItemsChange? } | "`InfiniteScrollList` receives a `fetchPage` function, an `ItemComponent` for rendering, optional `pageSize` and `initialFilters`, and callbacks for events like end reached or errors." |
| ItemComponent | ItemComponent(props: { item: ListItem; index: number }) | "The item renderer is a simple component that receives a `ListItem` and an index and just returns UI." |
Things you should explicitly expose
- `fetchPage(params)` for pagination-aware data loading.
- A way to inject the item renderer (component or render function).
- Optional `pageSize` and filter props.
- An `onError` callback for logging or toast notifications.
- An `onEndReached` callback if the parent cares when the list finishes.
- An `onItemsChange` callback if the parent wants the full list.
Things you should avoid in the API
- Exposing low-level scroll/observer details as props.
- Forcing the parent to manage many tiny state flags.
- Returning different shapes depending on the page (hard to reason about).
- Mixing concerns, e.g. `fetchPageAndRender()` in one function.
- Using magical string flags instead of clear booleans / fields.
Request lifecycle you should walk through
1
1. Initial call
On mount, the list calls `fetchPage({ page: 0, pageSize, filters })`.
→
2
2. Response handling
The promise resolves to `{ items, hasMore }`. The list stores items, updates pagination meta, and sets its UI state.
→
3
3. Load more
When the sentinel is visible and `hasMore` is true, the list calls `fetchPage({ page: nextPage, pageSize, filters })` or uses `nextCursor`.
→
4
4. Errors
If `fetchPage` rejects, the list sets an error state, calls `onError(err)` and shows an inline retry option.
→
5
5. End of list
When a response comes back with `hasMore = false`, the list stops requesting and can fire `onEndReached()` once.
API surface
Small & clear
A few well-named props and one fetch function are enough.
Direction of data
Top → down, events → up
Parents pass data/functions down; list emits events up.
Good signal
Contracts first
You explain interfaces before diving into implementation.
Key message to land
A strong interface makes the list easy to reuse and hard to misuse. If another engineer can use your component just by reading the props and function signatures you described, you’ve designed the API well.
Optimizations and deep dive
In this last step, the interviewer wants to see if you know where this design can break under real-world load and how you would improve it without over-engineering. You should talk about: performance, memory, UX on slow devices, and how you’d iterate based on measurements.
**How you should frame optimizations:**
"First, I’d ship a clean, straightforward implementation. Then I’d measure how it behaves with large data sets on mid-range devices. Based on that, I’d add targeted optimizations like virtualization, smarter rendering, and better loading states."
**What the interviewer listens for:**
- You start from a baseline and then optimize, not the other way around.
- You know the main bottlenecks: DOM size, unnecessary renders, layout thrashing.
- You can explain trade-offs, not just list buzzwords.
- You’re able to go deeper if they zoom in on one area.
How you should act here
Pick 2–3 realistic pain points (huge lists, slow phones, flaky networks) and walk through how you’d detect issues and improve them. Talk like someone who has actually debugged janky UIs, not like a cheat sheet of random tricks.
Baseline hygiene you should mention
- Use IntersectionObserver instead of scroll events for load-more triggers.
- Avoid expensive work on every scroll; let the browser handle scrolling.
- Keep list items as lightweight components with stable keys.
- Avoid unnecessary re-renders by memoizing heavy item subtrees where needed.
- Lazy-load images and heavy assets as items enter the viewport.
- Batch state updates so loading states don’t cause multiple reflows.
When the list becomes really large
- Introduce list virtualization/windowing so only visible items are mounted.
- Consider a small overscan buffer to keep scrolling smooth while not bloating the DOM.
- Be careful with variable-height items: measure heights or accept a simple approximation.
- Reuse DOM nodes where possible instead of constantly creating/destroying elements.
- Persist filters and scroll position so users can come back without re-loading everything.
Deep-dive topics you can offer
| Topic | Angle | How you talk about it |
|---|---|---|
| Virtualization vs simple list | Perf vs complexity | "I’d start with a simple list. If profiling shows too many nodes in the DOM, I’d add virtualization. That adds complexity around scroll position and accessibility, so I’d only introduce it when the data size justifies it." |
| Preserving scroll & filters | UX continuity | "If the user navigates away and comes back, I’d store filters and the current scroll offset (or item index) in a store or route state so they don’t lose context." |
| Caching & prefetch | Fewer network calls | "When users often scroll back and forth, I’d keep previous pages in memory and maybe prefetch the next page when we see fast scrolling." |
| Accessibility under virtualization | Keyboard & screen readers | "With virtualization, I’d verify that keyboard navigation and screen readers can still reach items, and that announcements for loading states are clear." |
| Error & retry strategy | Resilience | "Instead of a blocking modal, I’d show inline errors with a Retry button at the bottom of the list and maybe exponential backoff behind the scenes." |
Optimization path you should describe
1
1. Build the simple version
Clean list, IntersectionObserver sentinel, good loading/error states, no premature micro-optimizations.
→
2
2. Measure
Test with large datasets and mid-range devices. Use browser devtools to watch DOM node count, frame rate, and long tasks.
→
3
3. Fix obvious bottlenecks
Reduce unnecessary renders, lazy-load images, simplify item layout, and avoid heavy work inside render cycles.
→
4
4. Add virtualization if needed
When DOM size becomes the main issue, introduce windowing, handle scroll/keyboard/accessibility carefully, and re-measure.
Biggest perf win
Control DOM size
Most problems come from too many mounted nodes.
Most common issue
Janky scroll
Caused by heavy work or layout thrash while scrolling.
Strong senior signal
Measure → then optimize
You talk about profiling and trade-offs, not random tweaks.
Key message to land
Great candidates don’t just say "I’d use virtualization". They explain when it’s needed, what it breaks if done carelessly (scroll position, accessibility), and how they’d validate that the final experience is both fast and pleasant to use.
Use guided tracks for structured prep, then drill company-specific question sets when you need targeted practice.