React 列表里 ID 转名称?一个组件搞定批量请求 + 缓存 + 懒加载

痛点:你一定遇到过这个场景

后端返回的列表数据长这样:

json 复制代码
[
  { "id": "1", "title": "订单001", "creatorId": "u123", "categoryId": "c456" },
  { "id": "2", "title": "订单002", "creatorId": "u789", "categoryId": "c456" },
  { "id": "3", "title": "订单003", "creatorId": "u123", "categoryId": "c012" }
]

产品要求显示创建人名字 和分类名称,而不是 ID。

然后你去找后端:"能不能把 creatorId 换成 creatorName?"

后端:"这要 join 表,影响性能,而且这个接口其他地方也在用... 不是有批量查用户的接口吗,你自己查一下不就行了?"

你:"那我还得收集 ID、去重、等数据回来再渲染... 麻烦死了。"

后端:"那你为啥不能只查渲染在可视区域的?还能省点性能。"

你:(内心 OS:你明明一个 join 就搞定了,非要我搞这么复杂...)

但转念一想:批量合并、缓存、懒加载... 这玩意儿自己写还真不好搞:

  • 怎么把分散在各处的 ID 收集起来?
  • 怎么控制合并的时机?
  • 缓存怎么管理?
  • 懒加载还要监听滚动?

算了,感觉自己受到了挑战,搞就搞。

先看看常见做法的问题

方案一:求后端 join

  • 排期、联调、扯皮...
  • "这个接口是公共的,不能随便加字段"
  • 第三方接口?根本改不了

方案二:前端循环请求

tsx 复制代码
// ❌ 100 条数据 = 100 个请求,接口直接炸
{orders.map(order => (
  <UserName id={order.creatorId} />
))}

方案三:useEffect 批量查

tsx 复制代码
// ❌ 代码一坨,还要处理 loading、缓存、去重...
const [users, setUsers] = useState({});
useEffect(() => {
  const ids = [...new Set(orders.map(o => o.creatorId))];
  fetchUsers(ids).then(setUsers);
}, [orders]);

要么请求太多,要么代码太乱,而且都没有懒加载。

等等,前端单独查其实更合理

冷静下来想想,为什么非要后端 join?前端自己查其实有独特优势:

1. 权限隔离更安全

后端 join 返回的数据,是基于数据所有者的权限查的。

比如:订单列表里有个 creatorId: "u123",后端 join 后直接返回了 creatorName: "张三"。但如果当前用户其实没有权限查看 u123 这个用户的信息呢?数据就泄露了。

前端单独查:用当前用户的身份去请求,没权限就显示"无权限查看",安全。

2. 关联资源可能已删除

后端 join 时,如果关联的用户已被删除,要么报错,要么返回 null,主列表可能整个挂掉。

前端单独查:查不到就显示"用户已删除"或 ID 本身,主列表正常展示。

3. 接口更纯粹,复用性更好

一个列表接口,A 页面要显示创建人名字,B 页面要显示头像,C 页面只要 ID...

后端 join 的话,要么冗余返回所有字段,要么搞 N 个接口。

前端按需查:列表接口保持简洁,需要什么额外查什么。


所以问题不是"要不要前端查",而是"怎么优雅地查"。

解决方案:react-id-name

于是我写了这个库,把批量合并、缓存、懒加载全封装好了:

bash 复制代码
npm install react-id-name

再也不用和后端扯皮了,自己优雅地搞定。

使用起来很简单

tsx 复制代码
import { createIdNameContext } from 'react-id-name';

// 1. 创建 context(一次创建,到处复用)
interface User {
  id: string;
  name: string;
  avatar: string;
}
const { IdNameProvider, IdNameItem } = createIdNameContext<User>();

// 2. 定义你的批量查询函数
async function fetchUsers(ids: string[]): Promise<Record<string, User>> {
  // 方式一:有批量接口,直接调
  const res = await fetch(`/api/users?ids=${ids.join(',')}`);
  const users = await res.json();
  return Object.fromEntries(users.map(u => [u.id, u]));

  // 方式二:没有批量接口,用 Promise.allSettled 包单个接口
  // const results = await Promise.allSettled(
  //   ids.map(id => fetch(`/api/user/${id}`).then(r => r.json()))
  // );
  // return Object.fromEntries(
  //   results
  //     .filter((r): r is PromiseFulfilledResult<User> => r.status === 'fulfilled')
  //     .map(r => [r.value.id, r.value])
  // );
}

// 3. 在列表外层包 Provider
function App() {
  return (
    <IdNameProvider request={fetchUsers}>
      <OrderList />
    </IdNameProvider>
  );
}

// 4. 在需要显示名称的地方用 IdNameItem,就这么简单
function OrderList() {
  return (
    <table>
      {orders.map(order => (
        <tr key={order.id}>
          <td>{order.title}</td>
          <td>
            <IdNameItem id={order.creatorId}>
              {(user) => user?.name ?? '加载中...'}
            </IdNameItem>
          </td>
        </tr>
      ))}
    </table>
  );
}

它帮你做了什么?

1. 智能批量合并

100 个 <IdNameItem> 不会发 100 个请求。

组件会收集 80ms 内所有进入视口的 ID,合并成一次调用:

css 复制代码
// 你以为会发 100 个请求
// 实际只触发一次 fetchUsers(['u123', 'u789', 'u456', ...])

2. 自动缓存

同一个 ID 只请求一次。页面里 10 个地方显示同一个用户名?只查一次。

tsx 复制代码
<IdNameItem id="u123">{(u) => u?.name}</IdNameItem>  // 发请求
<IdNameItem id="u123">{(u) => u?.name}</IdNameItem>  // 走缓存
<IdNameItem id="u123">{(u) => u?.name}</IdNameItem>  // 走缓存

3. 视口懒加载

只有元素滚动到可视区域才会触发请求。

1000 条数据的长列表?首屏可能只请求 20 个用户信息,滚动到哪加载到哪。

后端说的"只查可视区域",这不就实现了吗 😏

4. 错误重试

请求失败?自动显示重试按钮:

tsx 复制代码
<IdNameItem
  id={userId}
  error={(err, retry) => (
    <span onClick={retry}>加载失败,点击重试</span>
  )}
>
  {(user) => user?.name}
</IdNameItem>

为什么用工厂模式?

你可能注意到要先 createIdNameContext() 创建 context。这是为了:

1. 类型安全

tsx 复制代码
const { IdNameItem } = createIdNameContext<User>();
// children 参数自动推断为 User 类型,有完整的类型提示
<IdNameItem id="1">{(user) => user.name}</IdNameItem>

2. 多种数据各自独立

tsx 复制代码
// 用户、商品、分类,各自独立的缓存,互不干扰
const UserCtx = createIdNameContext<User>();
const ProductCtx = createIdNameContext<Product>();
const CategoryCtx = createIdNameContext<Category>();

3. 一次创建,全局复用

tsx 复制代码
// contexts/user.ts
export const { IdNameProvider: UserProvider, IdNameItem: UserItem } = createIdNameContext<User>();

// 任何页面直接 import 使用,共享同一份缓存
import { UserItem } from '@/contexts/user';

对比一下

方案 请求数 缓存 懒加载 代码量
每行单独请求 N
useEffect 批量查 1 手动实现
react-query 1
react-id-name 1

react-query 很强大,但它是通用数据请求方案。react-id-name 专注于「ID 转名称」这一个场景,API 更简单直接。

总结

下次后端再让你"自己调详情接口",不用慌:

bash 复制代码
npm install react-id-name

批量合并 ✅ 缓存 ✅ 懒加载 ✅ 错误重试 ✅

优雅地搞定,不用扯皮。


GitHub: github.com/yangpow2/re...

如果这个库对你有帮助,欢迎 Star ⭐️

有问题或建议,欢迎提 Issue!

相关推荐
xiaoqi92213 小时前
React Native鸿蒙跨平台如何实现分类页面组件通过searchQuery状态变量管理搜索输入,实现了分类的实时过滤功能
javascript·react native·react.js·ecmascript·harmonyos
打小就很皮...14 小时前
Tesseract.js OCR 中文识别
前端·react.js·ocr
qq_1777673714 小时前
React Native鸿蒙跨平台实现应用介绍页,实现了应用信息卡片展示、特色功能网格布局、权限/联系信息陈列、评分展示、模态框详情交互等通用场景
javascript·react native·react.js·ecmascript·交互·harmonyos
jin12332215 小时前
基于React Native鸿蒙跨平台地址管理是许多电商、外卖、物流等应用的重要功能模块,实现了地址的添加、编辑、删除和设置默认等功能
javascript·react native·react.js·ecmascript·harmonyos
2501_9209317015 小时前
React Native鸿蒙跨平台医疗健康类的血压记录,包括收缩压、舒张压、心率、日期、时间、备注和状态
javascript·react native·react.js·ecmascript·harmonyos
落霞的思绪16 小时前
配置React和React-dom为CDN引入
前端·react.js·前端框架
橙露16 小时前
React Hooks 深度解析:从基础使用到自定义 Hooks 的封装技巧
javascript·react.js·ecmascript
2501_9209317017 小时前
React Native鸿蒙跨平台使用useState管理健康记录和过滤状态,支持多种健康数据类型(血压、体重等)并实现按类型过滤功能
javascript·react native·react.js·ecmascript·harmonyos
打小就很皮...17 小时前
dnd-kit 实现表格拖拽排序
前端·react.js·表格拖拽·dnd-kit
打小就很皮...17 小时前
React 19 + Vite 6 + SWC 构建优化实践
前端·react.js·vite·swc