我厌倦了 useQuery/Promise.all 的 Spaghetti,于是我构建了这个 🫖🦡

发布: (2026年2月22日 GMT+8 09:20)
6 分钟阅读
原文: Dev.to

Source: Dev.to

我使用 React。我不喜欢 React。我被它束缚住了。

我的后端是一堆热闹的微服务。很多很多。到处都是引用数据——工单、用户、团队、角色、观察者、负责人、权限。随着产品规模的扩大,前端变成了一个请求中继:

  • 获取工单。
  • 获取受让人。
  • 获取受让人的团队。
  • 获取团队负责人。
  • 获取观察者。
  • 获取角色。

每个字段都是一个 ID,每个 ID 都要再来一次往返。有时同一个用户 ID 在树中出现三次,导致三次请求。幸好有 TanStack Query,否则我早就崩溃了。

在更理想的世界里,我们会有 Redis、GraphQL,或者一个统一的 API。相反,我们只有没有共同基础的妖怪微服务。还有 Elasticsearch,但如果用错了会让人抓狂。所以我们只能这样。

我厌倦了,于是我做了一个东西。

任务

你的 REST API 返回 ID。你的 UI 需要对象。所以你编写了解析代码。

一开始很简单:

const ticket = await fetchTicket('t-1');
const assignee = await fetchUser(ticket.assigneeId);

随后代码膨胀。Ticket 现在有了 watcher。Assignee 有了 team。Team 又有了 lead。最终你得到一堆 Promise.alluseQueries、空值检查以及单独的 fetch。没有批量请求。没有去重。类型一团糟。每新增一个字段就要增加样板代码和额外的渲染。

const assignee = ticket.assigneeId ? await fetchUser(ticket.assigneeId) : null;
const watchers = await Promise.all(
  (ticket.watcherIds ?? []).map(id => (id ? fetchUser(id) : null))
);
const team = assignee?.teamId ? await fetchTeam(assignee.teamId) : null;
const lead = team?.leadUserId ? await fetchUser(team.leadUserId) : null;
// … 你明白我的意思了

页面最终会出现数十个引用字段,代码变得让人疲惫不堪。

我制作的东西

@nimir/references – 一个类型安全的嵌套引用解析器。你只需一次性定义 sources(批量获取的方式),随后声明哪些字段是 ID。库会自动处理批处理、去重、缓存以及嵌套遍历(最多 10 层)。它对 null 安全并且完整提供类型。

定义 sources

import { defineReferences } from '@nimir/references';

const refs = defineReferences(c => ({
  User: c.source({ batch: ids => fetchUsers(ids) }),
  Team: c.source({ batch: ids => fetchTeams(ids) }),
  Role: c.source({ batch: ids => fetchRoles(ids) }),
}));

声明引用映射

const result = await refs.inline(ticket, {
  fields: {
    assigneeId: {
      source: 'User',
      fields: {
        teamId: {
          source: 'Team',
          fields: { leadUserId: 'User' },
        },
        roleIds: 'Role',
      },
    },
    watcherIds: 'User',
  },
});

所有 User 的获取(受理人、观察者、团队负责人)都会被合并为一次调用。重复的 ID 只会获取一次。解析后的值会附加 T 后缀(assigneeIdTwatcherIdTs …),TypeScript 会自动推断它们的类型。

React + TanStack Query

Since you’re stuck with React, there’s a React entry point.

import { defineReferences } from '@nimir/references/react';

const useTicket = refs.hook(useGetTicket, {
  fields: { assigneeId: 'User', watcherIds: 'User' },
});

function TicketCard({ id }: { id: string }) {
  const { result, status, error, invalidate } = useTicket(id);
  // result.assigneeIdT → User | null
}

或者以响应式方式解析内联数据:

const resolved = refs.use(data, { fields: { assigneeId: 'User' } });

支持 TanStack Query、SWR,或任何返回数据的 hook。

缓存

Sources 支持可插拔缓存:内存缓存、IndexedDB(通过 idb-keyval)或 Redis。您可以配置 TTL、负向缓存和缓存预热。如果后端已有 Redis,直接接入即可;否则,使用内存缓存或 IndexedDB 仍能显著减少重复获取。

注意事项

  • 深度限制为 10 层(防止循环配置导致无限循环)。
  • 未知的来源名称会被静默跳过——fields 中的拼写错误会导致没有数据。TypeScript 有帮助,但运行时不会警告。

可能是我的单一使用案例

我为自己的混乱构建了它:REST 微服务,ID 随处可见,没有 GraphQL,也没有统一的后端。如果你正在处理遗留 API、第三方服务或混合数据源,这可能会有所帮助。如果你能够控制 API 并使用 GraphQL,请改用 GraphQL。

另说

构建过程非常有趣。我在工作时开始了它,随后它演变成了这个开源包。特别感谢我的女朋友,虽然她不懂这东西,但在精神上一直支持我。干杯!

GitHub · Docs · npm

0 浏览
Back to Blog

相关文章

阅读更多 »