I Got Tired of useQuery/Promise.all Spaghetti So I Built This 🫖🦡
Source: Dev.to
I use React. I don’t like React. I’m stuck with it.
My backend is a hot bunch of micro‑services. Lots of them. Referential data everywhere — tickets, users, teams, roles, watchers, leads, permissions. As the product scales, the frontend turns into a request relay:
- Fetch a ticket.
- Fetch the assignee.
- Fetch the assignee’s team.
- Fetch the team lead.
- Fetch the watchers.
- Fetch the roles.
Each field is an ID, each ID is another round‑trip. Sometimes the same user ID shows up three times in the tree, resulting in three fetches. Thank God for TanStack Query, or I would have lost my mind years ago.
In a saner world we’d have Redis, GraphQL, or a single coherent API. Instead we have goblin micro‑services with no common ground. There’s Elasticsearch, but it fumes when you look at it wrong. So here we are.
I got tired, so I built a thing.
The Chore
Your REST API returns IDs. Your UI needs objects. So you write resolution code.
It starts simple:
const ticket = await fetchTicket('t-1');
const assignee = await fetchUser(ticket.assigneeId);
Then it grows. The ticket now has watchers. The assignee has a team. The team has a lead. You end up with a blob of Promise.all, useQueries, null checks, and individual fetches. Nothing is batched. No deduplication. Types are a mess. Every new field adds boilerplate and extra renders.
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;
// … you get the idea
Pages end up with dozens of reference fields, and the code becomes exhausting.
The Thing I Made
@nimir/references – a type‑safe nested reference resolver. You define sources (how to fetch batches) once, then declare which fields are IDs. The library handles batching, deduplication, caching, and nested traversal (up to 10 levels). It’s null‑safe and fully typed.
Define 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) }),
}));
Declare a reference map
const result = await refs.inline(ticket, {
fields: {
assigneeId: {
source: 'User',
fields: {
teamId: {
source: 'Team',
fields: { leadUserId: 'User' },
},
roleIds: 'Role',
},
},
watcherIds: 'User',
},
});
All User fetches (assignee, watchers, team lead) are batched into a single call. Duplicate IDs are fetched once. Resolved values are attached with a T suffix (assigneeIdT, watcherIdTs, …) and TypeScript infers their types automatically.
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
}
Or resolve inline data reactively:
const resolved = refs.use(data, { fields: { assigneeId: 'User' } });
Works with TanStack Query, SWR, or any hook that returns data.
Caching
Sources support pluggable caches: in‑memory, IndexedDB (via idb-keyval), or Redis. You can configure TTL, negative caching, and cache warming. If you have Redis on the backend, plug it in; otherwise, in‑memory or IndexedDB still cuts down repeat fetches.
Caveats
- Depth limit of 10 levels (prevents infinite loops on circular configs).
- Unknown source names are silently skipped – a typo in
fieldsyields no data. TypeScript helps, but runtime won’t warn.
Probably My Single Use‑Case
I built this for my own mess: REST micro‑services, IDs everywhere, no GraphQL, no unified backend. If you’re dealing with legacy APIs, third‑party services, or mixed data sources, this might help. If you control the API and can use GraphQL, do that instead.
On the Other Note
It was very fun to build. I started it at work, then it morphed into this open‑source package. Shout‑out to my girlfriend who doesn’t understand this but morally supported me. Cheers!