Build a virtualized feed
Design the behavior contract for a virtualized feed. Focus on state, keyboard interaction, empty/loading/error states, and how the component composes with product data.
Answer Strategy
Virtualization is the question that exposes whether you understand the rendering pipeline. The hard parts are not the math — they are deciding what owns scroll position, how variable item heights are measured, when items are mounted vs unmounted, and how screen readers traverse a list that mostly does not exist in the DOM.
Separate the math from React. A pure getVisibleRange(items, scrollTop, viewportHeight, overscan) function is unit-testable without jsdom and reused identically on the server (for predicted skeletons) and client. The component then mounts only items[startIndex..endIndex] inside a translated container with a spacer that holds the full virtual height.
Volunteer the dangerous cases: variable heights need a measured cache (resize observers) plus a fallback estimated height; sticky headers and grid layouts break naive transform-based offsetting; assistive technology needs role=feed with aria-posinset / aria-setsize so users do not lose context; keyboard arrow keys must scroll the viewport, not move DOM focus to invisible items. The reference shows the fixed-height happy path with the right ARIA semantics — the variable-height extension is a follow-up worth narrating.
Reference Implementation: Virtualized Feed With Pure Range Math
A small VirtualFeed component plus a pure getVisibleRange function that decides what to mount based on scrollTop, viewportHeight, and overscan.
type FeedItem = { id: string; height: number };
// Pure logic that the React component (and tests) call. Decoupling math
// from DOM is what lets you assert correctness without rendering.
function getVisibleRange(
items: FeedItem[],
scrollTop: number,
viewportHeight: number,
overscan: number
): { startIndex: number; endIndex: number; offsetTop: number } {
let runningTop = 0;
let startIndex = 0;
let offsetTop = 0;
for (let index = 0; index < items.length; index += 1) {
if (runningTop + items[index].height >= scrollTop) {
startIndex = Math.max(0, index - overscan);
offsetTop = runningTop - (index - startIndex) * (items[index].height || 0);
break;
}
runningTop += items[index].height;
}
let visible = 0;
let endIndex = startIndex;
for (let index = startIndex; index < items.length; index += 1) {
visible += items[index].height;
endIndex = index + 1;
if (visible >= viewportHeight + overscan * (items[index].height || 0)) break;
}
return { startIndex, endIndex, offsetTop };
}
type VirtualFeedProps<T extends FeedItem> = {
items: T[];
viewportHeight: number;
overscan?: number;
renderItem: (item: T) => React.ReactNode;
};
export function VirtualFeed<T extends FeedItem>({
items,
viewportHeight,
overscan = 4,
renderItem,
}: VirtualFeedProps<T>) {
const [scrollTop, setScrollTop] = React.useState(0);
const totalHeight = React.useMemo(
() => items.reduce((sum, item) => sum + item.height, 0),
[items]
);
const { startIndex, endIndex, offsetTop } = React.useMemo(
() => getVisibleRange(items, scrollTop, viewportHeight, overscan),
[items, scrollTop, viewportHeight, overscan]
);
return (
<div
// role=feed makes the live region semantics explicit. Each item is a
// grouped article. Without these, screen readers cannot identify the
// virtualized list as a feed of separate posts.
role="feed"
aria-busy={false}
style={{ height: viewportHeight, overflow: 'auto' }}
onScroll={(event)=> setScrollTop(event.currentTarget.scrollTop)}
>
<div style={{ height: totalHeight, position: 'relative' }}>
<div style={{ transform: 'translateY(' + offsetTop + 'px)' }}>
{items.slice(startIndex, endIndex).map((item, index) => (
<article
key={item.id}
role="article"
aria-posinset={startIndex + index + 1}
aria-setsize={items.length}
style={{ height: item.height }}
>
{renderItem(item)}
</article>
))}
</div>
</div>
</div>
);
}Executable UI Sandbox
UI interview practice should behave like component documentation, not a static snippet. This uses the same isolation pattern as Storybook, Sandpack, CodeSandbox, and StackBlitz: editable source on one side, a sandboxed browser preview on the other. Edit the DOM code, run it, and verify focus, keyboard, pointer, and state behavior in the preview.
Testing Strategy
Convert the answer into observable behavior. In a mid-senior interview, say which behaviors are covered by unit tests, interaction tests, accessibility checks, and one browser smoke path.
import { describe, it, expect } from 'vitest';
describe('getVisibleRange', () => {
const items = Array.from({ length: 1000 }, (_, index) => ({
id: 'item-' + index,
height: 80,
}));
it('returns the correct slice for the top of the list', () => {
const range = getVisibleRange(items, 0, 320, 2);
expect(range.startIndex).toBe(0);
expect(range.endIndex).toBeGreaterThanOrEqual(4);
expect(range.offsetTop).toBe(0);
});
it('shifts the window when scrolled mid-list', () => {
const range = getVisibleRange(items, 8000, 320, 2);
expect(range.startIndex).toBeGreaterThan(95);
expect(range.startIndex).toBeLessThan(105);
});
it('clamps overscan at the start', () => {
const range = getVisibleRange(items, 100, 320, 50);
expect(range.startIndex).toBe(0);
});
});Interviewer Signal
Shows whether you can build components as interaction systems rather than visual boxes.
Constraints
- Name the controlled and uncontrolled state.
- Define keyboard and focus behavior.
- Include loading, empty, disabled, and error states.
Model Answer Shape
- Start with the accessibility role and interaction contract.
- Separate rendering slots from state management.
- Expose callbacks that describe user intent, not internal implementation details.
Tradeoffs
- A headless primitive is reusable but slower to consume.
- A product-specific component ships faster but can trap behavior in one use case.
Edge Cases
- Focus after close, selection, deletion, or route change.
- Large datasets and slow network responses.
- Screen reader labels and live updates.
Testing And Proof
- Keyboard path through the primary workflow.
- A11y names, descriptions, and roles.
- State transition after slow or failed data load.
Follow-Ups
- How would this component be documented in a design system?
- What props would you refuse to expose?