--- title: 'Born and Fixed in Twelve Minutes' excerpt: "GeekBye v1.8.10's changelog says it fixed a crash while editing keyboard shortcuts. It did — but the crash was introduced and fixed in the same pull request, twelve minutes apart, and never reached a single user. The real story is the reliability cascade that produced it: a small, correct change to when shortcuts are suspended, and the React teardown bug that fell out of one line meant to make the editor safer." date: '2026-07-21' author: 'Steven' authorAvatar: '/images/blog/authors/steven.jpg' coverImage: '/images/blog/covers/born-and-fixed-in-twelve-minutes.svg' tags: ['Engineering', 'React', 'Desktop', 'GeekBye Releases'] keywords: - 'react should have a queue error crash' - 'setstate on unmounting component during render' - 'input blur event fires on unmount react' - 'cancel editing on window blur electron' - 'raw dom addeventlistener vs react onblur cleanup' - 'global shortcut suspend while editing keyboard shortcut' lastModified: '2026-07-21' tldr: 'GeekBye v1.8.10''s headline fix — ''a crash while editing keyboard shortcuts and switching windows mid-edit'' — was a bug that never shipped: it was introduced by commit `52d1a8c` and removed by `bf28a50` in the same PR, twelve minutes apart. The release is really about a small correctness change (issue #233): suspend the app''s global shortcuts only while a key is actively being recorded, not for the whole time the settings page is open. Making that safe added a `useEffect` that cancelled editing when the window lost focus — and it attached a raw `blur` listener directly to the capture ``. Switching apps mid-capture fired the window blur, which called `cancelEditing()`, which unmounted the input, whose removal synchronously fired *its own* blur, which re-entered `cancelEditing()` during React''s commit phase — throwing React''s internal `"Should have a queue"` invariant. The fix deletes the raw input listener (keeping only the window one) and moves the click-away cancel onto React''s own `onBlur` prop, deferred a tick with `setTimeout(…, 0)` so no state update ever lands inside the render cycle. It''s a clean case study in why a raw DOM listener on a React-managed element is a landmine, and why ''fix a bug'' and ''ship a bug'' are sometimes the same commit.' keyTakeaways: - "The crash never shipped: commit `52d1a8c` introduced it and `bf28a50` removed it in the same PR twelve minutes later — the honest version of the changelog line is 'we found and fixed this before it left the building,' and the release's real content is the feature change underneath it" - "Attaching a raw `addEventListener('blur', …)` to a React-managed `` is a landmine: when the handler unmounts the element, React removing it from the DOM synchronously fires a *second* blur, re-entering your handler during the commit phase — use the React `onBlur` prop, which is scheduled safely, not a raw DOM listener" - 'Calling a state setter on a component that''s mid-teardown throws React''s `"Should have a queue"` invariant; if a cancel/cleanup path must run near unmount, defer it with `setTimeout(() => …, 0)` so it lands after the current render/commit cycle finishes' - "The crash was collateral from fixing a different bug: suspending global shortcuts for the whole settings visit could leave them dead if you navigated away, so v1.8.10 narrowed suspension to only-while-recording (#233) — and 'cancel editing on blur' was the safety valve that came with it, and the thing that broke" - 'Reliability work compounds: the same release also re-registers global shortcuts that macOS drops across sleep/wake, so the shortcut system got three fixes at once — narrower suspension, crash-free teardown, and self-healing after the machine wakes' --- Here is the most honest sentence I can write about GeekBye v1.8.10: the crash it's famous for fixing never happened to anyone. The changelog says "Fixed a crash while editing keyboard shortcuts and switching windows mid-edit," which is true, and reads like a bug that shipped, bit users, and got patched in a follow-up. It wasn't. The crash was introduced by one commit and deleted by another in the _same pull request_, twelve minutes apart, on the same Friday morning. It lived entirely inside a branch. The release notes describe a scar that formed and healed before anyone outside the repo could see it. That's not a failure of the changelog; it's the honest shape of iterative work, and it's worth telling because the twelve minutes contain a genuinely instructive React bug. But to understand the crash you have to understand what the release was actually _for_ — because the crash was collateral damage from fixing something else. ## What the release was actually about GeekBye lets you rebind its keyboard shortcuts in Settings. While you're recording a new combo, the app has to stop its _global_ shortcuts from firing — otherwise pressing Cmd+B to assign it would just toggle the window instead of being captured. So the editor suspends global shortcuts over IPC: the renderer calls `window.electronAPI.setShortcutsSuspended(true)`, and the main process's `ShortcutsHelper` responds by calling `unregisterAll()`, dropping every `globalShortcut` registration so the keypress reaches the capture input instead of triggering an action. On resume it calls `registerGlobalShortcuts()` to put them back. The bug that started all of this (issue #233) was about the _scope_ of that suspension. The old code suspended shortcuts for the entire time the shortcuts settings page was open — not just while you were actively recording a key. Most of the time that's invisible. But if you opened the page, started doing something else, and navigated away without ever finishing an edit, the app could be left with all its global shortcuts unregistered — silently dead — until you went back. The commit that fixes it is candid about the symptom: suspending too broadly risked "making the app appear broken if the user forgot they had the page open." The fix is a one-line narrowing: suspend only when a row is actually being edited — keyed on `!!editingShortcut`, the hook state holding the id of the row you're recording — instead of for the whole visit. Good change. But "only suspend while editing" raises an immediate question: what counts as _finishing_ an edit? Pressing a key finishes it. Pressing Escape cancels it. But what if you just… click away, or switch to another app entirely, mid-capture? If nothing cancels the edit, you're left recording a shortcut into a window that isn't focused, and shortcuts stay suspended. So the same PR added a safety valve: cancel editing when focus leaves. And that's the line that crashed. ## The twelve-minute bug The safety valve was a `useEffect` in `ShortcutsSettings.tsx` that, while a row was being edited, listened for blur: ```js const handleBlur = () => cancelEditing() window.addEventListener('blur', handleBlur) inputRef.current?.addEventListener('blur', handleBlur) // the landmine ``` Two listeners. One on the `window` — fires when you switch to another app. One attached _directly, as a raw DOM listener,_ to the capture `` — meant to fire when you click away to another element. They look redundant and harmless. They are not. Walk the sequence for "switching windows mid-edit," which is exactly the changelog's phrase: 1. You're recording a shortcut; the `` is focused. You Cmd+Tab to another app. 2. The **window** blur fires. `handleBlur` runs `cancelEditing()`, which sets `editingShortcut` back to `null`. 3. Setting that state **unmounts the ``** — the row leaves edit mode, so the input is removed from the DOM. 4. Removing a focused element from the DOM synchronously **dispatches a `blur` event on that element.** The _raw_ `inputRef` listener catches it and calls `cancelEditing()` **again** — this time in the middle of React's commit phase, while the component tree is being torn down. 5. Calling a state setter on a fiber that's mid-teardown trips one of React's internal invariants, and it throws: **`"Should have a queue"`**. The settings view crashes. The bug isn't the window listener and it isn't really the concept of "cancel on blur." It's that one of the two listeners was a **raw `addEventListener` on a React-managed element.** React doesn't know about that listener, doesn't clean it up on unmount, and — crucially — the browser fires blur during the very DOM removal that React is performing, so the handler re-enters your state logic at the worst possible instant. The fix commit's message says it more precisely than I can: "when cancelEditing unmounts the input, the blur fires and calls cancelEditing again during React's render cycle." ## The fix `bf28a50`, twelve minutes after the crash was born, does two small things. First, it **deletes the raw input listener** and keeps only the window one, which is the only one that needed to be a manual DOM listener at all (there's no React `onBlur` for "the whole window lost focus"): ```js const handleWindowBlur = () => cancelEditing() window.addEventListener('blur', handleWindowBlur) return () => { window.removeEventListener('blur', handleWindowBlur) } ``` Second, it moves the click-away case — "user clicked another element" — onto React's own `onBlur` prop on the input, and **defers it a tick**: ```jsx onBlur={() => { // Deferred to avoid state updates during React's render cycle. setTimeout(() => cancelEditing(), 0) }} ``` Both parts matter. Using React's `onBlur` prop instead of a raw listener means React owns the handler's lifecycle and won't leave it dangling. And the `setTimeout(…, 0)` guarantees that even when this blur _is_ triggered by an unmount, the `cancelEditing()` call lands on the next tick — after the current render and commit have finished — so it can never update state on a component that's still tearing down. If the component is already gone by then, the deferred call is a harmless no-op. That's the whole fix. Delete the listener React couldn't see; let React schedule the one that's left. ## Why this is the good kind of boring There's a temptation to file this under "trivial" — a `setTimeout`, a deleted line — and move on. But the shape of it is one of the most common ways React desktop apps crash, and it's worth internalizing: - The crash only appears at a **lifecycle boundary** (unmount), triggered by an **event the framework doesn't schedule** (a native blur during DOM removal). It won't show up in a click-through test; it needs the specific "focus leaves while editing" path. - The root cause is **mixing two ownership models** — raw DOM events and React's synthetic, scheduled events — on the same element. The moment your handler mutates state that can unmount that element, the raw path becomes re-entrant. - The fix is not "add a guard flag" (though an `isMounted` ref would have masked it); it's removing the raw listener entirely, which is the _smaller_ change and the correct one. Subtraction again. And the meta-lesson, the one this whole series keeps circling: read the diff, not the release note. "Fixed a crash while editing keyboard shortcuts" is accurate, but it hides that the crash was self-inflicted, same-morning, and never shipped — and that the real release is a quiet correctness win about _when_ to suspend a global shortcut. ## What else shipped Two more things rode along in v1.8.10, both worth a line. The shortcut system also gained **self-healing across sleep/wake**: macOS can silently drop an app's global shortcut registrations when the machine sleeps, so a resume handler now re-verifies and re-registers them, meaning your shortcuts still work after the laptop wakes without a relaunch. And on the AI side, the client got smarter about rate limits — streaming requests that hit a `429` now back off exponentially and reset their counter when you stop recording, instead of waiting out a fixed delay. Three reliability fixes to different subsystems, one of which is the crash that was never a crash. For the previous chapter in the v1 story, [printing a meeting to PDF without a PDF library](/blog/printing-a-meeting-to-pdf-without-a-pdf-library) (v1.8.9); and for the whole arc, [the anatomy of shipping software to perfection](/blog/anatomy-of-perfection).