Let’s charge into Question 3, which is a heavy favorite for remote teams dealing with data-heavy dashboards, social media feeds, or e-commerce storefronts.

It tests your ability to handle large lists of data efficiently without melting the user's browser, and it deeply examines your mastery of modern browser utility Web APIs instead of relying on heavy third-party npm packages.

Question 3: Handcrafting an Infinite Scroll List (手写无限滚动列表)

Interviewer (Me) Statement of Requirements:

"Imagine we are building a product discovery page for an e-commerce platform. The backend provides a paginated API endpoint: /api/products?page=[number], which returns an array of 10 items per page, alongside a boolean flag hasMore."

"Please write a React client component that renders this list of products. As the user scrolls down the page and approaches the bottom, the component must automatically fetch and append the next page of data seamlessly without making the user click a 'Load More' button."

The Initial Constraints / 初始硬性限制:

  1. No Third-Party Libraries: You cannot install packages like react-infinite-scroll-component or react-window.
  2. Performance Rule: You are strictly forbidden from listening to the high-frequency global window.onscroll event, as it severely damages frame rates and causes mobile browser jank. You must use the modern browser Intersection Observer API.

 

Think Out Loud / 中英双语思考提示:

Before looking at the syntax, how does an Intersection Observer infinite scroll actually work conceptually? You place a tiny, invisible "sentinel" element (like a spinner or a blank <div>) at the very bottom of your list, right below your last product card. You tell the browser to watch that element. When that element intersects with the viewport (meaning the user scrolled far enough to see it), you fire the function to load the next page.

English Thought Process:

"I need to maintain an array of items in state, alongside a page number counter and a loading flag. I will map through the items to render them. At the bottom of the list, I will insert a div element acting as our 'sentinel'." "I will create a useEffect that instantiates an Intersection Observer. I'll pass a callback to it: if entries[0].isIntersecting is true and we aren't already loading data, I will increment the page count to trigger a fetch. Finally, I must ensure I disconnect the observer in the cleanup function."

中文思考路径:

“首先我需要在 state 里维护一个数据列表数组、一个当前页码 page 计数器以及一个加载状态 loading 标志。我用 .map() 渲染所有商品数据。在列表的最底部,我塞入一个空 <div> 作为我们的‘哨兵(Sentinel)’。” “接着我写一个 useEffect 来实例化浏览器的 IntersectionObserver。给它传一个回调函数:如果发现 entries[0].isIntersecting 为真(代表哨兵出现在屏幕中了),且当前没有处于加载状态,我就把页码加一,从而触发网络请求。最后,必须在清除函数里把这个监听断开(disconnect),防止内存泄漏。”

 

Over to You!

Don't let the native API scare you. Try to lay down the state architecture and the list mapping first.

Show me your first-pass code structure or talk me through your implementation steps in English or Chinese!

 

手写代码

No worries at all! Handcrafting an infinite scroll list with the Intersection Observer API is one of those tasks that sounds intimidating at first, but becomes incredibly elegant once you see the pattern.

Let's build this together step-by-step using our Bilingual (中英双语) approach.

 

Step 1: Set Up the Component State & List (设置组件状态与列表渲染)

First, we need to declare our states to track the list data, the current page number, and whether we are currently fetching data or if we have reached the absolute end of the backend database.

English Thought Process:

"First, I need states for items (an empty array initially), page (starting at 1), loading (false), and hasMore (true). I'll map over the items to render them on screen, and append a specific empty div at the very bottom with a React useRef attached to it. This div will be our sentinel."

中文思考路径:

“第一步,我需要定义几个核心状态:items(初始化为空数组)、page(从第1页开始)、loading(加载状态)以及 hasMore(用来判断后端是不是还有数据)。我用 .map() 渲染产品列表,并在列表的最末尾放一个空的 div,用 useRef 钩子绑住它。这个 div 就是我们的‘哨兵’。”

 

Step 2: Connect the Intersection Observer (挂载浏览器哨兵监听器)

Now, we introduce the IntersectionObserver. We tell the browser: "Watch my sentinelRef. If the sentinel enters the screen, and we aren't currently loading anything, and there's more data to fetch, increment the page number by 1."

English Thought Process:

"Next, I'll write a useEffect to instantiate the IntersectionObserver. In its callback, if entry.isIntersecting is true, it means the sentinel is visible. If we aren't loading and hasMore is true, I will trigger setPage(prev => prev + 1). Crucially, I will run observer.disconnect() in the cleanup to avoid memory leaks."

中文思考路径:

“第二步,写一个 useEffect 来实例化原生的 IntersectionObserver。在回调函数里判断:如果 entry.isIntersecting 为真,说明用户已经滚到底部看到哨兵了。只要当前没有在加载,且后端还有数据,我就让页码自动加一:setPage(prev => prev + 1)。最关键的是,在清除函数里执行 observer.disconnect() 断开监听,防止页面卡死或内存泄漏。”

 

Step 3: Fetch and Aggregate the Data (网络请求与数据拼接)

Finally, we need a separate useEffect that fires whenever the page count changes. It fetches the raw data and concatenates (appends) it to our existing list.

English Thought Process & Explanation:

"Lastly, I'll add another useEffect dependent on page. Inside, I will turn loading to true, fetch the next page from our simulated API, append the new items onto the existing ones using the spread operator (...), and update hasMore based on the backend flag."

中文思考与解释:

“最后,添加一个专门监听 page 改变的 useEffect。一旦页码加一,就把 loading 设为 true,去后端请求下一页的数据。拿到新数据后,用展开运算符 ... 把它追加到原有数据的屁股后面,并用后端传回的 hasMore 更新我们的边界标志。”

 

The Final Self-Contained Implementation:

 

追问

Brilliant! You successfully built a highly efficient, custom native infinite scroll list without loading a single mega-byte of npm packages.

But as you know by now, a senior remote interviewer is about to put on a strict performance audit face. Here is the follow-up question coming straight at you:

"Your implementation is functionally clean. However, consider the Fast Scrolling / Double-Trigger Bug. If a user has a very fast fiber connection and scrolls down incredibly quickly, the sentinel might trigger page 2. Before the network response for page 2 returns, the sentinel is STILL visible on screen, meaning the observer will fire AGAIN and trigger page 3 immediately! How do you guarantee that you prevent this multi-trigger duplication while a network request is still pending?"

中文翻译提示:

“你的实现功能很清晰。但考虑一下 ‘极速滚动/二次重复触发 Bug’。如果用户的网速极快,并且疯狂向下滚动,哨兵会触发加载第 2 页。但在第 2 页的网络响应还没返回的这几百毫秒内,哨兵依然暴露在屏幕上,这意味着监听器会再次触发并直接去加载第 3 页!你怎么绝对保证,在一个网络请求还挂起(Pending)时,绝不会发生这样重复触发页码加载的 Bug?

This is an incredibly common edge-case glitch in real-world frontend apps. How would you answer or block this from happening? Try to explain or ask for the dual-language architecture patch!

 

追问解答

This is an excellent catch by the interviewer, and it happens all the time in real-world products. If you don't block this, your app will accidentally fetch page 2, page 3, and page 4 all at the exact same time, causing a messy UI overlap.

Let’s solve this Double-Trigger Bug using our Bilingual (中英双语) strategy.

The Solution: The "Strict Gatekeeper" Pattern (严格守门员模式)

The Core Problem (核心问题):

Inside our IntersectionObserver callback, we have a check: if (firstEntry.isIntersecting && !loading). However, due to how React state batching works, right after you call setPage(page + 1), the state hasn't re-rendered yet. For a split microsecond, loading is still false, allowing the observer to slip past the gate and trigger again Interview Feedback - Interviewing.io.

The Elegant Fix (优雅的修复方案):

Instead of relying purely on a asynchronous React state variable (loading), we can introduce a mutable instance variable using a React useRef (e.g., isFetchingRef.current).

Unlike state, updating a useRef value happens instantaneously and synchronously Interviewing.io Tips. We change it to true the exact millisecond the sentinel is crossed, creating an un-bypassable iron gate.

 

Code Adjustment (代码微调)

Let's upgrade your component with this foolproof synchronization layer:

English Thought Process & Code:

"To eliminate the double-trigger bug, I will introduce a isFetchingRef using useRef(false). The very instant the observer sees an intersection, I will flip isFetchingRef.current = true synchronously. This completely blocks any subsequent triggers until the async network operation finishes and flips it back to false."

中文思考与代码:

“为了彻底杜绝极速滚动导致的重复触发 Bug,我会引入一个 isFetchingRef 钩子,初始值为 false。就在监听器发现‘哨兵’露面的那一瞬间,我同步、立刻isFetchingRef.current 设为 true。这会像一把铁锁一样死死卡住后续的任何多余触发,直到整个异步网络请求彻底完成并返回数据后,我们再把它放开重置为 false。”

 

继续追问: The "DOM Node Melting" Crash (终极架构追问:内存暴涨崩溃)

If you solve the double-trigger bug, the interviewer will give you a massive nod of approval. But if they want to see if you can handle Staff / Architect level complexity, they will ask one final question before moving on:

Interviewer:

"Your scrolling loop is now perfectly safe. But imagine the user scrolls for 20 minutes and loads 5,000 product cards into the list. The browser DOM tree now contains tens of thousands of active image and text nodes. The page will become extremely laggy, skip frames, and eventually crash the smartphone's browser tab due to severe memory pressure."

"How would you conceptually upgrade this architecture to prevent 'DOM melting' when handling a near-infinite amount of records? (Hint: Think about what Twitter or Instagram does)."

中文翻译提示:

“你的滚动循环现在完美无瑕了。但想象一下,如果用户往下划拉了20分钟,加载了 5000 个商品卡片。此时浏览器的 DOM 树里会塞满几万个活跃的图片和文字节点。页面会变得极度卡顿、疯狂掉帧,最后让手机浏览器直接因为内存内存耗尽而崩溃。”

“你该如何在概念上升级这个架构,来应对这种数据量无限增长的场景,防止 DOM 结构过载崩溃?(提示:想想 Twitter 或者 Instagram 的做法)”

The Conceptual Answer (双语大局观解答):

To pass this final boss question, you must introduce the concept of "Virtual Scrolling" (虚拟滚动) or "Windowing" (窗口化化技术).

English Explanation to Interviewer:

"To handle massive lists without melting the DOM, we must implement Virtualization. Instead of rendering all 5,000 cards, we calculate the wrapper's height and only render the 10 or 15 items currently visible within the viewport, plus a tiny buffer above and below. As the user scrolls, we recycle the DOM nodes dynamically, replacing their inner text and data while using CSS transforms to position them correctly. This keeps the total DOM count perfectly flat at 15 elements, whether the database has 100 items or 1,000,000 items."

中文解释给面试官:

“为了处理海量列表而不至于撑爆 DOM 树,我们需要引入‘虚拟列表技术(Virtualization)’。我们不渲染全部的 5000 个卡片,而是根据视口高度计算,只渲染当前处于屏幕可视区域内的 10 到 15 个组件(外加头部和尾部的一点点缓存缓冲区)。随着用户滚动,我们动态复用这些 DOM 节点,仅仅替换里面的文字和图片数据,同时利用 CSS transform 撑开高度定位。这样无论列表里有 100 条还是 100 万条数据,浏览器里真实的 DOM 节点永远只有 15 个,性能永远是满分。”

 

继续追问

Yes, we can push this into senior production engineering territory.

Now that you have mastered the useRef sync lock and explained DOM Virtualization, an elite interviewer will test your ability to handle real-world user interface glitches and network instability.

Here are the final 2 advanced follow-up questions for the Infinite Scroll architecture, explained in 中英双语 (Bilingual):

Advanced Follow-Up 1: The "Scroll Position Jump" Layout Shift (无尽滚动的布局抖动)

Interviewer:

"Imagine each product card contains an image. When the user scrolls down, the sentinel triggers, and you append 10 new items. However, because the images take a fraction of a second to load, the new cards initially have a height of 0px before popping open to 300px."

"This causes the page layout to violently shake and jump, which ruins the user experience and ruins our Google Cumulative Layout Shift (CLS) web vital score. How do you prevent this layout shift when dynamically appending uneven content?"

中文翻译提示:

“想象一下,每个商品卡片里面都包含一张大图。当用户滚到底部,哨兵触发,你追加了 10 个新商品。然而,由于图片需要零点几秒来加载,新卡片在图片加载出来之前高度是 0px,随后突然撑大到 300px。”

“这会导致整个页面布局发生剧烈的抖动和跳跃,严重破坏用户体验,还会砸掉我们谷歌的 CLS(累积布局偏移)性能指标。你怎么在动态追加高度不固定的内容时,防止这种布局抖动?

The Solution / 解决方案:

We must implement structural placeholders and enforce strict aspect ratios. We should never let an image render without an explicit or container-bounded size.

English Answer: "I will assign a fixed structural aspect-ratio to the product card image containers (e.g., using Tailwind's aspect-square or aspect-video) and render a Skeleton Shimmer placeholder while loading. This reserves the exact layout pixel coordinates in the DOM tree *before* the network data arrives, dropping layout shifts to absolute zero."

中文回答: “我会给商品卡片的图片容器指定一个固定的物理宽高比(例如使用 Tailwind CSS 的 aspect-squareaspect-video),并在加载时渲染一个带有渐变动画的骨架屏(Skeleton Shimmer)。这能在数据返回之前就在 DOM 树里提前锁死它应有的像素高度,从而将布局偏移降到绝对的零。”

 

Advanced Follow-Up 2: Adaptive Error Recovery / Network Failures (断网容错与自动恢复机制)

Interviewer:

"Suppose the user is scrolling on a train, enters a tunnel, and loses their internet connection right when the sentinel crosses the viewport. Your fetchNewProducts triggers, hits a network error, and sets isFetchingRef.current = false."

"Now the user exits the tunnel and regains internet. But because the sentinel is *already* sitting inside the viewport, the IntersectionObserver won't fire again because it only triggers on the *transition* of entering the screen. The infinite scroll is now completely frozen. How do you implement an elegant error fallback and recovery state?"

中文翻译提示:

“假设用户正在火车上刷手机,突然进了解一个隧道,正好在哨兵划进屏幕的那一刻断网了。你的请求触发,遭遇网络错误挂掉,并重置了锁 isFetchingRef.current = false。”

“现在用户出了隧道,网络恢复了。但由于此时哨兵已经静静地躺在屏幕里了,IntersectionObserver 不会再次触发(因为它只有在元素‘由外向内进入’的刹那才会触发)。无尽滚动此时彻底卡死。你该怎么实现一个优雅的错误降级与重试恢复机制?

The Solution / 解决方案:

We introduce a local error state. If a network request fails, we conditional-render a manual "Retry Button" instead of the sentinel. Clicking the retry button manually runs the fetch again, clears the error, and brings back the automated observer tracking.

Question 3 Is Now 100% Completed!

We have successfully battle-tested this Infinite Scroll system from a basic implementation to production-level reliability. You have fully proved you can control memory, bypass browser limits, protect SEO performance layout scores, and handle real-world mobile internet disconnections Frontend Lead Mock Interview (8+ Years Experience).

We can now cleanly seal this topic away.