CodeIssuesPull RequestsActionsSecurityInsights
✨ AI
More
Settings

fix: stop Dictation panel crashing with React #185 (infinite render loop) #4743

Merged⚡ AI-generatedXSccantynz wants to mergeclaude/elegant-knuth-73u7bymainopened Jun 13, 2026
ccantynzcommented Jun 13, 2026

Originally written by @ccantynz-alt on GitHub.
Imported from https://github.com/ccantynz-alt/voxlen/pull/58


Problem

The Dictation view crashed on mount with:

Minified React error #185 — Maximum update depth exceeded

The Dictation ErrorBoundary caught it and showed "An unexpected error occurred in Dictation."

Root cause

DictationPanel selected its client list directly with a .filter() inside the Zustand selector:

const allClients = useClientsStore((s) => s.clients.filter((c) => !c.archived));

Zustand v5 is backed by React's useSyncExternalStore, which calls the selector during render and again immediately afterwards to detect store changes. .filter() allocates a brand-new array on every call, so the two snapshots were never referentially equal ([] !== []). React concluded the store never settles, force-re-rendered, and looped until it threw "Maximum update depth exceeded" (React #185).

This fired on every mount of the Dictation panel, regardless of how many clients existed, because even an empty .filter() returns a fresh array.

Fix

Wrap the selector in useShallow, which memoises the result with a shallow compare so the reference stays stable across renders:

const allClients = useClientsStore(useShallow((s) => s.clients.filter((c) => !c.archived)));

This is the idiomatic Zustand v5 pattern for selectors that derive a new array/object. A full sweep confirmed this was the only new-reference-returning selector in the codebase (the .reduce() selectors in ClientsPanel return scalars; ClientsPanel derives its filtered lists in the component body).

Tests

Added DictationPanel.test.tsx:

  • Mounts the panel and asserts it renders without throwing — this reproduces the exact #185 error on the old selector and passes on the fix.
  • Seeds active + archived clients and verifies the archived one is filtered out of the selector.
tsc --noEmit  ✓ clean
vitest run    ✓ 174 passed (12 files) — was 172, +2 new

Generated by Claude Code

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 Ready Ready Preview, Comment Jun 13, 2026 4:57am

Cross-repo impact

See what breaks downstream if this PR merges.

Analyze →
⮌ Merged

This pull request was merged into main.

c comment · e edit title · m merge · a approve · r request changes · ? shortcuts