From c026e4f21303a9fea25d228abdf3501e5c494609 Mon Sep 17 00:00:00 2001 From: Aryan Sharma Date: Mon, 15 Jun 2026 17:16:48 +0200 Subject: [PATCH] fix(windows): wrap pinnedCards Zustand selector in useShallow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symptom: blank window on app start — board renders nothing, devtools shows "Maximum update depth exceeded" originating from BoardView, with the preceding warning "The result of getSnapshot should be cached to avoid an infinite loop". Cause: `useBoardStore((s) => s.pinnedCards())` calls the computed helper on every selector run, which returns a *new* filtered+sorted array each time. Zustand v5 wraps React's useSyncExternalStore and treats a new array reference as a state change → React re-renders → selector runs again → new array → infinite loop → React unmounts the whole tree. Latent bug — has been broken since Zustand bumped to ^5.0.4. Likely masked until a particular state shape exposed the loop reliably. Fix: `useBoardStore(useShallow((s) => s.pinnedCards()))`. useShallow does a shallow array comparison, so a new array reference doesn't trigger a re-render when its contents (card refs) are unchanged. This is the canonical Zustand v5 pattern for selectors returning derived collections. Verified: - `npm run build` green - App window now renders the board instead of staying blank Co-Authored-By: Claude Opus 4.7 --- windows/src/components/BoardView.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/windows/src/components/BoardView.tsx b/windows/src/components/BoardView.tsx index 46f6522..8fbcc2b 100644 --- a/windows/src/components/BoardView.tsx +++ b/windows/src/components/BoardView.tsx @@ -5,6 +5,7 @@ import { } from "@dnd-kit/core"; import { arrayMove, SortableContext, horizontalListSortingStrategy } from "@dnd-kit/sortable"; import { useState } from "react"; +import { useShallow } from "zustand/react/shallow"; import { canMergeCards, mergeCards, useBoardStore } from "../store/boardStore"; import { COLUMNS, type CardDto, type KanbanColumn } from "../types"; import { useTheme, t } from "../theme"; @@ -21,7 +22,11 @@ const dropAnimation: DropAnimation = { export default function BoardView() { const { moveCard, reorderCards, reorderPinnedCards, isLoading, cards, setNewTaskOpen, refresh, setMergeTargetId } = useBoardStore(); - const pinnedCards = useBoardStore((s) => s.pinnedCards()); + // useShallow keeps the selector's new-array-per-call shape from triggering + // an infinite loop under Zustand v5's stricter useSyncExternalStore snapshot + // contract. Array element identity is preserved across renders (cards are + // memoized in the store), so a shallow compare is all React needs. + const pinnedCards = useBoardStore(useShallow((s) => s.pinnedCards())); const [draggingCard, setDraggingCard] = useState(null); const { theme } = useTheme(); const c = t(theme);