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:
parent
2bda324e1a
commit
8b30930c04
5 changed files with 1263 additions and 1238 deletions
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue