A DataLoader batches the individual load(key) calls made during one tick of the event loop into a single call to a batch function, then caches each key's result so repeat loads are free. It is the standard fix for the N+1 problem: instead of firing one request per key as the keys arrive, you collect a tick's worth of keys and fetch them all at once. This is the pattern popularised by Facebook's DataLoader, the backbone of most GraphQL servers.
Implement dataloader(batchFn). It returns a loader with load, loadMany, clear, and clearAll. Every load(key) returns a promise; all load calls made in the same synchronous tick are gathered and dispatched together as one batchFn(keys) on the next microtask. batchFn returns a promise of an array of values, positionally matched to the keys it was given.
function dataloader<K, V>(
batchFn: (keys: K[]) => Promise<Array<V | Error>>
): DataLoader<K, V>;
interface DataLoader<K, V> {
load(key: K): Promise<V>; // joins the current tick's batch, or the cache
loadMany(keys: K[]): Promise<V[]>; // Promise.all of loads, values in order
clear(key: K): void; // evict one key from the cache
clearAll(): void; // evict every key
}
const users = dataloader((keys) => {
// Called ONCE with ['1', '2', '3'] — not three times.
return Promise.resolve(keys.map((id) => ({ id, name: 'User ' + id })));
});
// Three loads in the same tick collapse into a single batchFn call.
const [a, b, c] = await Promise.all([
users.load('1'),
users.load('2'),
users.load('3'),
]);
a.name; // 'User 1' (the value at index 0 of the batch)
c.name; // 'User 3' (the value at index 2)
const users = dataloader((keys) => Promise.resolve(keys.map(fetchUser)));
users.load('1');
users.load('1'); // same tick, same key → batchFn sees ['1'] once; both share it
await users.load('1'); // resolved and now cached
users.load('1'); // a later tick → returns the cached promise, no new batch
users.clear('1'); // evict → the next load('1') batches afresh
load called before the next microtask is collected; batchFn runs once with all the keys, in first-requested order. Use queueMicrotask (or Promise.resolve().then), not setTimeout.batchFn(keys) must resolve to an array where values[i] is the answer for keys[i], with the same length and order.values[i] is an Error instance, only that key's load rejects; the rest still resolve. If batchFn itself rejects, every load in that batch rejects.keys; both callers share the one result.load and reused on later ticks until you clear(key) or clearAll().Unlock the solution & editor
Runnable editor + tests
Solve in the browser with instant Jest feedback.
Detailed solutions
Walkthroughs, edge cases, and complexity notes.
Multi-framework variants
React, Vue, Vanilla, Angular — same question, different stacks.