chore(lint): hoist jsx-as-prop in app components batch 2

Memoize inline JSX passed as props across agent-list, agent-status-bar,
file-explorer-pane, git-actions-split-button, and message to satisfy
react-perf/jsx-no-jsx-as-prop.
This commit is contained in:
Mohamed Boudra
2026-04-24 05:36:46 +07:00
parent 865c2f0b50
commit b4978a1ef7
5 changed files with 113 additions and 68 deletions

View File

@@ -215,6 +215,11 @@ function SessionRow({
[isSelected],
);
const archivedIcon = useMemo(
() => <Archive size={theme.fontSize.xs} color={theme.colors.foregroundMuted} />,
[theme.fontSize.xs, theme.colors.foregroundMuted],
);
return (
<Pressable
style={pressableStyle}
@@ -230,12 +235,7 @@ function SessionRow({
<Text style={sessionTitleStyle} numberOfLines={1}>
{agent.title || "New session"}
</Text>
{agent.archivedAt ? (
<SessionBadge
label="Archived"
icon={<Archive size={theme.fontSize.xs} color={theme.colors.foregroundMuted} />}
/>
) : null}
{agent.archivedAt ? <SessionBadge label="Archived" icon={archivedIcon} /> : null}
{(agent.pendingPermissionCount ?? 0) > 0 ? (
<SessionBadge label={`${agent.pendingPermissionCount} pending`} tone="warning" />
) : null}
@@ -428,6 +428,19 @@ export function AgentList({
[isActionDaemonUnavailable],
);
const refreshControl = useMemo(
() =>
onRefresh ? (
<RefreshControl
refreshing={isRefreshing}
onRefresh={onRefresh}
tintColor={theme.colors.foregroundMuted}
colors={refreshColors}
/>
) : undefined,
[onRefresh, isRefreshing, theme.colors.foregroundMuted, refreshColors],
);
return (
<>
<FlatList
@@ -439,16 +452,7 @@ export function AgentList({
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
ListFooterComponent={listFooterComponent}
refreshControl={
onRefresh ? (
<RefreshControl
refreshing={isRefreshing}
onRefresh={onRefresh}
tintColor={theme.colors.foregroundMuted}
colors={refreshColors}
/>
) : undefined
}
refreshControl={refreshControl}
/>
<Modal

View File

@@ -316,19 +316,17 @@ function ControlledStatusBar({
selected: boolean;
active: boolean;
onPress: () => void;
}) => {
const visuals = getModeVisuals(provider, option.id, providerDefinitions);
const IconComponent = visuals?.icon ? MODE_ICONS[visuals.icon] : ShieldCheck;
return (
<ComboboxItem
label={option.label}
selected={selected}
active={active}
onPress={onPress}
leadingSlot={<IconComponent size={16} color={theme.colors.foreground} />}
/>
);
},
}) => (
<ModeComboboxOption
option={option}
selected={selected}
active={active}
onPress={onPress}
provider={provider}
providerDefinitions={providerDefinitions}
iconColor={theme.colors.foreground}
/>
),
[provider, providerDefinitions, theme.colors.foreground],
);
@@ -1021,6 +1019,40 @@ function ThinkingMenuItem({
);
}
function ModeComboboxOption({
option,
selected,
active,
onPress,
provider,
providerDefinitions,
iconColor,
}: {
option: ComboboxOption;
selected: boolean;
active: boolean;
onPress: () => void;
provider: string;
providerDefinitions: AgentProviderDefinition[];
iconColor: string;
}) {
const visuals = getModeVisuals(provider, option.id, providerDefinitions);
const IconComponent = visuals?.icon ? MODE_ICONS[visuals.icon] : ShieldCheck;
const leadingSlot = useMemo(
() => <IconComponent size={16} color={iconColor} />,
[IconComponent, iconColor],
);
return (
<ComboboxItem
label={option.label}
selected={selected}
active={active}
onPress={onPress}
leadingSlot={leadingSlot}
/>
);
}
function ModeMenuItem({
mode,
provider,
@@ -1042,12 +1074,13 @@ function ModeMenuItem({
onSelectMode?.(mode.id);
}, [mode.id, onSelectMode]);
const leadingIcon = useMemo(
() => <Icon size={16} color={theme.colors.foreground} />,
[Icon, theme.colors.foreground],
);
return (
<DropdownMenuItem
selected={selected}
onSelect={handleSelect}
leading={<Icon size={16} color={theme.colors.foreground} />}
>
<DropdownMenuItem selected={selected} onSelect={handleSelect} leading={leadingIcon}>
{mode.label}
</DropdownMenuItem>
);

View File

@@ -142,6 +142,15 @@ function TreeRowItem({
[isExpanded],
);
const copyLeading = useMemo(
() => <Copy size={14} color={theme.colors.foregroundMuted} />,
[theme.colors.foregroundMuted],
);
const downloadLeading = useMemo(
() => <Download size={14} color={theme.colors.foregroundMuted} />,
[theme.colors.foregroundMuted],
);
return (
<Pressable onPress={handlePress} style={pressableStyle}>
{depth > 0 && Array.from({ length: depth }, (_, i) => <IndentGuide key={i} index={i} />)}
@@ -187,17 +196,11 @@ function TreeRowItem({
</View>
</View>
<DropdownMenuSeparator />
<DropdownMenuItem
leading={<Copy size={14} color={theme.colors.foregroundMuted} />}
onSelect={handleCopy}
>
<DropdownMenuItem leading={copyLeading} onSelect={handleCopy}>
Copy path
</DropdownMenuItem>
{entry.kind === "file" ? (
<DropdownMenuItem
leading={<Download size={14} color={theme.colors.foregroundMuted} />}
onSelect={handleDownload}
>
<DropdownMenuItem leading={downloadLeading} onSelect={handleDownload}>
Download
</DropdownMenuItem>
) : null}

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo, type ReactElement } from "react";
import { useCallback, useMemo } from "react";
import {
View,
Text,
@@ -17,6 +17,7 @@ import {
} from "@/components/ui/dropdown-menu";
import { Shortcut } from "@/components/ui/shortcut";
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
import type { ShortcutKey } from "@/utils/format-shortcut";
import { useToast } from "@/contexts/toast-context";
import type { GitAction, GitActions } from "@/components/git-actions-policy";
@@ -28,7 +29,7 @@ interface GitActionsSplitButtonProps {
interface GitActionMenuItemProps {
action: GitAction;
onSelect: (action: GitAction) => void;
trailing?: ReactElement | null;
archiveShortcutKeys?: ShortcutKey[][] | null;
needsSeparator?: boolean;
showSeparator?: boolean;
closeOnSelect?: boolean;
@@ -37,12 +38,19 @@ interface GitActionMenuItemProps {
function GitActionMenuItem({
action,
onSelect,
trailing,
archiveShortcutKeys,
needsSeparator,
showSeparator,
closeOnSelect,
}: GitActionMenuItemProps) {
const handleSelect = useCallback(() => onSelect(action), [onSelect, action]);
const trailing = useMemo(
() =>
action.id === "archive-worktree" && archiveShortcutKeys ? (
<Shortcut chord={archiveShortcutKeys} />
) : undefined,
[action.id, archiveShortcutKeys],
);
return (
<View>
{needsSeparator && showSeparator ? <DropdownMenuSeparator /> : null}
@@ -154,11 +162,7 @@ export function GitActionsSplitButton({ gitActions, hideLabels }: GitActionsSpli
key={action.id}
action={action}
onSelect={handleActionSelect}
trailing={
action.id === "archive-worktree" && archiveShortcutKeys ? (
<Shortcut chord={archiveShortcutKeys} />
) : undefined
}
archiveShortcutKeys={archiveShortcutKeys}
needsSeparator={
action.id === "merge-from-base" || action.id === "archive-worktree"
}

View File

@@ -1129,26 +1129,27 @@ const NativeExpandableBadgeShimmer = memo(function NativeExpandableBadgeShimmer(
[nativeShimmerPeakStyle, peakWidth, rowHeight],
);
const maskElement = useMemo(
() => (
<View pointerEvents="none" style={nativeShimmerMaskStyle}>
<Text style={nativeLabelMaskStyle} numberOfLines={1}>
{label}
</Text>
{secondaryLabel ? (
<Text style={nativeSecondaryMaskStyle} numberOfLines={1}>
{secondaryLabel}
</Text>
) : (
<View style={expandableBadgeStylesheet.spacer} />
)}
</View>
),
[nativeShimmerMaskStyle, nativeLabelMaskStyle, nativeSecondaryMaskStyle, label, secondaryLabel],
);
return (
<View style={expandableBadgeStylesheet.shimmerOverlay} pointerEvents="none">
<MaskedView
pointerEvents="none"
style={nativeShimmerTrackStyle}
maskElement={
<View pointerEvents="none" style={nativeShimmerMaskStyle}>
<Text style={nativeLabelMaskStyle} numberOfLines={1}>
{label}
</Text>
{secondaryLabel ? (
<Text style={nativeSecondaryMaskStyle} numberOfLines={1}>
{secondaryLabel}
</Text>
) : (
<View style={expandableBadgeStylesheet.spacer} />
)}
</View>
}
>
<MaskedView pointerEvents="none" style={nativeShimmerTrackStyle} maskElement={maskElement}>
<View pointerEvents="none" style={nativeShimmerTrackStyle}>
<Animated.View pointerEvents="none" style={nativeShimmerPeakCombinedStyle}>
<Svg width="100%" height="100%" preserveAspectRatio="none">