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 { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
|
||||
|
|
@ -173,13 +173,14 @@ function applyProjectFilter(tasks: GlobalTask[], projectPath: string | null): Gl
|
|||
return tasks.filter((t) => t.projectPath && normalizePath(t.projectPath) === normalized);
|
||||
}
|
||||
|
||||
export const GlobalTaskList = ({
|
||||
export const GlobalTaskList = memo(
|
||||
({
|
||||
hideHeader = false,
|
||||
filters: externalFilters,
|
||||
onFiltersChange: externalOnFiltersChange,
|
||||
filtersPopoverOpen: externalFiltersPopoverOpen,
|
||||
onFiltersPopoverOpenChange: externalOnFiltersPopoverOpenChange,
|
||||
}: GlobalTaskListProps = {}): React.JSX.Element => {
|
||||
}: GlobalTaskListProps = {}): React.JSX.Element => {
|
||||
const {
|
||||
globalTasks,
|
||||
globalTasksLoading,
|
||||
|
|
@ -209,7 +210,8 @@ export const GlobalTaskList = ({
|
|||
const filters = externalFilters ?? internalFilters;
|
||||
const setFilters = externalOnFiltersChange ?? setInternalFilters;
|
||||
const filtersPopoverOpen = externalFiltersPopoverOpen ?? internalFiltersPopoverOpen;
|
||||
const setFiltersPopoverOpen = externalOnFiltersPopoverOpenChange ?? setInternalFiltersPopoverOpen;
|
||||
const setFiltersPopoverOpen =
|
||||
externalOnFiltersPopoverOpenChange ?? setInternalFiltersPopoverOpen;
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [groupingMode, setGroupingModeState] = useState<TaskGroupingMode>(loadGroupingMode);
|
||||
const [sortMode, setSortModeState] = useState<TaskSortMode>(loadSortMode);
|
||||
|
|
@ -416,7 +418,10 @@ export const GlobalTaskList = ({
|
|||
);
|
||||
const projectVisibleCountByKey = useMemo(
|
||||
() =>
|
||||
syncProjectGroupVisibleCountByKey(projectRequestedVisibleCountByKey, projectGroupVisibility),
|
||||
syncProjectGroupVisibleCountByKey(
|
||||
projectRequestedVisibleCountByKey,
|
||||
projectGroupVisibility
|
||||
),
|
||||
[projectRequestedVisibleCountByKey, projectGroupVisibility]
|
||||
);
|
||||
|
||||
|
|
@ -573,7 +578,9 @@ export const GlobalTaskList = ({
|
|||
onClick={() => setGroupingMode(mode)}
|
||||
className={cn(
|
||||
'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}
|
||||
|
|
@ -850,4 +857,5 @@ export const GlobalTaskList = ({
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useMemo } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
getTeamColorSet,
|
||||
|
|
@ -37,7 +37,8 @@ interface MemberBadgeProps {
|
|||
* When onClick is provided, both avatar and badge are clickable as one unit.
|
||||
* Wrapped in MemberHoverCard to show member info on hover.
|
||||
*/
|
||||
export const MemberBadge = ({
|
||||
export const MemberBadge = memo(
|
||||
({
|
||||
name,
|
||||
color,
|
||||
teamName,
|
||||
|
|
@ -45,7 +46,7 @@ export const MemberBadge = ({
|
|||
hideAvatar,
|
||||
onClick,
|
||||
disableHoverCard,
|
||||
}: MemberBadgeProps): React.JSX.Element => {
|
||||
}: MemberBadgeProps): React.JSX.Element => {
|
||||
const colors = getTeamColorSet(color ?? '');
|
||||
const { isLight } = useTheme();
|
||||
const selectedTeamName = useStore((s) => s.selectedTeamName);
|
||||
|
|
@ -114,4 +115,5 @@ export const MemberBadge = ({
|
|||
{content}
|
||||
</MemberHoverCard>
|
||||
);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState } from 'react';
|
||||
import { memo, useState } from 'react';
|
||||
|
||||
import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer';
|
||||
import { MemberBadge } from '@renderer/components/team/MemberBadge';
|
||||
|
|
@ -20,12 +20,13 @@ interface ReplyQuoteBlockProps {
|
|||
/** Threshold (characters) above which the "more/less" toggle is shown. */
|
||||
const LONG_QUOTE_THRESHOLD = 200;
|
||||
|
||||
export const ReplyQuoteBlock = ({
|
||||
export const ReplyQuoteBlock = memo(
|
||||
({
|
||||
reply,
|
||||
memberColor,
|
||||
bodyMaxHeight = 'max-h-56',
|
||||
replyTaskRefs,
|
||||
}: ReplyQuoteBlockProps): React.JSX.Element => {
|
||||
}: ReplyQuoteBlockProps): React.JSX.Element => {
|
||||
const isLong = reply.originalText.length > LONG_QUOTE_THRESHOLD;
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
|
|
@ -76,4 +77,5 @@ export const ReplyQuoteBlock = ({
|
|||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { memo } from 'react';
|
||||
|
||||
import { SyncedLoader2 } from '@renderer/components/ui/SyncedLoader2';
|
||||
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
||||
|
||||
|
|
@ -15,13 +17,14 @@ interface CurrentTaskIndicatorProps {
|
|||
* Inline indicator showing a spinning loader + "working on" + task label button.
|
||||
* Shared between MemberCard and MemberHoverCard.
|
||||
*/
|
||||
export const CurrentTaskIndicator = ({
|
||||
export const CurrentTaskIndicator = memo(
|
||||
({
|
||||
task,
|
||||
borderColor,
|
||||
maxSubjectLength,
|
||||
activityLabel = 'working on',
|
||||
onOpenTask,
|
||||
}: CurrentTaskIndicatorProps): React.JSX.Element => {
|
||||
}: CurrentTaskIndicatorProps): React.JSX.Element => {
|
||||
const subjectText =
|
||||
typeof maxSubjectLength === 'number' &&
|
||||
maxSubjectLength > 0 &&
|
||||
|
|
@ -53,4 +56,5 @@ export const CurrentTaskIndicator = ({
|
|||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { memo } from 'react';
|
||||
|
||||
import { useSyncedAnimationStyle } from '@renderer/hooks/useSyncedAnimationStyle';
|
||||
import { cn } from '@renderer/lib/utils';
|
||||
|
||||
|
|
@ -8,10 +10,8 @@ interface MemberPresenceDotProps {
|
|||
label: string;
|
||||
}
|
||||
|
||||
export const MemberPresenceDot = ({
|
||||
className,
|
||||
label,
|
||||
}: MemberPresenceDotProps): React.JSX.Element => {
|
||||
export const MemberPresenceDot = memo(
|
||||
({ className, label }: MemberPresenceDotProps): React.JSX.Element => {
|
||||
const shouldSyncPulse = className?.includes('animate-pulse') === true;
|
||||
const syncedPulseStyle = useSyncedAnimationStyle(shouldSyncPulse, PULSE_DURATION_MS);
|
||||
|
||||
|
|
@ -25,4 +25,5 @@ export const MemberPresenceDot = ({
|
|||
aria-label={label}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue