CodeIssuesPull RequestsActionsSecurityInsights
✨ AI
More
Settings

fix: prevent infinite render loop crash in DictationPanel (React #185) #4744

Merged⚡ AI-generatedXSccantynz wants to mergeclaude/dazzling-shannon-ck8lmrmainopened Jun 13, 2026
ccantynzcommented Jun 13, 2026

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":

Something went wrong — An unexpected error occurred in Dictation.

Root cause

DictationPanel selected 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 requires getSnapshot to return a stable/cached value. .filter() allocates a brand-new array reference on every render — and crucially, even [].filter() returns a fresh [] that is not Object.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 DictationPanel renders (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 (or undefined) and is stable under Object.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 — clean

  • vitest run176 passing (172 prior + 4 new)

https://claude.ai/code/session_01Fb4NGxGDzokcK1SFVR26YX


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:47am

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