Aller au contenu principal

defer()

defer<T>(fn): Promise<T>

Defers execution of a function to the next tick of the event loop.

remarque

Uses setTimeout(0) to yield to the event loop.


Type Parameters

T: T

The return type of the function.


Parameters

fn: () => T | Promise<T>

The function to defer.


Returns: Promise<T>

Promise that resolves to the function result.


Throws

Rejects if the deferred function throws or rejects.


Since

1.1.0


Also known as

defer (Lodash, es-toolkit, Radashi) · ❌ (Remeda, Ramda, Effect, Modern Dash, Antfu)


Example

await defer(() => updateDOM());

const result = await defer(async () => fetchData());

How it works?

Defers function execution to the next tick of the event loop. Uses setTimeout(0) to yield control back to the browser/runtime before executing.

Practical Flow


Use Cases

Yield to the browser between heavy operations 📌

Allow the browser to repaint and handle user input between CPU-intensive tasks. Essential for keeping UI responsive during long computations.

const processInChunks = async (items: Item[]) => {
const results: Result[] = [];

for (const chunk of chunks(items, 100)) {
// Process chunk
results.push(...chunk.map(heavyTransform));

// Yield to browser - allows repaint & input handling
await defer(() => {});
}

return results;
};

// UI stays responsive even with 10,000 items
await processInChunks(largeDataset);

Ensure state updates propagate before next operation

Wait for React/Vue state updates to flush before reading DOM. Critical for accurate measurements after state changes.

const measureAfterUpdate = async () => {
setItems([...items, newItem]); // React state update

// Wait for React to flush and browser to repaint
await defer(() => {});

// Now DOM reflects the new state
const height = containerRef.current.scrollHeight;
containerRef.current.scrollTop = height;
};

Debounce rapid sequential calls naturally

Create a simple "next tick" debounce without timers. Useful for batching multiple synchronous updates.

let pendingUpdate: Promise<void> | null = null;

const batchedSave = async (data: Data) => {
latestData = data;

if (!pendingUpdate) {
pendingUpdate = defer(async () => {
await saveToServer(latestData);
pendingUpdate = null;
});
}

return pendingUpdate;
};

// Multiple rapid calls = single save with latest data
batchedSave({ a: 1 });
batchedSave({ a: 2 });
batchedSave({ a: 3 }); // Only this one is saved

Defer analytics tracking after page render

Send analytics events after the page has finished rendering. Essential for not blocking the critical rendering path with tracking calls.

const trackPageView = async (page: string) => {
// Let the page render first
await defer(() => {});

analytics.track("page_view", {
page,
timestamp: Date.now(),
viewport: `${window.innerWidth}x${window.innerHeight}`,
});
};

// Called on route change - doesn't block rendering
trackPageView("/dashboard");

Defer portal content rendering after container mount

Wait for the portal container to be mounted in the DOM before rendering content. Essential for CDK-style Portal implementations where content is projected into a remote container.

const renderInPortal = async (content: HTMLElement, portalOutlet: HTMLElement) => {
document.body.appendChild(portalOutlet);

// Wait for portal container to be in the DOM
await defer(() => {});

portalOutlet.appendChild(content);

// Wait for content to render before measuring
await defer(() => {});

const rect = content.getBoundingClientRect();
positionOverlay(rect);
};

Yield between heavy DOM updates for skeleton transitions

Allow the browser to paint skeleton placeholders before replacing with real content. Critical for smooth skeleton-to-content transitions.

const loadSection = async (sectionId: string) => {
showSkeleton(sectionId);

// Let skeleton render
await defer(() => {});

const data = await fetchSectionData(sectionId);

// Let browser breathe before heavy DOM swap
await defer(() => {});

replaceSkeleton(sectionId, renderContent(data));
};

Animate element entrance after DOM insertion

Defer adding the animation class so the browser registers the initial state first. Essential for CSS transition-based entrance animations.

const animateIn = async (element: HTMLElement) => {
element.style.opacity = "0";
element.style.transform = "translateY(20px)";
container.appendChild(element);

// Wait for browser to register initial state
await defer(() => {});

element.style.transition = "opacity 0.3s, transform 0.3s";
element.style.opacity = "1";
element.style.transform = "translateY(0)";
};

Sequence exit then entrance animations

Wait for an exit animation to complete before starting the entrance of new content. Perfect for page transitions, tab switches, and panel swaps.

const swapContent = async (oldEl: HTMLElement, newEl: HTMLElement) => {
// Exit animation
oldEl.classList.add("fade-out");

// Wait for exit to paint
await defer(() => {});
await sleep(300); // Match CSS transition duration

oldEl.remove();
container.appendChild(newEl);

// Wait for DOM insertion
await defer(() => {});

// Entrance animation
newEl.classList.add("fade-in");
};