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.
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.
hljs Synchronous code -> Microtasks (queueMicrotask, Promise.then) -> Macrotasks (setTimeout, setInterval) -> Browser paint
You have two React components:
WardsContainer registers hotkeys using useHotkey from TanStack HotkeysHotkeyHints subscribes to the hotkey manager store to display active hotkeysuseHotkey 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).
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
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.
Any time an external store subscription fires setState synchronously during another component's render:
store.subscribe(() => {
queueMicrotask(() => setState(store.getSnapshot()));
});
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.
function handleClick() {
setOptimisticState(newValue);
queueMicrotask(() => {
validateAndCorrectIfNeeded(newValue);
});
}
| 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 |
setTimeout or Web Workers instead.