Batch and Coalesce Fetch Calls in React

Twenty cards on a page each ask for /api/users/:id and you cut server load 20x with this 30-line dataloader. The batch fires on the next microtask; same id never goes over the wire twice.

JavaScript
Frontend
3 snippets
batched-dispatch
react
performance
sophiesharma

By @sophiesharma

March 25, 2026

·

Updated May 18, 2026

813 views

22

4.4 (10)

// Inspired by Facebook's DataLoader. The pattern: every load(id) call enqueues
// the id and returns a promise; on the next microtask, all queued ids are
// flushed in a single batchFn call and each promise resolves with its share
// of the result. Same id requested twice resolves twice from one fetch.

function createLoader(batchFn) {
    let pending = [];   // { id, resolve, reject }
    let scheduled = false;

    async function flush() {
        const batch = pending;
        pending = [];
        scheduled = false;
        try {
            const ids = [...new Set(batch.map((b) => b.id))];
            const result = await batchFn(ids);  // expected: { id: data } map
            for (const { id, resolve, reject } of batch) {
                if (id in result) resolve(result[id]);
                else reject(new Error(`loader: missing id ${id}`));
            }
        } catch (err) {
            for (const { reject } of batch) reject(err);
        }
    }

    return function load(id) {
        return new Promise((resolve, reject) => {
            pending.push({ id, resolve, reject });
            if (!scheduled) {
                scheduled = true;
                queueMicrotask(flush);
            }
        });
    };
}

// Pretend this is a /api/users?ids=1,2,3 endpoint.
let apiCalls = 0;
async function batchFetchUsers(ids) {
    apiCalls++;
    return Object.fromEntries(ids.map((id) => [id, { id, name: `user_${id}` }]));
}

const loadUser = createLoader(batchFetchUsers);

// 20 components each call loadUser. Most ids are duplicates.
const requests = [1, 2, 1, 3, 2, 4, 1, 5, 3, 2].map((id) => loadUser(id));
Promise.all(requests).then((results) => {
    console.log('completed:', results.length, ' api calls:', apiCalls);
    console.log('first:', results[0]);
});

Three moving parts: a pending queue, a one-shot scheduled flag, and queueMicrotask to fire the flush at the end of the current synchronous burst. The microtask boundary is the right granularity for React: between the time the first component's effect runs and the next paint, every component on the page has had a chance to enqueue its id. The new Set dedupe before the batch fetch is the second win after batching itself; the same user id requested by 5 cards still hits the API once. I have shipped this exact pattern to coalesce avatar lookups across a discussion thread; it cut requests by 12x without any component changes.