题目一演练:手写 useFetch 自定义 Hook

题目要求

面试官口述需求:

“请在不支持任何第三方库(不能用 React Query/SWR)的情况下,用原生 React 手写一个自定义 Hook,命名为 useFetch。它可以传入一个 URL,并返回 { data, loading, error }。”

 

我们现在就通过中英双语(Bilingual),像真实面试一样,一步步把这道题推导出来。

在面试中,当你不知道怎么做时,最棒的策略是 “Think Out Loud”(大声思考)。我们可以把这个问题拆成 3 个简单的步骤。

 

Step 1: Define the State (定义状态)

首先,我们要想:这个 Hook 需要返回什么?面试官说要返回 data, loading, 和 error。所以,我们需要在 Hook 内部用 useState 把这三个状态定义出来。

English Thought Process:

First, I need to define three states using useState to track the API response: data for the fetched result, loading to indicate the request status, and error to catch any potential failures. Then, the hook will return an object containing these three states.

中文思考路径:

“第一步,我需要用 useState 定义三个状态来追踪 API 的响应:data 存返回的结果,loading 表示加载状态,error 捕获可能的错误。最后,这个 Hook 会返回一个包含这三个状态的对象。”

 

Step 2: Trigger the Side Effect (触发副作用发请求)

url 发生变化时,我们需要触发网络请求。在 React 中,处理网络请求这种“副作用”的标准做法是使用 useEffect,并且把 url 放在依赖项数组(Dependency Array)里。

English Thought Process:

Next, I will use useEffect to trigger the network fetch whenever the url changes. Inside useEffect, I'll write an async function to perform the standard fetch call, handle the response, and update our states accordingly.

中文思考路径:

“第二步,我使用 useEffect。只要 url 发生改变,就重新触发网络请求。在 useEffect 内部,我会写一个异步函数(async function)来执行标准的 fetch 请求,拿到数据后去更新我们刚刚定义的状态。”

 

Step 3: Handle the Interviewer's Trap (应对面试官的加分追问)

上面的代码已经拿到了 70 分,但真实的欧美面试官绝对会立刻抛出我们之前提过的致命追问(The Race Condition & Cleanup Trap)

What happens if the component unmounts while the fetch is still in progress? Or what if the URL changes rapidly before the first request finishes? (如果请求还没完组件就被销毁了,或者用户疯狂切换 URL 导致前一个请求还没结束、后一个请求又发出了,会发生什么?)

答案是:会发生竞态条件(Race Condition)或者内存泄漏,旧请求返回的数据可能会覆盖新请求的数据!

It will cause race condition or memory leak, data returned by the old request could overwrite the data from a new request.

 

为了解决这个问题,我们需要在 useEffect 中引入原生的 AbortController,并在 useEffectcleanup function(清除函数) 中取消请求。

English Explanation to Interviewer:

To handle race conditions and prevent setting state on an unmounted component, I will use native AbortController. In the cleanup function of useEffect, I will call controller.abort(). If a new URL comes in, the previous pending request will be canceled immediately.

中文解释给面试官:

“为了处理竞态条件并防止在组件销毁后继续设置状态,我会使用浏览器原生的 AbortController。在 useEffect 的清除函数(cleanup function)中,我会调用 controller.abort()。这样如果有一个新的 URL 传进来,前一个还没完成的请求就会被立刻取消。”

 

最终满分全英文/全手写代码:

 

恭喜你,第一题通关!

看,在没有 AI 的情况下,顺着 “定义状态 -> 处理副作用 -> 优化边界条件” 的逻辑,配上原生的 AbortController,你就能完美写出一个工业级的自定义 Hook。这也是欧美高薪岗位最想看到的扎实基本功。

 

我的问题

step3我有问题:为什么需要使用abortcontroller来取消请求,直接写成这样return () => fetchData()不就行了吗?

幸好提前提出了问题,否则就会暴露我react项目没有写过多少的事实。这个return () => fetchData()看上去像是取消了请求,实际上是发起了请求。这根本就不是清理函数,只是看上去像而已。

那为什么abortcontroller可以取消请求呢?

useFetch 中,使用 AbortController 的核心作用只有三个:

  1. 防止数据错乱(解决竞态条件):当 url 频繁变化时,它会切断之前未完成的请求,确保只有最后一次请求的数据会被渲染,避免旧数据覆盖新数据。
  2. 避免白费力气(优化组件卸载):当用户离开页面或组件被销毁时,它能立即叫停进行中的请求,防止组件死后还在后台消耗网络和内存去解析数据。
  3. 真正切断传输(节省用户流量):它是浏览器底层的机制,调用后会直接关闭 HTTP 连接并停止接收数据包,而不仅仅是不去渲染数据。

 

追问

You have exactly the right mindset! In a real interview for a $5000/mo remote role, writing the initial code is only 30% of the battle. The remaining 70% is how you defend and adapt your code when the interviewer starts grillin' you with follow-up questions.

Let's stay on Question 1 (useFetch). You just submitted that beautiful version with AbortController. I am now stepping back into my role as your Senior Remote Interviewer.

 

Follow-Up Question 1: Object Reference Issue (Object Re-rendering)

Interviewer:

Your hook works perfectly fine, but I noticed something about your return statement. Every time useFetch re-runs, it returns a brand new object literal: return { data, loading, error };.

If a consumer component destructures this object inside a useEffect dependency array like this: useEffect(() => { ... }, [fetchResult]), it will trigger an infinite rendering loop because the object reference changes on every single render. How would you solve this object reference stability issue without forcing the user to memoize it on their end?

中文翻译提示:

“你的 Hook 跑得很好。但我注意到你的返回值:每次 useFetch 重新渲染,它都会返回一个全新的对象字面量 return { data, loading, error };。”

“如果用户在组件里把这个返回对象放进了另一个 useEffect 的依赖项里,因为每次引用地址都不同,就会导致死循环渲染。你该怎么在你的 Hook 内部解决这个返回值引用不稳定的问题,而不用让用户自己去特殊处理?”

 

Follow-Up Question 2: Caching & Global State

Interviewer:

Right now, your useFetch hook is purely local. If I have two different components on the screen rendering at the same time, and both of them call useFetch('/api/user'), it will fire two separate HTTP requests to the backend. Furthermore, if I navigate away and come back, it refetches and shows the loading spinner again.

Without installing React Query or Zustand, how can you add a simple In-Memory Cache Mechanism inside this file so that if a URL has already been fetched successfully, subsequent components can read the data instantly from memory instead of hitting the network?

中文翻译提示:

“目前你的 useFetch 是纯组件局部的。如果屏幕上同时渲染了两个不同的组件,它们都调用了 useFetch('/api/user'),这会向后端发出两次独立的 HTTP 请求。而且如果我切走页面再切回来,它又会重新请求并显示 loading。”

“在不安装 React Query 或 Zustand 的前提下,你如何在当前文件里加一个简单的内存缓存机制?这样只要某个 URL 曾经请求成功过,后续的组件就能立刻从内存里读数据,而不是去走网络请求?”

 

Now, it's your turn to defend your code!

Don't panic! You don't have to write perfect code right away. Try to answer one or both of these follow-up questions using the "Think Out Loud" approach we practiced.

How would you tackle these two scenarios? Reply with your ideas or code snippets (in Chinese or English), and let's see how we can level up your code to a Staff Engineer standard!

 

 

追问解答

No worries at all! This is exactly why we practice. These two follow-up questions are precisely how interviewers differentiate a Junior developer from a Senior/Lead developer.

Let’s break them down one by one using our Bilingual (中英双语) approach and see how simple the solutions actually are.

 

Follow-Up 1: Fixing the Object Reference Issue (解决返回值引用问题)

The Problem (问题所在):

Every time our hook runs, return { data, loading, error }; creates a brand new object in JavaScript memory. Even if data, loading, and error haven't changed, the container object has a new memory address. This breaks React's shallow comparison and can cause infinite rendering loops Interviewing.io Tips.

The Solution (解决方案):

We can use React's useMemo to cache the returned object. It will only create a new object reference when data, loading, or error actually change.

English Thought Process & Explanation:

To prevent unnecessary re-renders and stabilize the object reference, I will wrap the returned object inside a useMemo. This ensures that the consumer component receives the exact same object reference across renders, unless the inner state (data, loading, or error) changes.

中文思考与解释:

“为了防止不必要的二次渲染并稳定对象的内存引用,我会使用 useMemo 把要返回的对象包裹起来。这样能确保只要内部状态(dataloadingerror)没有改变,消费该 Hook 的组件在每次渲染时拿到的都是同一个对象引用。”

Code Adjustment (代码微调):

 

Follow-Up 2: Adding an In-Memory Cache (添加内存缓存机制)

The Problem (问题所在):

Currently, our hook holds its state inside the function component. When the component unmounts, the data is wiped out. If multiple components call the hook, they don't share data.

The Solution (解决方案):

We can declare a simple JavaScript Object (const cache = {}) outside the useFetch function block (at the file/module level). Because it's outside the hook, it acts as a global in-memory singleton cache that persists across all components and re-renders!

 

English Thought Process & Explanation:

To implement an in-memory cache without external libraries, I can declare a simple JavaScript object named cache outside the custom hook function. Before firing any network request, I will check if the url exists in this cache object. If it does, I'll instantly set the data from memory and skip the fetch call.

中文思考与解释:

“要在不依赖第三方库的前提下实现内存缓存,我可以在自定义 Hook 函数的外部声明一个普通的 JavaScript 对象 cache。在每次发起网络请求之前,先检查这个 cache 对象里是否已经有了对应的 url。如果有,就直接从内存中读取数据并更新状态,从而跳过实际的网络请求。”

Code Adjustment (代码微调):

 

Summary for Your Interview Arsenal (面试通关总结)

Look at what you just achieved! By adding just a few lines of code without AI:

  1. You solved Race Conditions using AbortController.
  2. You fixed Performance/Infinite Loop issues using useMemo.
  3. You built a Global Cache Mechanism using a basic JavaScript closure module pattern.

This is exactly what senior interviewers at top-tier startups look for. They don't care if you know every Next.js API by heart, but they care immensely about whether you understand how JavaScript memory, network requests, and React rendering interact.

 

 

 

继续追问

Yes, absolutely! An interviewer for a high-paying remote role ($5000+/mo) can push you even further into Staff/Principal Engineer territory.

Now that you have built a custom hook with AbortController, useMemo, and an In-Memory Cache, a top-tier interviewer will start challenging the architecture and production scaling of your solution.

Here are the next 3 brutal follow-up questions they would ask you on this exact same problem, along with how to handle them in 中英双语 (Bilingual).

 

Advanced Follow-Up 1: Cache Invalidation & Expiration (缓存过期与失效)

Interviewer:

Your module-level cache object works, but it lasts forever until the user refreshes the browser. If the server data changes frequently, the user will be stuck looking at stale data forever. Also, if the user visits 1,000 different URLs, your cache object will grow indefinitely, causing a memory leak.

How would you add a simple Time-To-Live (TTL) or expiration mechanism so that the cached data automatically invalidates after, say, 5 minutes?

中文翻译提示:

“你的全局 cache 对象确实管用,但除非用户刷新浏览器,否则这些数据永远不会消失。如果后端数据频繁更新,用户看到的就永远是旧数据。而且,如果用户访问了 1000 个不同的网址,你的 cache 对象会无限膨胀,导致内存泄漏。”

“你该怎么加一个简单的 TTL(生存时间) 缓存失效机制,让数据在存入 5 分钟后自动过期并强制重新请求?”

The Solution / 解决方案:

Instead of just storing the raw data (cache[url] = json), we should store an object containing both the data and a timestamp of when it was fetched (cache[url] = { data: json, timestamp: Date.now() }). Before reading from the cache, we check if the current time minus the timestamp exceeds 5 minutes.

 

Advanced Follow-Up 2: Request Deduplication / Network Race (请求去重/并发控制)

Interviewer:

Imagine a dashboard page where Component A and Component B are mounted at the exact same millisecond. Both of them call useFetch('/api/profile') simultaneously.

Since the first request hasn't finished yet, cache['/api/profile'] is still empty. Therefore, BOTH components will bypass the cache check and fire two parallel HTTP requests for the exact same data. How do you implement Request Deduplication so that if a request for a URL is already in-flight, the second component waits for the first request instead of firing a new one?

中文翻译提示:

“想象一个仪表盘页面,组件 A组件 B 在同一毫秒被渲染。它们同时调用了 useFetch('/api/profile')。”

“因为第一个请求此时还没结束,cache 里还是空的。所以这两个组件都会通过缓存检查,向后端发送两个并发的 HTTP 请求。你该如何实现请求去重?确保如果某个 URL 的请求已经在路上了,第二个组件会等待它的结果,而不是重复发请求?”

The Solution / 解决方案:

在工业级的真实生产环境(Production)中,我们要确保:

  1. 如果缓存已经有了且未过期 直接读缓存(TTL 守护)。
  2. 如果没有缓存或已过期,但此时已经有另外一个相同的请求正在路上飞 两个组件共享同一个 Promise,不重复发请求(并发去重守护)。

保留TTL 缓存,新增并发请求去重

English Architectural Design:

In a true production environment, we maintain two separate state tracks outside the hook: cache stores the finalized data with timestamps, and inflightRequests tracks active execution Promises. When a component mounts, we check the TTL first. If there's no cache or cache expired, we look at the promise tracker. If another component already initiated the fetch, we simply await that exact same in-flight Promise instead of spamming the backend.

中文架构设计:

“在真实的生产环境中,我们在 Hook 外部维护两个独立的追踪器:cache 负责存已经成功拿到并带有时间戳的数据,inflightRequests 负责存正在天空中飞的活跃 Promise。当组件加载时,我们先检查 TTL 缓存。如果没中或者过期了,我们再去看 Promise 追踪器。如果发现另一个组件已经率先发起了请求,我们就直接去 await 这个现成的 Promise,而不会去重复轰炸后端。”

其实代码还有点问题,不过不用管。

fetch(url, { signal }) 的多实例问题: 原代码在创建第一个共享 promise 时传入了 signal。如果第 1 个组件销毁了触发 abort(),那么后面正在等待同一个 promise 的第 2、3 个组件全部都会跟着一起被 abort 掉。

 

Advanced Follow-Up 3: Next.js Server-Side Rendering (SSR) Safety (服务端渲染安全性)

Interviewer:

Since you are applying for a Next.js Full-Stack role, remember that custom hooks run on the server during SSR before hitting the browser. Your code references browser-specific variables like AbortController and fetch (if on older Node environments), and it modifies a global cache object.

If 100 different users access this Next.js site simultaneously, they will all share the exact same const cache = {} on your Node.js server, leading to severe cross-user data leaks (User A seeing User B's private data). How do you make this hook safe for Next.js SSR?

中文翻译提示:

“既然你申请的是 Next.js 全栈岗位,别忘了自定义 Hook 在服务端渲染(SSR)阶段也会在服务器上执行。你的代码引用了浏览器专有的 AbortController,而且修改了全局 cache 对象。”

“如果 100 个不同的用户同时访问这个 Next.js 网站,他们在你的 Node.js 服务器上会共享同一个 cache 对象,导致严重的用户数据泄露(用户 A 看到了用户 B 的隐私数据)。你怎么让这个 Hook 在 Next.js SSR 环境下变得安全?”

这个题目是什么意思?

useFetch只能在client components里面使用啊,为什么要联系到ssr呢?

这里隐藏了 Next.js 的一个重要底层机制:在 App Router 架构下,Client Components 依然会在服务端执行 SSR(Server-Side Rendering,服务端渲染)。

你提出的是一个极其核心且完全合理的疑问。许多人第一次接触 Next.js 时都会产生这样的困惑:明明声明了 'use client' 变成了 Client Component,为什么还要说它会在服务端运行呢?

实际上,这里隐藏了 Next.js 的一个重要底层机制:在 App Router 架构下,Client Components 依然会在服务端执行 SSR(Server-Side Rendering,服务端渲染)。

以下为你进行中英双语的深度拆解,帮你彻底击碎这个盲区,并在面试中避开致命陷阱:


1. 为什么 Client Components 依然会联系到 SSR?

你可能以为 'use client' 的意思是“这个组件只在浏览器运行,跟服务器无关”。但这是一个非常常见的误区。


2. 这个自定义 Hook 在 SSR 阶段发生了什么?

既然 Client Component 会在服务器被执行一遍,那位于其中的 useFetch(url) 自然也无法逃脱。在服务器执行它时,生命周期是这样的:


3. 那为什么你的代码在刚才的例子里运行得好好的?

因为在 React 的设计中,useEffect 具有绝对的特权——它永远不会在服务端渲染(SSR)期间执行!它有且仅会在组件安全抵达浏览器、并完成 Hydration(水合)后,才在客户端被唤醒。

回头看一下我们写好的代码:我们的网络请求、AbortController 实例化、以及对 cacheinflightRequests 的读写,全部都被严密包裹在 useEffect 的闭包内部

那 useState 呢?服务端没有这个api啊?

React 框架本身是具备全栈同构能力的,useState 这个 API 在服务端(Node.js)完全存在,并且可以在服务端安全运行。

在服务器渲染(SSR)期间,React 遇到 Client Component 里的 useState 时,处理逻辑非常特殊。我们可以称之为 “只读单向执行(Read-Only Single Pass)”

The Solution / 解决方案:

English Explanation:

I fully understand the trade-offs. In Next.js, even Client Components execute on the Node.js server during the initial SSR phase. To ensure absolute cross-user data safety and avoid runtime environment crashes, I engineered this hook with two deliberate structural defenses:

  1. Isolation inside useEffect: All browser-specific APIs (like AbortController) and write mutations to our memory maps are confined strictly inside useEffect. Because useEffect never triggers on the server, the Node.js backend remains untouched and error-free.
  2. Universal SSR State Guard: For the initial state factory inside useState, I will wrap the cache retrieval inside an environment guard clause: if (typeof window !== 'undefined'). This guarantees the global cache is strictly read and populated on the client's local sandbox browser, cutting off any possibility of global Node.js cross-user data leaks.

中文回答:

“我完全理解这个全栈架构的权衡。在 Next.js 中,即使是 Client Component 在首屏加载的 SSR 阶段也会在 Node.js 服务器上执行。为了确保绝对的跨用户数据安全,并避免服务器运行期环境崩溃,我在设计这个 Hook 时做了两层极其严密的结构化防御:”

“1. 将副作用完全隔离在 useEffect 内部:所有诸如 AbortController 这样的浏览器专有 API,以及对全局缓存和飞行锁的改写,都被死死地锁在 useEffect 里。因为 useEffect 在服务端绝对不会执行,这就保证了 Node.js 后端在编译期间绝不会报错崩溃。” “2. useState 初始化加入环境守护:针对 useState 内部的那段初次挂载缓存读取,我会额外加上一行环境检查:if (typeof window !== 'undefined')。这能确保这个全局 cache 记录仅在用户的浏览器本地沙盒内被读取和写入,从而彻底斩断在 Node.js 服务器层级发生跨用户数据泄露(Cross-user leaks)的任何理论可能。”

解决办法

加上 SSR 环境安全锁的代码:

What this means for you:

As you can see, a simple 10-line useFetch hook can explode into a deep architectural debate about memory, networks, concurrency, and backend server environments. This is why you don't need to memorize 100 interview questions. You just need to know how to deeply dissect a few.

加上ts类型的代码:

 

继续追问

an elite principal or staff-level interviewer can push this single useFetch hook to its absolute architectural limit.

At this stage, they are no longer testing if you know React or Next.js. They are testing if you understand Browsers, Operating Systems, Memory Management, and Network Protocols.

Here are the final 2 "boss-level" follow-up questions an interviewer could possibly ask on this topic, explained in 中英双语 (Bilingual):

Boss-Level Follow-Up 1: The "10,000 Components" Memory Pressure (内存暴涨与垃圾回收)

Interviewer:

Let's look at your global cache object again. Suppose this is a large enterprise application. Over a 5-hour user session, the user navigates through thousands of pages, loading megabytes of heavy JSON data into your cache object.

Even if you implement a TTL (expiration time), the JavaScript Garbage Collector (GC) cannot clear those expired objects from memory because your global cache object still holds a strong reference to their keys and values. How do you prevent this from causing a browser tab crash due to out-of-memory, without manually writing a complex cache-clearing loop?

中文翻译提示:

“再来看看你的全局 cache 对象。假设这是一个大型企业级应用。在用户长达 5 小时的使用过程中,用户浏览了数千个页面,将数 MB 的沉重 JSON 数据加载到了你的 cache 对象中。”

“哪怕你实现了 TTL(过期时间),JavaScript 的垃圾回收机制(GC)也无法从内存中清理这些过期对象,因为你的全局 cache 对象仍然牢牢持有它们的引用。你怎么防止这导致浏览器标签页因内存溢出而崩溃,同时又不用自己手动写复杂的定时清理循环?”

分析

虽然设置了TTL,但如果用户访问了很多个页面,这些页面有很多不同的接口,大部分都是访问一次之后就不再访问了,那么这些接口都会保存在cache对象中,并不会清理。之前的代码里面,只是对过期的接口进行了更新,里面残留的数据是不会进行清理的。

The Solution / 解决方案:

方案一:引入 WeakRef 与 FinalizationRegistry(现代浏览器原生垃圾回收联动)- 满分推荐

这是最契合面试官要求的黑科技解法。利用现代 JavaScript 原生的 WeakRef(弱引用)FinalizationRegistry(清理注册表),我们可以直接让浏览器的垃圾回收器来帮我们做免费的清洁工。

原理: WeakRef 允许我们引用一个对象,但不阻止它被垃圾回收。当浏览器内存紧张、或者该组件销毁后(组件销毁后就清除这个对象,没有问题,虽然说感觉cache白做了,但是因为TTL就5分钟,如果在5分钟之内就将这个组件销毁了,说明这个组件也没有那么重要),GC 会自发把这个对象从内存中抹去。而 FinalizationRegistry 则能在 GC 把对象回收的瞬间,触发一个回调,把无用的空 Key 从我们的 cache 字典里彻底剔除。

我有一个疑问:

使用了weakRef之后,虽然解决了cache导致的内存暴涨的问题,但是weakRef在组件销毁之后,会清除里面的url请求的缓存。

我想问一下,那岂不是Cache白做了,如果连5分钟都没到就清除了,cache是不是白做了?


简单直接地回答你的疑问:不,Cache 并没有白做,但它的工作原理确实和你想象的传统缓存不太一样。

你之所以觉得它“白做了”,是因为这里有一个关于垃圾回收(GC)和 WeakRef 的常见误解:组件销毁(Unmount)不等于数据立刻被垃圾回收。

我们可以从以下几个层面来理清这背后的逻辑:

1. 关键误解:组件销毁 垃圾回收

当 React 组件销毁时,组件内部的 useState 状态确实解绑了。但这并不意味着 fetch 回来的 json 对象在内存中立刻消失了。

垃圾回收器(GC)是定期、自发运行的。

2. WeakRef 缓存的真正定位:内存敏感型二级缓存

这种设计并不是为了替代常规的“强引用定时缓存”,它解决的是特定场景的痛点:

为什么不用传统的强引用缓存?

如果用传统缓存(普通对象或 Map),只要不手动删除,数据就永远死死占着内存。如果用户疯狂切换路由,加载了几百兆的数据,内存就会暴涨,导致低端设备甚至高端浏览器卡死崩溃。

WeakRef 缓存的潜台词是:

“只要这个数据还在浏览器的内存里(还没来得及被回收,或者别人还在用),那接下来的 5 分钟内,谁要用,我就直接给谁,免去网络请求。

但如果浏览器内存紧张,或者没人用了,被 GC 回收了,那丢了就丢了吧,大不了重新发一次请求。”

它是一种“尽力而为(Best-Effort)”的缓存机制,在内存安全缓存命中率之间取得了一个完美的平衡。

总结

你贴出的这段代码,其精妙之处在于“白嫖”浏览器的剩余内存。它没有白做,它让那些“刚销毁不久、还热乎着、没被 GC 清理”的数据得以重复利用。

如果你对数据的实时性和确定性要求极高(比如股票价格、聊天消息),WeakRef 确实不适合;但如果是针对大体积、频繁切换查看、且不常变动的数据(比如文章详情、图片列表),这种缓存设计既优雅又安全。

方案二:手写一个固定容量的 LRU(Least Recently Used)淘汰算法(算法大局观)

如果面试官追问:“如果用户内存很大,GC 一直不触发,那 cache 里的 Key 不还是会缓慢增长吗?我们能不能主动控制上限?”

这时你要抛出经典的数据结构:LRU(最近最少使用)缓存算法。我们严格锁死缓存的最大容量(比如最多只存 100 条数据)。当第 101 条数据进来时,算法会自动把最古老、最没人访问的那条数据物理抹除。这样,内存曲线在长达 5 小时的会话里会是一条完美的水平直线

利用 JavaScript Map 的有序特性:

答案:

To prevent memory bloated crashes in a big application without writing messy setInterval loops, we must transition from a naive javascript object to a memory-aware data structure. I would propose two battle-tested production strategies:

  1. The Native Reactive Garbage Collector Approach (WeakRef + FinalizationRegistry): Instead of storing strong references to the heavy JSON payloads, we wrap them inside a browser-native WeakRef. This tells the JavaScript engine that it is fully permitted to garbage collect these expired or unmounted JSON objects whenever memory pressure spikes. Simultaneously, I will hook them into a FinalizationRegistry. When the GC reclaims the memory, it fires a synchronous lifecycle hook to clean the leftover string keys from our map, maintaining a self-healing memory tree.
  2. The Hard-Bounded LRU Cache Pattern: To strictly enforce a flat memory ceiling, we can wrap our external tracking layer inside an LRU (Least Recently Used) cache backed by a native JavaScript Map. By locking the capacity to say, 100 entries, the exact millisecond the 101st request arrives, the algorithm automatically evicts(逐出、驱出) the oldest unvisited network sector from memory. This guarantees that whether the user clicks through 10 pages or 10,000 pages over a 5-hour session, the memory footprint remains absolutely rock-solid and predictable.

 

Boss-Level Follow-Up 2: React 19 Architecture Alignment (React 19 异步流控制)

Interviewer:

Since we are coding in the modern React era (React 19 / Next.js App Router), the React team is actively moving away from triggering data fetching inside useEffect because of all the boilerplate we just wrote (cleaning up, caching, handling race conditions).

React 19 introduced the new use hook which allows you to consume Promises directly in the render phase. If I ask you to completely delete useEffect from this file, how would you refactor this hook to leverage React 19's native Suspense architecture and the use(promise) API?

中文翻译提示:

“既然我们处于现代 React 时代(React 19 / Next.js App Router),React 团队正在极力让大家不要useEffect 里面写数据请求,因为这需要写太多死板的模板代码(清理、缓存、处理竞态条件)。”

“React 19 引入了全新的 use Hook,允许你在渲染阶段直接消费 Promise。如果我要求你把这个文件里的 useEffect 完全删掉,你该如何重构这个 Hook,从而利用 React 19 原生的 Suspense 架构和 use(promise) API 呢?”

分析

在 React 19 之前,所有的 Hook(如 useState, useEffect)都绝对不允许写在条件语句(if)或者循环里。

但 React 19 引入的 use() 是一个特例,它可以在 if 语句和渲染阶段直接使用。

使用方法:

我们不需要在组件内管理 loadingerror 的状态了。

The Solution / 解决方案:

In React 19, instead of tracking state (data, loading, error) locally, we cache the Promise itself at the module level. Then, inside the hook, we simply return use(cachedPromise). React's <Suspense> boundary outside the component will automatically handle the loading state, and the <ErrorBoundary> will automatically catch the error. The hook shrinks from 50 lines down to 5 lines!

 

你不需要去写任何取消请求、重置 loading、或者捕获 error 的代码。因为使用了use之后, Loading 状态由 <Suspense> 接管,Error 状态由 <ErrorBoundary> 接管

 

The Ultimate Takeaway from Question 1

We have now officially exhausted this question. You just saw how a single coding prompt can scale from a junior-level implementation up to a staff-level architectural deep-dive into browser memory and React 19 core internals.

This is the secret of international remote interviews: They don't expect you to know everything, but they want to see how far your technical curiosity goes when pushed into a corner.

You did an amazing job holding your ground and walking through this with me.

We can now officially lock Question 1 away as "Mastered."