DEV Community

Mehwish
Mehwish

Posted on

The Bug That Only Showed Up in a Second Tab: Debugging a Multi-Tab localStorage Race Condition

Summer Bug Smash: Smash Stories 🐛🛹

This is a submission for DEV's Summer Bug Smash: Smash Stories powered by Sentry.

I was working on InnerHue, a mood-tracking app that stores mood entries in localStorage via a Zustand store. The reported bug: open the app in two tabs, log a mood in each, and entries would randomly go missing or get overwritten.

The investigation

The Zustand store (lib/useMoodStore.ts) kept an in-memory copy of moodHistory, but it never listened for changes coming from other tabs. So Tab A and Tab B each held their own stale snapshot, and whichever tab wrote to localStorage last simply clobbered whatever the other tab had written. Classic race condition, just spread across browser tabs instead of threads.

The fix

I added a window.addEventListener('storage', ...) listener that calls useMoodStore.persist.rehydrate() whenever the mood-storage key changes in another tab, keeping every open tab in sync instead of working off a frozen snapshot.

The bonus bug

While reproducing the race condition, I noticed something else: visiting a mood page kept creating duplicate entries. Turned out addMood() was called directly inside a useEffect with no guard, so React Strict Mode's dev-mode double-invocation of effects fired it twice. I added a useRef guard so it only fires once per genuine page visit.

Both bugs lived in the same localStorage write path, so I fixed them together in one PR.

PR: https://github.com/Nitya-003/InnerHue/pull/291

What I learned

Bugs that only reproduce with multiple tabs open are easy to miss in normal dev testing, so you have to deliberately go looking for them. It's a good reminder to test state-management code against concurrent access, not just sequential single-tab flows.

Top comments (1)

Collapse
 
morphoices profile image
MORPHOICΞS.

Multi-tab bugs are the worst because everything looks fine until you test the exact scenario you weren’t thinking about. ~