From 0d4b694208fd8e516e1f50f7384d1ac5bd6561e0 Mon Sep 17 00:00:00 2001 From: Dries Augustyns Date: Fri, 6 Feb 2026 08:55:24 +0100 Subject: [PATCH] fix: Implement merging for activity updates to preserve component state --- apps/web/src/components/ActivityFeed.tsx | 39 +++++++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/ActivityFeed.tsx b/apps/web/src/components/ActivityFeed.tsx index 6752f5e..26d2100 100644 --- a/apps/web/src/components/ActivityFeed.tsx +++ b/apps/web/src/components/ActivityFeed.tsx @@ -34,7 +34,8 @@ export function ActivityFeed({typeFilter, dateRangeDays = 30, contactId}: Activi setIsLoadingMore(true); } else { setIsLoading(true); - setActivities([]); + // Don't clear activities immediately - we'll do a smart merge + // This preserves React component instances and their local state setNextCursor(undefined); setHasMore(true); } @@ -58,11 +59,25 @@ export function ActivityFeed({typeFilter, dateRangeDays = 30, contactId}: Activi const result = await network.fetch>('GET', `/activity?${params.toString()}`); if (cursor) { - // Append to existing activities + // Append to existing activities (pagination) setActivities(prev => [...prev, ...result.data]); } else { - // Replace activities - setActivities(result.data); + // Smart merge: preserve existing activity objects by ID to maintain component state + setActivities(prev => { + // If no previous activities, just use new data + if (prev.length === 0) { + return result.data; + } + + // Create a map of existing activities by ID for fast lookup + const existingMap = new Map(prev.map(activity => [activity.id, activity])); + + // For each new activity, reuse existing object if ID matches (preserves React component instances) + // Otherwise use new object. This maintains correct ordering while preserving component state. + return result.data.map(newActivity => + existingMap.get(newActivity.id) ?? newActivity + ); + }); } setNextCursor(result.cursor); @@ -95,7 +110,21 @@ export function ActivityFeed({typeFilter, dateRangeDays = 30, contactId}: Activi const result = await network.fetch<{activities: Activity[]}>('GET', `/activity/upcoming?${params.toString()}`); - setUpcomingActivities(result.activities); + // Smart merge: preserve existing activity objects by ID to maintain component state + setUpcomingActivities(prev => { + // If no previous activities, just use new data + if (prev.length === 0) { + return result.activities; + } + + // Create a map of existing activities by ID for fast lookup + const existingMap = new Map(prev.map(activity => [activity.id, activity])); + + // For each new activity, reuse existing object if ID matches + return result.activities.map(newActivity => + existingMap.get(newActivity.id) ?? newActivity + ); + }); } catch (err) { console.error('Error fetching upcoming activities:', err); // Don't set error state for upcoming - just fail silently