痛点:你一定遇到过这个场景
后端返回的列表数据长这样:
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!