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 { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
|
||||||
import { getTeamColorSet } from '@renderer/constants/teamColors';
|
import { getTeamColorSet } from '@renderer/constants/teamColors';
|
||||||
|
|
@ -69,7 +69,8 @@ interface SidebarTaskItemProps {
|
||||||
getDisplaySubject?: (task: GlobalTask) => string | undefined;
|
getDisplaySubject?: (task: GlobalTask) => string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SidebarTaskItem = ({
|
export const SidebarTaskItem = memo(
|
||||||
|
({
|
||||||
task,
|
task,
|
||||||
hideTeamName,
|
hideTeamName,
|
||||||
showTeamName,
|
showTeamName,
|
||||||
|
|
@ -283,4 +284,5 @@ export const SidebarTaskItem = ({
|
||||||
)}
|
)}
|
||||||
</button>
|
</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 { DndContext, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
||||||
import { arrayMove } from '@dnd-kit/sortable';
|
import { arrayMove } from '@dnd-kit/sortable';
|
||||||
|
|
@ -311,7 +311,8 @@ const SortableKanbanTaskCard = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const KanbanBoard = ({
|
export const KanbanBoard = memo(
|
||||||
|
({
|
||||||
tasks,
|
tasks,
|
||||||
teamName,
|
teamName,
|
||||||
kanbanState,
|
kanbanState,
|
||||||
|
|
@ -532,7 +533,9 @@ export const KanbanBoard = ({
|
||||||
const columnModeSearchWidth =
|
const columnModeSearchWidth =
|
||||||
primaryVisibleColumnId != null ? (columnWidths.get(primaryVisibleColumnId) ?? 256) : 256;
|
primaryVisibleColumnId != null ? (columnWidths.get(primaryVisibleColumnId) ?? 256) : 256;
|
||||||
const toolbarLeftWidth =
|
const toolbarLeftWidth =
|
||||||
viewMode === 'grid' ? (gridPrimaryColumnWidth ?? columnModeSearchWidth) : columnModeSearchWidth;
|
viewMode === 'grid'
|
||||||
|
? (gridPrimaryColumnWidth ?? columnModeSearchWidth)
|
||||||
|
: columnModeSearchWidth;
|
||||||
|
|
||||||
const clearScheduledScrollRestore = useCallback(() => {
|
const clearScheduledScrollRestore = useCallback(() => {
|
||||||
for (const timeoutId of scrollRestoreTimeoutsRef.current) {
|
for (const timeoutId of scrollRestoreTimeoutsRef.current) {
|
||||||
|
|
@ -697,7 +700,12 @@ export const KanbanBoard = ({
|
||||||
showAddButton: columnSupportsAddButton(column.id, onAddTask),
|
showAddButton: columnSupportsAddButton(column.id, onAddTask),
|
||||||
skeletonCards: columnTasks.map((task) => ({
|
skeletonCards: columnTasks.map((task) => ({
|
||||||
key: task.id,
|
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;
|
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. */
|
/* 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 ReactGridLayout, { WidthProvider } from 'react-grid-layout/legacy';
|
||||||
|
|
||||||
import { usePersistedGridLayout } from '@renderer/hooks/usePersistedGridLayout';
|
import { usePersistedGridLayout } from '@renderer/hooks/usePersistedGridLayout';
|
||||||
|
|
@ -387,7 +387,8 @@ const LoadedKanbanGridLayout = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const KanbanGridLayout = ({
|
export const KanbanGridLayout = memo(
|
||||||
|
({
|
||||||
columns,
|
columns,
|
||||||
allColumnIds,
|
allColumnIds,
|
||||||
primaryColumnId,
|
primaryColumnId,
|
||||||
|
|
@ -454,7 +455,8 @@ export const KanbanGridLayout = ({
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export { SKELETON_HIDE_DELAY_MS, SKELETON_HIDE_DELAY_MS_ON_MODE_SWITCH };
|
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. */
|
/* 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 { Badge } from '@renderer/components/ui/badge';
|
||||||
import { SyncedLoader2 } from '@renderer/components/ui/SyncedLoader2';
|
import { SyncedLoader2 } from '@renderer/components/ui/SyncedLoader2';
|
||||||
|
|
@ -91,7 +91,8 @@ function splitRuntimeSummaryMemory(runtimeSummary: string | undefined): {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MemberCard = ({
|
export const MemberCard = memo(
|
||||||
|
({
|
||||||
member,
|
member,
|
||||||
memberColor,
|
memberColor,
|
||||||
runtimeSummary,
|
runtimeSummary,
|
||||||
|
|
@ -277,7 +278,9 @@ export const MemberCard = ({
|
||||||
const restartActionErrorFallback = canRelaunchOpenCode
|
const restartActionErrorFallback = canRelaunchOpenCode
|
||||||
? 'Failed to relaunch OpenCode teammate'
|
? 'Failed to relaunch OpenCode teammate'
|
||||||
: 'Failed to retry 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.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
if (!onRestartMember || retryingLaunch) {
|
if (!onRestartMember || retryingLaunch) {
|
||||||
|
|
@ -470,7 +473,9 @@ export const MemberCard = ({
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
type="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"
|
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}
|
disabled={retryingLaunch}
|
||||||
onClick={handleRestartMember}
|
onClick={handleRestartMember}
|
||||||
|
|
@ -539,7 +544,9 @@ export const MemberCard = ({
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
type="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"
|
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}
|
disabled={retryingLaunch || skippingLaunch}
|
||||||
onClick={handleRestartMember}
|
onClick={handleRestartMember}
|
||||||
|
|
@ -581,7 +588,9 @@ export const MemberCard = ({
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
type="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"
|
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}
|
disabled={retryingLaunch}
|
||||||
onClick={handleRestartMember}
|
onClick={handleRestartMember}
|
||||||
|
|
@ -709,4 +718,5 @@ export const MemberCard = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
KANBAN_COLUMN_DISPLAY,
|
KANBAN_COLUMN_DISPLAY,
|
||||||
REVIEW_STATE_DISPLAY,
|
REVIEW_STATE_DISPLAY,
|
||||||
|
|
@ -12,7 +14,7 @@ interface TaskRowProps {
|
||||||
task: TeamTaskWithKanban;
|
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 blockedByIds = task.blockedBy?.filter((id) => id.length > 0) ?? [];
|
||||||
const blocksIds = task.blocks?.filter((id) => id.length > 0) ?? [];
|
const blocksIds = task.blocks?.filter((id) => id.length > 0) ?? [];
|
||||||
const kanbanColumn = getTaskKanbanColumn(task);
|
const kanbanColumn = getTaskKanbanColumn(task);
|
||||||
|
|
@ -62,4 +64,4 @@ export const TaskRow = ({ task }: TaskRowProps): React.JSX.Element => {
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue