Skip to content

Commit 5dc2d0c

Browse files
committed
Make tasks in useSidebarData a stable reference to avoid all consumers re-rendering unnecessarily
1 parent b5c7bcd commit 5dc2d0c

1 file changed

Lines changed: 118 additions & 74 deletions

File tree

apps/array/src/renderer/features/sidebar/hooks/useSidebarData.ts

Lines changed: 118 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { getUserDisplayName } from "@hooks/useUsers";
99
import { filtersMatch } from "@lib/filters";
1010
import { useRegisteredFoldersStore } from "@renderer/stores/registeredFoldersStore";
1111
import type { RegisteredFolder, Task, Workspace } from "@shared/types";
12-
import { useEffect } from "react";
12+
import { useEffect, useMemo } from "react";
1313
import { useWorkspaceStore } from "@/renderer/features/workspace/stores/workspaceStore";
1414
import {
1515
getTaskRepository,
@@ -339,94 +339,138 @@ export function useSidebarData({
339339
syncFolderOrder(folderIds);
340340
}, [syncFolderOrder, folderIds]);
341341

342-
// Sort folders by persisted order
343-
const sortedFolders = sortFoldersByOrder(folders, folderOrder);
344-
const tasksByFolder = groupTasksByFolder(allTasks, folders, workspaces);
345-
346342
const activeTaskId =
347343
activeView.type === "task-detail" && activeView.data
348344
? activeView.data.id
349345
: null;
350346

351-
const getSessionForTask = (taskId: string): AgentSession | undefined => {
352-
return Object.values(sessions).find((s) => s.taskId === taskId);
353-
};
354-
355-
const folderData: FolderData[] = sortedFolders.map((folder) => {
356-
const folderTasks = tasksByFolder.get(folder.id) || [];
357-
358-
const tasksWithActivity = folderTasks.map((task) => {
359-
const session = getSessionForTask(task.id);
360-
// Use max of task.updated_at and local activity timestamp for accurate ordering
361-
const apiUpdatedAt = new Date(task.updated_at).getTime();
362-
const localActivity = localActivityAt[task.id];
363-
const lastActivityAt = localActivity
364-
? Math.max(apiUpdatedAt, localActivity)
365-
: apiUpdatedAt;
366-
const isPinned = pinnedTaskIds.has(task.id);
367-
return {
368-
task,
369-
lastActivityAt,
370-
isGenerating: session?.isPromptPending ?? false,
371-
isPinned,
372-
};
373-
});
347+
// Memoize sorted folders to maintain stable reference
348+
const sortedFolders = useMemo(
349+
() => sortFoldersByOrder(folders, folderOrder),
350+
[folders, folderOrder],
351+
);
374352

375-
// Sort by pinned first, then by most recent activity
376-
tasksWithActivity.sort((a, b) => {
377-
// Pinned tasks come first
378-
if (a.isPinned && !b.isPinned) return -1;
379-
if (!a.isPinned && b.isPinned) return 1;
380-
// Then sort by most recent activity
381-
return b.lastActivityAt - a.lastActivityAt;
382-
});
353+
// Memoize tasks grouped by folder to maintain stable reference
354+
const tasksByFolder = useMemo(
355+
() => groupTasksByFolder(allTasks, folders, workspaces),
356+
[allTasks, folders, workspaces],
357+
);
383358

384-
return {
385-
id: folder.id,
386-
name: folder.name,
387-
path: folder.path,
388-
tasks: tasksWithActivity.map(
389-
({ task, lastActivityAt, isGenerating, isPinned }) => {
390-
const taskLastViewedAt = lastViewedAt[task.id];
391-
const isCurrentlyViewing = activeTaskId === task.id;
392-
// Only show unread if: user has viewed it before AND there's new activity since
393-
const isUnread =
394-
!isCurrentlyViewing &&
395-
taskLastViewedAt !== undefined &&
396-
lastActivityAt > taskLastViewedAt;
397-
398-
return {
399-
id: task.id,
400-
title: task.title,
401-
lastActivityAt,
402-
isGenerating,
403-
isUnread,
404-
isPinned,
405-
};
406-
},
407-
),
359+
// Memoize folder data to prevent unnecessary re-renders in consumers
360+
const folderData: FolderData[] = useMemo(() => {
361+
const getSessionForTask = (taskId: string): AgentSession | undefined => {
362+
return Object.values(sessions).find((s) => s.taskId === taskId);
408363
};
409-
});
410364

411-
const historyData = buildHistoryData(
412-
allTasks,
413-
workspaces,
414-
folders,
365+
return sortedFolders.map((folder) => {
366+
const folderTasks = tasksByFolder.get(folder.id) || [];
367+
368+
const tasksWithActivity = folderTasks.map((task) => {
369+
const session = getSessionForTask(task.id);
370+
// Use max of task.updated_at and local activity timestamp for accurate ordering
371+
const apiUpdatedAt = new Date(task.updated_at).getTime();
372+
const localActivity = localActivityAt[task.id];
373+
const lastActivityAt = localActivity
374+
? Math.max(apiUpdatedAt, localActivity)
375+
: apiUpdatedAt;
376+
const isPinned = pinnedTaskIds.has(task.id);
377+
return {
378+
task,
379+
lastActivityAt,
380+
isGenerating: session?.isPromptPending ?? false,
381+
isPinned,
382+
};
383+
});
384+
385+
// Sort by pinned first, then by most recent activity
386+
tasksWithActivity.sort((a, b) => {
387+
// Pinned tasks come first
388+
if (a.isPinned && !b.isPinned) return -1;
389+
if (!a.isPinned && b.isPinned) return 1;
390+
// Then sort by most recent activity
391+
return b.lastActivityAt - a.lastActivityAt;
392+
});
393+
394+
return {
395+
id: folder.id,
396+
name: folder.name,
397+
path: folder.path,
398+
tasks: tasksWithActivity.map(
399+
({ task, lastActivityAt, isGenerating, isPinned }) => {
400+
const taskLastViewedAt = lastViewedAt[task.id];
401+
const isCurrentlyViewing = activeTaskId === task.id;
402+
// Only show unread if: user has viewed it before AND there's new activity since
403+
const isUnread =
404+
!isCurrentlyViewing &&
405+
taskLastViewedAt !== undefined &&
406+
lastActivityAt > taskLastViewedAt;
407+
408+
return {
409+
id: task.id,
410+
title: task.title,
411+
lastActivityAt,
412+
isGenerating,
413+
isUnread,
414+
isPinned,
415+
};
416+
},
417+
),
418+
};
419+
});
420+
}, [
421+
sortedFolders,
422+
tasksByFolder,
415423
sessions,
416-
lastViewedAt,
417424
localActivityAt,
418425
pinnedTaskIds,
426+
lastViewedAt,
419427
activeTaskId,
420-
historyVisibleCount,
428+
]);
429+
430+
const historyData = useMemo(
431+
() =>
432+
buildHistoryData(
433+
allTasks,
434+
workspaces,
435+
folders,
436+
sessions,
437+
lastViewedAt,
438+
localActivityAt,
439+
pinnedTaskIds,
440+
activeTaskId,
441+
historyVisibleCount,
442+
),
443+
[
444+
allTasks,
445+
workspaces,
446+
folders,
447+
sessions,
448+
lastViewedAt,
449+
localActivityAt,
450+
pinnedTaskIds,
451+
activeTaskId,
452+
historyVisibleCount,
453+
],
421454
);
422455

423-
const pinnedData = buildPinnedData(
424-
allTasks,
425-
sessions,
426-
lastViewedAt,
427-
localActivityAt,
428-
pinnedTaskIds,
429-
activeTaskId,
456+
const pinnedData = useMemo(
457+
() =>
458+
buildPinnedData(
459+
allTasks,
460+
sessions,
461+
lastViewedAt,
462+
localActivityAt,
463+
pinnedTaskIds,
464+
activeTaskId,
465+
),
466+
[
467+
allTasks,
468+
sessions,
469+
lastViewedAt,
470+
localActivityAt,
471+
pinnedTaskIds,
472+
activeTaskId,
473+
],
430474
);
431475

432476
return {

0 commit comments

Comments
 (0)