fix: prevent infinite render loop crash in DictationPanel (React #185) #4744
ccantynzcommented Jun 13, 2026
Originally written by @vercel[bot] on GitHub.
The latest updates on your projects. Learn more about Vercel for GitHub.
| Project | Deployment | Actions | Updated (UTC) |
|---|---|---|---|
| voxlen | Preview, Comment | Jun 13, 2026 4:47am |
Cross-repo impact
See what breaks downstream if this PR merges.
⮌ Merged
This pull request was merged into main.
c comment · e edit title · m merge · a approve · r request changes · ? shortcuts
Originally written by @ccantynz-alt on GitHub.
Imported from https://github.com/ccantynz-alt/voxlen/pull/57
Problem
The Dictation view crashed with React error #185 — "Maximum update depth exceeded":
Root cause
DictationPanelselected the active clients with a selector that calls.filter()directly inside the Zustand hook:const allClients = useClientsStore((s) => s.clients.filter((c) => !c.archived));The project uses Zustand v5, which is backed by
useSyncExternalStore. That API requiresgetSnapshotto return a stable/cached value..filter()allocates a brand-new array reference on every render — and crucially, even[].filter()returns a fresh[]that is notObject.is-equal to the previous snapshot. React's snapshot consistency check therefore detects a "change" every render, re-renders forever, and bails out with error #185.Because this fires whenever
DictationPanelrenders (regardless of how many clients exist), the crash was effectively unconditional.Fix
Wrap the selector in
useShallow, the idiomatic Zustand v5 remedy. It memoises the derived array by shallow equality, so the reference stays stable until the underlying clients actually change:const allClients = useClientsStore(useShallow((s) => s.clients.filter((c) => !c.archived)));I swept the rest of the codebase for the same anti-pattern. The only other array-method selector is
GrammarPanel.tsx's.find(...), which returns an existing element reference (orundefined) and is stable underObject.is— so no change needed there.Tests
Added
src/components/dictation/clientSelector.test.tsx(4 cases) covering:reference stability across re-renders (the property the buggy version could never satisfy),
the empty-array case (the unconditional crash path),
invalidation when a client is added,
archived-client exclusion.
tsc --noEmit— cleanvitest run— 176 passing (172 prior + 4 new)https://claude.ai/code/session_01Fb4NGxGDzokcK1SFVR26YX
Generated by Claude Code