diff --git a/src/components/email/AttachmentChips.tsx b/src/components/email/AttachmentChips.tsx index d2708f9a..dc4385ba 100644 --- a/src/components/email/AttachmentChips.tsx +++ b/src/components/email/AttachmentChips.tsx @@ -45,11 +45,21 @@ export function AttachmentChips({ email, jmapAccountId, calendarBannerShown, tne const t = useLocaleStore((s) => s.t); const hideInlineImageAttachments = useSettingsStore((s) => s.hideInlineImageAttachments); const mailAttachmentAction = useSettingsStore((s) => s.mailAttachmentAction); - const exportOpts = useSettingsStore((s) => ({ - spaceReplacement: s.exportSpaceReplacement, - lowercase: s.exportLowercase, - stripDiacritics: s.exportStripDiacritics, - })); + // Select primitives, not a fresh object: zustand v5 compares snapshots with + // Object.is, so a selector returning a new object literal on every call + // reports a change on every render - React then re-renders until it gives + // up with "Maximum update depth exceeded" (it crashed the whole viewer). + const exportSpaceReplacement = useSettingsStore((s) => s.exportSpaceReplacement); + const exportLowercase = useSettingsStore((s) => s.exportLowercase); + const exportStripDiacritics = useSettingsStore((s) => s.exportStripDiacritics); + const exportOpts = React.useMemo( + () => ({ + spaceReplacement: exportSpaceReplacement, + lowercase: exportLowercase, + stripDiacritics: exportStripDiacritics, + }), + [exportSpaceReplacement, exportLowercase, exportStripDiacritics], + ); const [expanded, setExpanded] = React.useState(false); const [busyKey, setBusyKey] = React.useState(null); diff --git a/src/lib/sidebar-apps.ts b/src/lib/sidebar-apps.ts index f3e76e3e..d18eb49f 100644 --- a/src/lib/sidebar-apps.ts +++ b/src/lib/sidebar-apps.ts @@ -1,3 +1,4 @@ +import React from 'react'; import { Linking } from 'react-native'; import { useSettingsStore, type SidebarApp } from '../stores/settings-store'; @@ -7,7 +8,11 @@ import { useSettingsStore, type SidebarApp } from '../stores/settings-store'; * edits them. Mirrors the webmail's navigation-rail filter on `showOnMobile`. */ export function useMobileSidebarApps(): SidebarApp[] { - return useSettingsStore((s) => s.sidebarApps.filter((app) => app.showOnMobile)); + // `.filter()` in the selector would allocate a new array per call, which + // zustand v5 reads as a changed snapshot on every render (infinite loop). + // Subscribe to the stored array and derive from it. + const apps = useSettingsStore((s) => s.sidebarApps); + return React.useMemo(() => apps.filter((app) => app.showOnMobile), [apps]); } /**