perf: memoize MemberBadge, CurrentTaskIndicator, MemberPresenceDot, ReplyQuoteBlock, GlobalTaskList
Prevent unnecessary re-renders on these frequently-rendered components that appear in MemberCard rows, activity feeds, and the sidebar task list.
This commit is contained in:
parent
8b30930c04
commit
e300d4cbd5
5 changed files with 821 additions and 804 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { confirm } from '@renderer/components/common/ConfirmDialog';
|
import { confirm } from '@renderer/components/common/ConfirmDialog';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
|
||||||
|
|
@ -173,7 +173,8 @@ function applyProjectFilter(tasks: GlobalTask[], projectPath: string | null): Gl
|
||||||
return tasks.filter((t) => t.projectPath && normalizePath(t.projectPath) === normalized);
|
return tasks.filter((t) => t.projectPath && normalizePath(t.projectPath) === normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GlobalTaskList = ({
|
export const GlobalTaskList = memo(
|
||||||
|
({
|
||||||
hideHeader = false,
|
hideHeader = false,
|
||||||
filters: externalFilters,
|
filters: externalFilters,
|
||||||
onFiltersChange: externalOnFiltersChange,
|
onFiltersChange: externalOnFiltersChange,
|
||||||
|
|
@ -209,7 +210,8 @@ export const GlobalTaskList = ({
|
||||||
const filters = externalFilters ?? internalFilters;
|
const filters = externalFilters ?? internalFilters;
|
||||||
const setFilters = externalOnFiltersChange ?? setInternalFilters;
|
const setFilters = externalOnFiltersChange ?? setInternalFilters;
|
||||||
const filtersPopoverOpen = externalFiltersPopoverOpen ?? internalFiltersPopoverOpen;
|
const filtersPopoverOpen = externalFiltersPopoverOpen ?? internalFiltersPopoverOpen;
|
||||||
const setFiltersPopoverOpen = externalOnFiltersPopoverOpenChange ?? setInternalFiltersPopoverOpen;
|
const setFiltersPopoverOpen =
|
||||||
|
externalOnFiltersPopoverOpenChange ?? setInternalFiltersPopoverOpen;
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [groupingMode, setGroupingModeState] = useState<TaskGroupingMode>(loadGroupingMode);
|
const [groupingMode, setGroupingModeState] = useState<TaskGroupingMode>(loadGroupingMode);
|
||||||
const [sortMode, setSortModeState] = useState<TaskSortMode>(loadSortMode);
|
const [sortMode, setSortModeState] = useState<TaskSortMode>(loadSortMode);
|
||||||
|
|
@ -416,7 +418,10 @@ export const GlobalTaskList = ({
|
||||||
);
|
);
|
||||||
const projectVisibleCountByKey = useMemo(
|
const projectVisibleCountByKey = useMemo(
|
||||||
() =>
|
() =>
|
||||||
syncProjectGroupVisibleCountByKey(projectRequestedVisibleCountByKey, projectGroupVisibility),
|
syncProjectGroupVisibleCountByKey(
|
||||||
|
projectRequestedVisibleCountByKey,
|
||||||
|
projectGroupVisibility
|
||||||
|
),
|
||||||
[projectRequestedVisibleCountByKey, projectGroupVisibility]
|
[projectRequestedVisibleCountByKey, projectGroupVisibility]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -573,7 +578,9 @@ export const GlobalTaskList = ({
|
||||||
onClick={() => setGroupingMode(mode)}
|
onClick={() => setGroupingMode(mode)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded px-1.5 py-0.5 transition-colors',
|
'rounded px-1.5 py-0.5 transition-colors',
|
||||||
groupingMode === mode ? 'text-text' : 'text-text-muted hover:text-text-secondary'
|
groupingMode === mode
|
||||||
|
? 'text-text'
|
||||||
|
: 'text-text-muted hover:text-text-secondary'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
|
|
@ -850,4 +857,5 @@ export const GlobalTaskList = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getTeamColorSet,
|
getTeamColorSet,
|
||||||
|
|
@ -37,7 +37,8 @@ interface MemberBadgeProps {
|
||||||
* When onClick is provided, both avatar and badge are clickable as one unit.
|
* When onClick is provided, both avatar and badge are clickable as one unit.
|
||||||
* Wrapped in MemberHoverCard to show member info on hover.
|
* Wrapped in MemberHoverCard to show member info on hover.
|
||||||
*/
|
*/
|
||||||
export const MemberBadge = ({
|
export const MemberBadge = memo(
|
||||||
|
({
|
||||||
name,
|
name,
|
||||||
color,
|
color,
|
||||||
teamName,
|
teamName,
|
||||||
|
|
@ -114,4 +115,5 @@ export const MemberBadge = ({
|
||||||
{content}
|
{content}
|
||||||
</MemberHoverCard>
|
</MemberHoverCard>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState } from 'react';
|
import { memo, useState } from 'react';
|
||||||
|
|
||||||
import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer';
|
import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer';
|
||||||
import { MemberBadge } from '@renderer/components/team/MemberBadge';
|
import { MemberBadge } from '@renderer/components/team/MemberBadge';
|
||||||
|
|
@ -20,7 +20,8 @@ interface ReplyQuoteBlockProps {
|
||||||
/** Threshold (characters) above which the "more/less" toggle is shown. */
|
/** Threshold (characters) above which the "more/less" toggle is shown. */
|
||||||
const LONG_QUOTE_THRESHOLD = 200;
|
const LONG_QUOTE_THRESHOLD = 200;
|
||||||
|
|
||||||
export const ReplyQuoteBlock = ({
|
export const ReplyQuoteBlock = memo(
|
||||||
|
({
|
||||||
reply,
|
reply,
|
||||||
memberColor,
|
memberColor,
|
||||||
bodyMaxHeight = 'max-h-56',
|
bodyMaxHeight = 'max-h-56',
|
||||||
|
|
@ -76,4 +77,5 @@ export const ReplyQuoteBlock = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
import { SyncedLoader2 } from '@renderer/components/ui/SyncedLoader2';
|
import { SyncedLoader2 } from '@renderer/components/ui/SyncedLoader2';
|
||||||
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
||||||
|
|
||||||
|
|
@ -15,7 +17,8 @@ interface CurrentTaskIndicatorProps {
|
||||||
* Inline indicator showing a spinning loader + "working on" + task label button.
|
* Inline indicator showing a spinning loader + "working on" + task label button.
|
||||||
* Shared between MemberCard and MemberHoverCard.
|
* Shared between MemberCard and MemberHoverCard.
|
||||||
*/
|
*/
|
||||||
export const CurrentTaskIndicator = ({
|
export const CurrentTaskIndicator = memo(
|
||||||
|
({
|
||||||
task,
|
task,
|
||||||
borderColor,
|
borderColor,
|
||||||
maxSubjectLength,
|
maxSubjectLength,
|
||||||
|
|
@ -53,4 +56,5 @@ export const CurrentTaskIndicator = ({
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
import { useSyncedAnimationStyle } from '@renderer/hooks/useSyncedAnimationStyle';
|
import { useSyncedAnimationStyle } from '@renderer/hooks/useSyncedAnimationStyle';
|
||||||
import { cn } from '@renderer/lib/utils';
|
import { cn } from '@renderer/lib/utils';
|
||||||
|
|
||||||
|
|
@ -8,10 +10,8 @@ interface MemberPresenceDotProps {
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MemberPresenceDot = ({
|
export const MemberPresenceDot = memo(
|
||||||
className,
|
({ className, label }: MemberPresenceDotProps): React.JSX.Element => {
|
||||||
label,
|
|
||||||
}: MemberPresenceDotProps): React.JSX.Element => {
|
|
||||||
const shouldSyncPulse = className?.includes('animate-pulse') === true;
|
const shouldSyncPulse = className?.includes('animate-pulse') === true;
|
||||||
const syncedPulseStyle = useSyncedAnimationStyle(shouldSyncPulse, PULSE_DURATION_MS);
|
const syncedPulseStyle = useSyncedAnimationStyle(shouldSyncPulse, PULSE_DURATION_MS);
|
||||||
|
|
||||||
|
|
@ -25,4 +25,5 @@ export const MemberPresenceDot = ({
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue