diff --git a/packages/views/issues/components/board-view.tsx b/packages/views/issues/components/board-view.tsx index 0de0278107..a527e2aef1 100644 --- a/packages/views/issues/components/board-view.tsx +++ b/packages/views/issues/components/board-view.tsx @@ -116,7 +116,7 @@ function buildGroups( const EMPTY_PROGRESS_MAP = new Map(); const EMPTY_IDS: string[] = []; -export function BoardView({ +function BoardViewImpl({ issues, assigneeGroups, assigneeGroupQueryKey, @@ -643,3 +643,11 @@ function BoardHiddenColumnsPanel({ /> ); } + +/** + * Memoized: the surface controller re-renders on loading-flag flips (e.g. a + * query enabling when the view changes) — without memo every such flip + * re-rendered this entire view tree (hundreds of ms). All props are + * referentially stable useMemo/useCallback outputs from the controller. + */ +export const BoardView = memo(BoardViewImpl); diff --git a/packages/views/issues/components/list-view.tsx b/packages/views/issues/components/list-view.tsx index 80e6d9a00a..07c6966fc5 100644 --- a/packages/views/issues/components/list-view.tsx +++ b/packages/views/issues/components/list-view.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useCallback, useMemo, useEffect, useRef } from "react"; +import { memo, useState, useCallback, useMemo, useEffect, useRef } from "react"; import { ChevronRight, Plus } from "lucide-react"; import { Accordion } from "@base-ui/react/accordion"; import { @@ -68,7 +68,7 @@ function buildListGroups(visibleStatuses: IssueStatus[]): BoardColumnGroup[] { })); } -export function ListView({ +function ListViewImpl({ issues, visibleStatuses, childProgressMap = EMPTY_PROGRESS_MAP, @@ -594,3 +594,11 @@ function StatusAccordionItem({ ); } + +/** + * Memoized: the surface controller re-renders on loading-flag flips (e.g. a + * query enabling when the view changes) — without memo every such flip + * re-rendered this entire view tree (hundreds of ms). All props are + * referentially stable useMemo/useCallback outputs from the controller. + */ +export const ListView = memo(ListViewImpl); diff --git a/packages/views/issues/components/swimlane-view.tsx b/packages/views/issues/components/swimlane-view.tsx index 9b5c3e5d3e..bd579caf94 100644 --- a/packages/views/issues/components/swimlane-view.tsx +++ b/packages/views/issues/components/swimlane-view.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useCallback, useMemo, useEffect, useRef } from "react"; +import { memo, useState, useCallback, useMemo, useEffect, useRef } from "react"; import { DndContext, DragOverlay, @@ -450,7 +450,7 @@ function buildAssigneeLanes( ]; } -export function SwimLaneView({ +function SwimLaneViewImpl({ issues, unfilteredIssues, activeFilters: activeFiltersProp, @@ -1641,3 +1641,11 @@ function SwimLaneLoadMoreCell({ if (!hasMore) return
; return ; } + +/** + * Memoized: the surface controller re-renders on loading-flag flips (e.g. a + * query enabling when the view changes) — without memo every such flip + * re-rendered this entire view tree (hundreds of ms). All props are + * referentially stable useMemo/useCallback outputs from the controller. + */ +export const SwimLaneView = memo(SwimLaneViewImpl); diff --git a/packages/views/issues/surface/issue-surface.tsx b/packages/views/issues/surface/issue-surface.tsx index 2d72cac3b6..c8d5a01b50 100644 --- a/packages/views/issues/surface/issue-surface.tsx +++ b/packages/views/issues/surface/issue-surface.tsx @@ -146,6 +146,15 @@ function IssueSurfaceContent({ }, [controller], ); + // Stable reference for BoardView's issues: the inline flatMap allocated a + // fresh array every render, defeating BoardView's memo. + const boardIssues = useMemo( + () => + controller.assigneeGroups + ? controller.assigneeGroups.flatMap((group) => group.issues) + : issues, + [controller.assigneeGroups, issues], + ); const shouldShowClientEmpty = !!clientFilter && issues.length === 0 && @@ -200,11 +209,7 @@ function IssueSurfaceContent({
{controller.viewMode === "board" && ( group.issues) - : issues - } + issues={boardIssues} assigneeGroups={controller.assigneeGroups} assigneeGroupQueryKey={controller.assigneeGroupQueryKey} assigneeGroupFilter={controller.assigneeGroupFilter} diff --git a/packages/views/issues/surface/selection-context.tsx b/packages/views/issues/surface/selection-context.tsx index 69f5f682a1..2090bf44f5 100644 --- a/packages/views/issues/surface/selection-context.tsx +++ b/packages/views/issues/surface/selection-context.tsx @@ -29,7 +29,10 @@ export function useCreateIssueSurfaceSelection( const [selectedIds, setSelectedIds] = useState(() => new Set()); useEffect(() => { - setSelectedIds(new Set()); + // Functional bail: on mount (and on resetKey changes where nothing was + // selected) the selection is already empty — swapping in a NEW empty Set + // here re-rendered the entire surface once per mount for nothing. + setSelectedIds((current) => (current.size === 0 ? current : new Set())); }, [resetKey]); const toggle = useCallback((id: string) => {