From 736672007bd2e77a0e5c520a83948caaab36bffc Mon Sep 17 00:00:00 2001 From: Dries Augustyns Date: Thu, 4 Dec 2025 10:14:18 +0100 Subject: [PATCH] Refactor components to use memoization and callbacks for performance improvements --- apps/web/src/components/ActivityItem.tsx | 5 +- .../web/src/components/BillingConsumption.tsx | 32 +-- apps/web/src/components/BillingLimits.tsx | 24 +-- apps/web/src/components/DashboardLayout.tsx | 30 ++- .../src/components/EmailEditor/Toolbar.tsx | 199 ++++++++++++------ .../src/components/SegmentFilterBuilder.tsx | 118 ++++++----- apps/web/src/components/WorkflowBuilder.tsx | 16 +- 7 files changed, 260 insertions(+), 164 deletions(-) diff --git a/apps/web/src/components/ActivityItem.tsx b/apps/web/src/components/ActivityItem.tsx index 4608eb8..cf32c14 100644 --- a/apps/web/src/components/ActivityItem.tsx +++ b/apps/web/src/components/ActivityItem.tsx @@ -1,5 +1,6 @@ import {Badge, Collapsible, CollapsibleContent, CollapsibleTrigger} from '@plunk/ui'; import type {Activity} from './ActivityFeed'; +import {memo} from 'react'; import { AlertCircle, Calendar, @@ -293,7 +294,7 @@ function getActivityConfig(activity: Activity): ActivityConfig { } } -export function ActivityItem({activity, isUpcoming = false}: ActivityItemProps) { +export const ActivityItem = memo(function ActivityItem({activity, isUpcoming = false}: ActivityItemProps) { const config = getActivityConfig(activity); const Icon = config.icon; const timestamp = new Date(activity.timestamp); @@ -358,4 +359,4 @@ export function ActivityItem({activity, isUpcoming = false}: ActivityItemProps) ); -} +}); diff --git a/apps/web/src/components/BillingConsumption.tsx b/apps/web/src/components/BillingConsumption.tsx index fe58cfb..3c23eca 100644 --- a/apps/web/src/components/BillingConsumption.tsx +++ b/apps/web/src/components/BillingConsumption.tsx @@ -2,6 +2,7 @@ import {Card, CardContent, CardDescription, CardHeader, CardTitle, Alert} from ' import {AlertCircle, TrendingUp} from 'lucide-react'; import {useBillingConsumption} from '../lib/hooks/useBillingConsumption'; import {useConfig} from '../lib/hooks/useConfig'; +import {useCallback} from 'react'; interface BillingConsumptionProps { projectId: string; @@ -15,6 +16,22 @@ export function BillingConsumption({projectId, hasSubscription}: BillingConsumpt // Always call the hook to satisfy Rules of Hooks const {consumptionData, isLoading, error} = useBillingConsumption(projectId, hasSubscription && billingEnabled); + // Define callbacks BEFORE any conditional returns (Rules of Hooks) + const formatCurrency = useCallback((amount: number, currency: string) => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: currency.toUpperCase(), + }).format(amount / 100); + }, []); + + const formatDate = useCallback((dateString: string) => { + return new Date(dateString).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }); + }, []); + if (!billingEnabled) { // If billing is globally disabled, hide the card entirely return null; @@ -79,21 +96,6 @@ export function BillingConsumption({projectId, hasSubscription}: BillingConsumpt return null; } - const formatCurrency = (amount: number, currency: string) => { - return new Intl.NumberFormat('en-US', { - style: 'currency', - currency: currency.toUpperCase(), - }).format(amount / 100); - }; - - const formatDate = (dateString: string) => { - return new Date(dateString).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - }); - }; - return ( diff --git a/apps/web/src/components/BillingLimits.tsx b/apps/web/src/components/BillingLimits.tsx index 77a7ae8..0736307 100644 --- a/apps/web/src/components/BillingLimits.tsx +++ b/apps/web/src/components/BillingLimits.tsx @@ -1,4 +1,4 @@ -import {useEffect, useState} from 'react'; +import {memo, useEffect, useMemo, useState} from 'react'; import {useForm} from 'react-hook-form'; import {zodResolver} from '@hookform/resolvers/zod'; import {BillingLimitSchemas} from '@plunk/shared'; @@ -270,24 +270,24 @@ interface UsageDisplayProps { usage: CategoryLimit; } -function UsageDisplay({category, usage}: UsageDisplayProps) { - const getStatusColor = () => { +const UsageDisplay = memo(function UsageDisplay({category, usage}: UsageDisplayProps) { + const statusColor = useMemo(() => { if (usage.isBlocked) return 'text-red-600'; if (usage.isWarning) return 'text-orange-600'; return 'text-green-600'; - }; + }, [usage.isBlocked, usage.isWarning]); - const getProgressColor = () => { + const progressColor = useMemo(() => { if (usage.isBlocked) return 'bg-red-600'; if (usage.isWarning) return 'bg-orange-500'; return 'bg-green-600'; - }; + }, [usage.isBlocked, usage.isWarning]); - const getStatusIcon = () => { + const statusIcon = useMemo(() => { if (usage.isBlocked) return ; if (usage.isWarning) return ; return ; - }; + }, [usage.isBlocked, usage.isWarning]); const limitText = usage.limit === null ? 'Unlimited' : usage.limit.toLocaleString(); @@ -300,8 +300,8 @@ function UsageDisplay({category, usage}: UsageDisplayProps) { {usage.usage.toLocaleString()} / {limitText} emails this month