I've been building Notie, an AI-integrated PDF study app, for the past several months. Along the way I've collected a list of bugs and design traps that are very easy to fall into if you're building a note-taking or document-annotation app. None of them broke the product on day one — they just made it feel like a side project instead of a real tool. Sharing in case anyone else is in the same trenches.
**1. The "POST → await → refetch" tax**
Every interaction in your app probably has a network round-trip somewhere. The naive shape is: user clicks, you POST, you await the response, you refetch the canonical list from the server, then you setState. That's two network calls and 200–500ms before the user sees anything.
You can get away with this when you have one or two interactions per session. The moment users start doing the thing your app exists for — making highlights, creating cards, adding notes — every action feels like the app is thinking. Even on fast networks. Even on localhost.
The fix is optimistic UI: update local state immediately, fire the network call in the background, swap the temp record for the real one when the response lands, roll back on failure. The trap is that the edge cases will bite you if you don't write the plan down before coding:
- What if the user deletes a temp record before its POST returns? (Track cancelled temps, fire a follow-up DELETE on the real id when POST eventually resolves.)
- What if an unrelated refresh fires mid-flight and wipes your temp? (Preserve in-flight temps through refreshes, dedup by fingerprint — geometry or content, not server id.)
- What about handler functions that try to use a temp-… id (e.g., a double-click that opens a chat panel)? (Handler-level early-return on the temp prefix — single source of truth, can't be bypassed by a UI surface you forgot about.)
I went through four rounds of plan review with an AI code reviewer before writing a line of code on this. Every round caught a real correctness bug. The version that shipped is <300 lines and cut my highlight latency from 400ms to under 50ms.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**2. Trackpad pinch is just wheel + ctrlKey**
I lost an embarrassing amount of time looking for a "pinch" event before realizing browsers translate trackpad pinch gestures into wheel events with ctrlKey: true. Same event the browser sends for Cmd/Ctrl + scroll. One handler covers Mac trackpad pinch, Windows precision trackpad pinch, and the keyboard combo.
Two specific gotchas if you go down this path:
- **`addEventListener('wheel', handler, { passive: false })`**. React's onWheel is passive by default in modern browsers, which means your preventDefault() silently no-ops and the browser zooms the whole page instead of your content. You have to register the listener manually.
- **Cursor-anchored zoom.** The pixel under the cursor needs to stay under the cursor as you zoom, or the page jumps in unpredictable directions and feels broken. The math isn't hard but it has to happen after the new layout commits — use useLayoutEffect, not synchronous scroll mutation in the wheel handler.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**3. Imperative DOM nodes survive React unmounts**
The single sneakiest bug I've hit. If your component imperatively appendChilds elements into a DOM node React doesn't own (in my case, a .highlight-overlay div inside the PDF.js text layer), React doesn't track those nodes. When your component unmounts, your cleanup function had better clear them yourself, because nothing else will.
Symptom in my case: optimistic deletes worked perfectly for the panel row but the colored highlight box stayed painted on the PDF page until the user reloaded. It only happened when the last highlight on a page got deleted — because at that point the component unmounted entirely. When other highlights remained, the within-update cleanup ran and hid the bug.
Fix is one line in the useEffect cleanup: overlay.innerHTML = ''. Finding the bug took an hour because I assumed it was a state issue, not a React lifecycle issue.
If you're doing anything imperative in a React app — drawing on canvases, mounting third-party widgets, manipulating PDF.js layers — set yourself a rule: **every appendChild needs a matching cleanup**.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**4. Storing pixel coordinates breaks the moment you add zoom (or responsive layout)**
This one I got lucky on. My highlight storage uses fractional coordinates — (x, y, width, height) ∈ [0, 1] of each page — instead of absolute pixels. When I later added pinch-to-zoom, every highlight stayed perfectly aligned at every zoom level for free. No migration. No coordinate math. The data was already scale-invariant by construction.
If I'd stored pixels, every existing highlight would have broken the moment a user zoomed in, and I would have had to backfill the entire database with normalized coordinates.
Related but more subtle: if you do anything with getBoundingClientRect() to position child elements inside a CSS-transformed parent (`transform: scale(z)`), those measurements come back in post-transform viewport pixels. Writing them straight back as style.left/top makes the browser apply the transform again, double-scaling the position. You have to divide by the current zoom factor — or read offsetWidth/offsetHeight instead of boundingClientRect, which gives you pre-transform layout dimensions.
I missed this twice in my own code before a careful reviewer caught it. Three separate sites had to be fixed. None of them threw an error — they just silently misaligned.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**The thread running through all four**
Users don't experience your feature list. They experience the moments between clicks. If those moments feel like "the app is thinking," nothing else matters. The boring fixes — latency, gesture handling, cleanup discipline, coordinate hygiene — are the ones nobody else will do for you. AI tools will happily generate features all day. They won't notice your highlight panel feels laggy unless you specifically ask them to time it.
If you're building something with a lot of interaction, sit down and time your top five user actions today. Write down the latencies. Half of them are probably worth one afternoon of work to halve. Compounds fast.
Product is live at [notieapp.com] if you want to see the result — free to start, drop in a PDF.