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!

相关推荐
xhxxx9 小时前
Vite + React 黄金组合:打造秒开、可维护、高性能的现代前端工程
前端·react.js·vite
用户81686947472510 小时前
深入 useState、useEffect 的底层实现
前端·react.js
Tzarevich10 小时前
React 中的 JSX 与组件化开发:以函数为单位构建现代前端应用
前端·react.js·面试
别急国王10 小时前
React Hooks 为什么不能写在判断里
react.js
Mintopia10 小时前
⚛️ React 17 vs React 18:Lanes 是同一个模型,但跑法不一样
前端·react.js·架构
玉木成琳10 小时前
Taro + React + @nutui/nutui-react-taro 时间选择器重写
前端·react.js·taro
2401_8604947010 小时前
在React Native中实现鸿蒙跨平台开发中开发一个运动类型管理系统,使用React Navigation设置应用的导航结构,创建一个堆栈导航器
react native·react.js·harmonyos
2301_7965125210 小时前
使用状态管理、持久化存储或者利用现有的库来辅助React Native鸿蒙跨平台开发开发一个允许用户撤销删除的操作
javascript·react native·react.js