fix(CQ-WEB-8): Add React.memo to performance-sensitive components
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

Wrap 7 list-item/card components with React.memo to prevent unnecessary
re-renders when parent components update but props remain unchanged:
- TaskItem (task lists)
- EventCard (calendar views)
- EntryCard (knowledge base)
- WorkspaceCard (workspace list)
- TeamCard (team list)
- DomainItem (domain list)
- ConnectionCard (federation connections)

All are pure components rendered inside .map() loops that depend solely
on their props for rendering output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-06 18:28:08 -06:00
parent 1005b7969c
commit 214139f4d5
7 changed files with 32 additions and 14 deletions

View File

@@ -1,3 +1,4 @@
import React from "react";
import type { Event } from "@mosaic/shared";
import { formatTime } from "@/lib/utils/date-format";
@@ -5,7 +6,9 @@ interface EventCardProps {
event: Event;
}
export function EventCard({ event }: EventCardProps): React.JSX.Element {
export const EventCard = React.memo(function EventCard({
event,
}: EventCardProps): React.JSX.Element {
return (
<div className="bg-white p-3 rounded-lg border-l-4 border-blue-500 shadow-sm hover:shadow-md transition-shadow">
<div className="flex justify-between items-start mb-1">
@@ -23,4 +26,4 @@ export function EventCard({ event }: EventCardProps): React.JSX.Element {
{event.location && <p className="text-xs text-gray-500">📍 {event.location}</p>}
</div>
);
}
});

View File

@@ -1,5 +1,6 @@
"use client";
import React from "react";
import type { Domain } from "@mosaic/shared";
interface DomainItemProps {
@@ -8,7 +9,11 @@ interface DomainItemProps {
onDelete?: (domain: Domain) => void;
}
export function DomainItem({ domain, onEdit, onDelete }: DomainItemProps): React.ReactElement {
export const DomainItem = React.memo(function DomainItem({
domain,
onEdit,
onDelete,
}: DomainItemProps): React.ReactElement {
return (
<div className="border rounded-lg p-4 hover:shadow-md transition-shadow">
<div className="flex items-start justify-between">
@@ -52,4 +57,4 @@ export function DomainItem({ domain, onEdit, onDelete }: DomainItemProps): React
</div>
</div>
);
}
});

View File

@@ -3,6 +3,7 @@
* Displays a single federation connection with PDA-friendly design
*/
import React from "react";
import { FederationConnectionStatus, type ConnectionDetails } from "@/lib/api/federation";
interface ConnectionCardProps {
@@ -50,7 +51,7 @@ function getStatusDisplay(status: FederationConnectionStatus): {
}
}
export function ConnectionCard({
export const ConnectionCard = React.memo(function ConnectionCard({
connection,
onAccept,
onReject,
@@ -149,4 +150,4 @@ export function ConnectionCard({
)}
</div>
);
}
});

View File

@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
import React from "react";
import type { KnowledgeEntryWithTags } from "@mosaic/shared";
import { EntryStatus } from "@mosaic/shared";
import Link from "next/link";
@@ -32,7 +33,9 @@ const visibilityIcons = {
PUBLIC: <Eye className="w-3 h-3" />,
};
export function EntryCard({ entry }: EntryCardProps): React.JSX.Element {
export const EntryCard = React.memo(function EntryCard({
entry,
}: EntryCardProps): React.JSX.Element {
const statusInfo = statusConfig[entry.status];
const visibilityIcon = visibilityIcons[entry.visibility];
@@ -107,4 +110,4 @@ export function EntryCard({ entry }: EntryCardProps): React.JSX.Element {
</div>
</Link>
);
}
});

View File

@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
import React from "react";
import type { Task } from "@mosaic/shared";
import { TaskStatus, TaskPriority } from "@mosaic/shared";
import { formatDate, isPastTarget, isApproachingTarget } from "@/lib/utils/date-format";
@@ -21,7 +22,7 @@ const priorityLabels: Record<TaskPriority, string> = {
[TaskPriority.LOW]: "Low priority",
};
export function TaskItem({ task }: TaskItemProps): React.JSX.Element {
export const TaskItem = React.memo(function TaskItem({ task }: TaskItemProps): React.JSX.Element {
const statusIcon = statusIcons[task.status];
const priorityLabel = priorityLabels[task.priority];
@@ -61,4 +62,4 @@ export function TaskItem({ task }: TaskItemProps): React.JSX.Element {
</div>
</div>
);
}
});

View File

@@ -1,3 +1,4 @@
import React from "react";
import type { Team } from "@mosaic/shared";
import { Card, CardHeader, CardContent } from "@mosaic/ui";
import Link from "next/link";
@@ -7,7 +8,10 @@ interface TeamCardProps {
workspaceId: string;
}
export function TeamCard({ team, workspaceId }: TeamCardProps): React.JSX.Element {
export const TeamCard = React.memo(function TeamCard({
team,
workspaceId,
}: TeamCardProps): React.JSX.Element {
return (
<Link href={`/settings/workspaces/${workspaceId}/teams/${team.id}`}>
<Card className="hover:shadow-lg transition-shadow cursor-pointer">
@@ -27,4 +31,4 @@ export function TeamCard({ team, workspaceId }: TeamCardProps): React.JSX.Elemen
</Card>
</Link>
);
}
});

View File

@@ -1,3 +1,4 @@
import React from "react";
import type { Workspace } from "@mosaic/shared";
import { WorkspaceMemberRole } from "@mosaic/shared";
import Link from "next/link";
@@ -22,7 +23,7 @@ const roleLabels: Record<WorkspaceMemberRole, string> = {
[WorkspaceMemberRole.GUEST]: "Guest",
};
export function WorkspaceCard({
export const WorkspaceCard = React.memo(function WorkspaceCard({
workspace,
userRole,
memberCount,
@@ -58,4 +59,4 @@ export function WorkspaceCard({
</div>
</Link>
);
}
});