perf: memoize KanbanBoard, KanbanGridLayout, MemberCard, TaskRow, SidebarTaskItem

Wrap five hot-path components in React.memo to prevent unnecessary
re-renders when parent state changes don't affect their props.
This commit is contained in:
Mike 2026-05-02 21:20:25 +05:00
parent 2bda324e1a
commit 8b30930c04
5 changed files with 1263 additions and 1238 deletions

View file

@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { memo, useEffect, useMemo, useRef, useState } from 'react';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { getTeamColorSet } from '@renderer/constants/teamColors';
@ -69,7 +69,8 @@ interface SidebarTaskItemProps {
getDisplaySubject?: (task: GlobalTask) => string | undefined;
}
export const SidebarTaskItem = ({
export const SidebarTaskItem = memo(
({
task,
hideTeamName,
showTeamName,
@ -283,4 +284,5 @@ export const SidebarTaskItem = ({
)}
</button>
);
};
}
);

View file

@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { DndContext, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import { arrayMove } from '@dnd-kit/sortable';
@ -311,7 +311,8 @@ const SortableKanbanTaskCard = ({
);
};
export const KanbanBoard = ({
export const KanbanBoard = memo(
({
tasks,
teamName,
kanbanState,
@ -532,7 +533,9 @@ export const KanbanBoard = ({
const columnModeSearchWidth =
primaryVisibleColumnId != null ? (columnWidths.get(primaryVisibleColumnId) ?? 256) : 256;
const toolbarLeftWidth =
viewMode === 'grid' ? (gridPrimaryColumnWidth ?? columnModeSearchWidth) : columnModeSearchWidth;
viewMode === 'grid'
? (gridPrimaryColumnWidth ?? columnModeSearchWidth)
: columnModeSearchWidth;
const clearScheduledScrollRestore = useCallback(() => {
for (const timeoutId of scrollRestoreTimeoutsRef.current) {
@ -697,7 +700,12 @@ export const KanbanBoard = ({
showAddButton: columnSupportsAddButton(column.id, onAddTask),
skeletonCards: columnTasks.map((task) => ({
key: task.id,
height: estimateGridSkeletonCardHeight(task, column.id, kanbanState, hasReviewers),
height: estimateGridSkeletonCardHeight(
task,
column.id,
kanbanState,
hasReviewers
),
})),
};
})}
@ -752,4 +760,5 @@ export const KanbanBoard = ({
}
return boardContent;
};
}
);

View file

@ -1,5 +1,5 @@
/* eslint-disable tailwindcss/no-custom-classname -- this adapter needs stable non-Tailwind class hooks for react-grid-layout handles. */
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import ReactGridLayout, { WidthProvider } from 'react-grid-layout/legacy';
import { usePersistedGridLayout } from '@renderer/hooks/usePersistedGridLayout';
@ -387,7 +387,8 @@ const LoadedKanbanGridLayout = ({
);
};
export const KanbanGridLayout = ({
export const KanbanGridLayout = memo(
({
columns,
allColumnIds,
primaryColumnId,
@ -454,7 +455,8 @@ export const KanbanGridLayout = ({
) : null}
</div>
);
};
}
);
export { SKELETON_HIDE_DELAY_MS, SKELETON_HIDE_DELAY_MS_ON_MODE_SWITCH };
/* eslint-enable tailwindcss/no-custom-classname -- stable class hooks remain scoped to this file. */

View file

@ -1,4 +1,4 @@
import { useMemo, useState } from 'react';
import { memo, useMemo, useState } from 'react';
import { Badge } from '@renderer/components/ui/badge';
import { SyncedLoader2 } from '@renderer/components/ui/SyncedLoader2';
@ -91,7 +91,8 @@ function splitRuntimeSummaryMemory(runtimeSummary: string | undefined): {
};
}
export const MemberCard = ({
export const MemberCard = memo(
({
member,
memberColor,
runtimeSummary,
@ -277,7 +278,9 @@ export const MemberCard = ({
const restartActionErrorFallback = canRelaunchOpenCode
? 'Failed to relaunch OpenCode teammate'
: 'Failed to retry teammate';
const handleRestartMember = async (event: React.MouseEvent<HTMLButtonElement>): Promise<void> => {
const handleRestartMember = async (
event: React.MouseEvent<HTMLButtonElement>
): Promise<void> => {
event.preventDefault();
event.stopPropagation();
if (!onRestartMember || retryingLaunch) {
@ -470,7 +473,9 @@ export const MemberCard = ({
<TooltipTrigger asChild>
<button
type="button"
aria-label={retryingLaunch ? restartActionBusyLabel : restartActionIdleLabel}
aria-label={
retryingLaunch ? restartActionBusyLabel : restartActionIdleLabel
}
className="rounded p-1 text-amber-300 transition-colors hover:bg-amber-500/10 hover:text-amber-200 disabled:cursor-not-allowed disabled:opacity-60"
disabled={retryingLaunch}
onClick={handleRestartMember}
@ -539,7 +544,9 @@ export const MemberCard = ({
<TooltipTrigger asChild>
<button
type="button"
aria-label={retryingLaunch ? restartActionBusyLabel : restartActionIdleLabel}
aria-label={
retryingLaunch ? restartActionBusyLabel : restartActionIdleLabel
}
className="rounded p-1 text-red-300 transition-colors hover:bg-red-500/10 hover:text-red-200 disabled:cursor-not-allowed disabled:opacity-60"
disabled={retryingLaunch || skippingLaunch}
onClick={handleRestartMember}
@ -581,7 +588,9 @@ export const MemberCard = ({
<TooltipTrigger asChild>
<button
type="button"
aria-label={retryingLaunch ? restartActionBusyLabel : restartActionIdleLabel}
aria-label={
retryingLaunch ? restartActionBusyLabel : restartActionIdleLabel
}
className="rounded p-1 text-zinc-300 transition-colors hover:bg-zinc-500/10 hover:text-zinc-100 disabled:cursor-not-allowed disabled:opacity-60"
disabled={retryingLaunch}
onClick={handleRestartMember}
@ -709,4 +718,5 @@ export const MemberCard = ({
</div>
</div>
);
};
}
);

View file

@ -1,3 +1,5 @@
import { memo } from 'react';
import {
KANBAN_COLUMN_DISPLAY,
REVIEW_STATE_DISPLAY,
@ -12,7 +14,7 @@ interface TaskRowProps {
task: TeamTaskWithKanban;
}
export const TaskRow = ({ task }: TaskRowProps): React.JSX.Element => {
export const TaskRow = memo(({ task }: TaskRowProps): React.JSX.Element => {
const blockedByIds = task.blockedBy?.filter((id) => id.length > 0) ?? [];
const blocksIds = task.blocks?.filter((id) => id.length > 0) ?? [];
const kanbanColumn = getTaskKanbanColumn(task);
@ -62,4 +64,4 @@ export const TaskRow = ({ task }: TaskRowProps): React.JSX.Element => {
</td>
</tr>
);
};
});