From ea1d1e2870f8cf894748cf191df27e5e06010c70 Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Sun, 18 Oct 2026 16:01:43 +0200
Subject: [PATCH] fix: stop re-rendering every row when a search starts
---
.../thread-list-item-source-folder.test.tsx | 19 ++++++++++++++++++-
components/email/thread-list-item.tsx | 12 ++++++++----
2 files changed, 26 insertions(+), 5 deletions(-)
diff --git a/components/email/__tests__/thread-list-item-source-folder.test.tsx b/components/email/__tests__/thread-list-item-source-folder.test.tsx
index 023ab287d..672aad8da 100644
--- a/components/email/__tests__/thread-list-item-source-folder.test.tsx
+++ b/components/email/__tests__/thread-list-item-source-folder.test.tsx
@@ -1,4 +1,5 @@
-import { render, screen } from '@testing-library/react';
+import { Profiler } from 'react';
+import { act, render, screen } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ThreadListItem } from '../thread-list-item';
import { useSettingsStore, DEFAULT_KEYWORDS } from '@/stores/settings-store';
@@ -56,6 +57,7 @@ function renderThread(emails: Email[]) {
);
}
+const noop = () => {};
const single = () => [makeEmail()];
const conversation = () => [
makeEmail({ id: 'email-1', receivedAt: '2024-01-15T10:00:00Z' }),
@@ -123,5 +125,20 @@ describe('ThreadListItem folder chip in search results (#1172)', () => {
renderThread(emails());
expect(screen.getByTitle('Archive')).toBeTruthy();
});
+
+ it('does not re-render a row without a folder when a search starts', () => {
+ // The folder's own list stays on screen until the results arrive; its
+ // rows carry no sourceFolder and have no chip to show.
+ const [thread] = groupEmailsByThread(emails().map((e) => ({ ...e, sourceFolder: undefined })));
+ const onRender = vi.fn();
+ render(
+
+
+ ,
+ );
+ onRender.mockClear();
+ act(() => useEmailStore.setState({ searchQuery: 'invoice', searchMailboxId: '' }));
+ expect(onRender).not.toHaveBeenCalled();
+ });
});
});
diff --git a/components/email/thread-list-item.tsx b/components/email/thread-list-item.tsx
index 95d7c3bff..0719eb98d 100644
--- a/components/email/thread-list-item.tsx
+++ b/components/email/thread-list-item.tsx
@@ -253,9 +253,13 @@ function SourceFolderTag({ name }: { name: string }) {
* Whether rows show their folder chip: in the aggregate "All …" views, and in
* search results that span every folder rather than the one picked in the
* search panel (#1172). Rows of a single-folder list carry no sourceFolder.
+ *
+ * Such a row never subscribes to the view state: starting a search flips it
+ * while the old list is still on screen, and every row re-rendered for a chip
+ * it has nothing to put in.
*/
-function useShowSourceFolder(): boolean {
- return useEmailStore((state) => state.isUnifiedView || isUnscopedSearchActive(state));
+function useShowSourceFolder(sourceFolder: string | undefined): boolean {
+ return useEmailStore((state) => !!sourceFolder && (state.isUnifiedView || isUnscopedSearchActive(state)));
}
/**
@@ -377,7 +381,7 @@ const SingleEmailItem = React.forwardRef(
const interfaceLayout = useSettingsStore((state) => state.interfaceLayout);
const hideJunkAvatarImages = currentMailboxRole === 'junk' && !showAvatarsInJunk;
// Show the originating folder in the aggregate "All …" views.
- const showSourceFolder = useShowSourceFolder() && !!email.sourceFolder;
+ const showSourceFolder = useShowSourceFolder(email.sourceFolder);
const getAccountById = useAccountStore((state) => state.getAccountById);
const account = email.accountId ? getAccountById(email.accountId) : undefined;
const accountColor = account?.avatarColor;
@@ -932,7 +936,7 @@ export const ThreadListItem = React.forwardRef state.getAccountById);
const threadAccount = latestEmail.accountId ? getAccountById(latestEmail.accountId) : undefined;
const threadAccountColor = threadAccount?.avatarColor;