queueMicrotask : What It Is and When to Use It

While building a reusable shortcut hints component with TanStack Hotkeys, I hit React's "Cannot update a component while rendering a different component" error. Here's how queueMicrotask fixes it -- and when you should (and shouldn't) reach for it.

April 6, 2026

queueMicrotask schedules a callback to run after the current synchronous JavaScript finishes, but before the browser paints or processes macrotasks (like setTimeout). It sits in the microtask queue alongside resolved Promises.

Execution Order

hljs Synchronous code -> Microtasks (queueMicrotask, Promise.then) -> Macrotasks (setTimeout, setInterval) -> Browser paint

A Real-World React Bug and Fix

The Problem

You have two React components:

  • WardsContainer registers hotkeys using useHotkey from TanStack Hotkeys
  • HotkeyHints subscribes to the hotkey manager store to display active hotkeys

useHotkey internally calls setOptions() on the hotkey manager store during render. The store synchronously notifies all subscribers. HotkeyHints has a subscription that calls setHints() (a setState). This means a setState fires in HotkeyHints while WardsContainer is still mid-render.

React throws: Cannot update a component (HotkeyHints) while rendering a different component (WardsContainer).

Without queueMicrotask

useEffect(() => {
  const manager = getHotkeyManager();
  setHints(readHints(filter));
  const sub = manager.registrations.subscribe(() => setHints(readHints(filter)));
  return () => sub.unsubscribe();
}, [filter]);

The call chain is fully synchronous:

hljs WardsContainer render
  -> useHotkey calls setOptions (synchronous)
    -> store notifies subscribers (synchronous)
      -> HotkeyHints' setHints fires (synchronous, still inside WardsContainer's render)
        -> React error

With queueMicrotask

useEffect(() => {
  const manager = getHotkeyManager();
  setHints(readHints(filter));
  const sub = manager.registrations.subscribe(() => {
    queueMicrotask(() => setHints(readHints(filter)));
  });
  return () => sub.unsubscribe();
}, [filter]);

The synchronous chain is broken:

hljs WardsContainer render
  -> useHotkey calls setOptions (synchronous)
    -> store notifies subscribers (synchronous)
      -> callback queues a microtask (returns immediately)
WardsContainer render finishes
Microtask runs -> setHints fires -> HotkeyHints re-renders

Zero visual delay. The microtask runs before the next paint.

When to Reach for queueMicrotask

1. Breaking synchronous notification chains

Any time an external store subscription fires setState synchronously during another component's render:

store.subscribe(() => {
  queueMicrotask(() => setState(store.getSnapshot()));
});

2. Batching multiple synchronous state updates from non-React sources

If an external event emitter fires multiple events in a tight loop:

socket.on("batch-update", (items) => {
  items.forEach((item) => {
    queueMicrotask(() => updateItem(item));
  });
});

React 18+ auto-batches setState calls in the same microtask, so all updates consolidate into one render.

3. Deferring work that must happen before paint but after current execution

function handleClick() {
  setOptimisticState(newValue);

  queueMicrotask(() => {
    validateAndCorrectIfNeeded(newValue);
  });
}

queueMicrotask vs Alternatives

Approach Timing Use When
queueMicrotask After sync, before paint Breaking sync chains, need immediate-but-deferred
Promise.resolve().then() Same as microtask Same scenarios (microtask is cleaner and more explicit)
setTimeout(fn, 0) After paint (macrotask) Truly deferring to next event loop tick, OK with visual delay
requestAnimationFrame Before next paint Animation-related work
React.startTransition Low-priority React update Keeping UI responsive during expensive re-renders

When NOT to Use It

  • Inside React event handlers -- React already batches these. No deferral needed.
  • For heavy computation -- It blocks the paint just like synchronous code would. Use setTimeout or Web Workers instead.
  • As a general "fix React warnings" tool -- It masks the symptom. Only use it when you genuinely need to break a synchronous call chain from an external store.