diff --git a/src/components/calendar/MonthView.tsx b/src/components/calendar/MonthView.tsx index 0fa9717f..2b3b3932 100644 --- a/src/components/calendar/MonthView.tsx +++ b/src/components/calendar/MonthView.tsx @@ -32,7 +32,7 @@ interface MonthViewProps { onLongPressDate?: (date: Date) => void; } -export function MonthView({ +function MonthViewInner({ currentDate, selectedDate, events, @@ -118,6 +118,8 @@ export function MonthView({ ); } +export const MonthView = React.memo(MonthViewInner); + const styles = StyleSheet.create({ grid: { paddingHorizontal: spacing.sm }, weekdayRow: { flexDirection: 'row', paddingHorizontal: spacing.xs, marginBottom: 4 }, diff --git a/src/components/calendar/WeekView.tsx b/src/components/calendar/WeekView.tsx index 8604c0ed..7fb8a680 100644 --- a/src/components/calendar/WeekView.tsx +++ b/src/components/calendar/WeekView.tsx @@ -39,7 +39,7 @@ interface WeekViewProps { weekStartsOn?: 0 | 1; } -export function WeekView({ +function WeekViewInner({ selectedDate, events, calendars, @@ -303,6 +303,8 @@ export function WeekView({ ); } +export const WeekView = React.memo(WeekViewInner); + function minutesToTimeLabel(minutes: number): string { const h = Math.floor(minutes / 60); const m = minutes % 60; diff --git a/src/screens/CalendarScreen.tsx b/src/screens/CalendarScreen.tsx index af2a363c..4a58d349 100644 --- a/src/screens/CalendarScreen.tsx +++ b/src/screens/CalendarScreen.tsx @@ -319,7 +319,7 @@ export default function CalendarScreen() { eventsByDay={eventsByDay} calendars={calendars} onSelectDate={handleSelectDate} - onLongPressDate={(date) => openCreate(date)} + onLongPressDate={openCreate} /> )} {viewMode === 'week' && ( @@ -330,7 +330,7 @@ export default function CalendarScreen() { calendars={calendars} onSelectDate={handleSelectDate} onSelectEvent={setDetailEvent} - onCreateAtTime={(date) => openCreate(date)} + onCreateAtTime={openCreate} /> )} {viewMode === 'agenda' && ( diff --git a/src/screens/EmailListScreen.tsx b/src/screens/EmailListScreen.tsx index d6deb694..3d0acd0d 100644 --- a/src/screens/EmailListScreen.tsx +++ b/src/screens/EmailListScreen.tsx @@ -43,7 +43,7 @@ function formatRelativeTime(date: Date): string { return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); } -function EmailRow({ +const EmailRow = React.memo(function EmailRow({ item, onPress, onLongPress, @@ -51,8 +51,8 @@ function EmailRow({ selectionMode, }: { item: Email; - onPress: () => void; - onLongPress: () => void; + onPress: (id: string) => void; + onLongPress: (id: string) => void; selected: boolean; selectionMode: boolean; }) { @@ -61,6 +61,9 @@ function EmailRow({ const unread = isUnread(item); const starred = isStarred(item); + const handlePress = React.useCallback(() => onPress(item.id), [onPress, item.id]); + const handleLongPress = React.useCallback(() => onLongPress(item.id), [onLongPress, item.id]); + return ( [ @@ -68,8 +71,8 @@ function EmailRow({ pressed && styles.emailRowPressed, selected && styles.emailRowSelected, ]} - onPress={onPress} - onLongPress={onLongPress} + onPress={handlePress} + onLongPress={handleLongPress} delayLongPress={300} > {selectionMode && ( @@ -117,7 +120,11 @@ function EmailRow({ ); -} +}); + +const EmailRowSeparator = () => ; + +const emailKeyExtractor = (item: Email) => item.id; interface EmailListScreenProps { onEmailPress?: (email: Email) => void; @@ -152,6 +159,16 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL const allSelected = emails.length > 0 && emails.every((e) => selectedIds.has(e.id)); + // Refs so row press handlers stay referentially stable across renders. + // FlatList rows then skip re-render when the parent re-renders for unrelated + // reasons (e.g. opening the filter modal). + const onEmailPressRef = React.useRef(onEmailPress); + React.useEffect(() => { onEmailPressRef.current = onEmailPress; }, [onEmailPress]); + const selectionModeRef = React.useRef(selectionMode); + React.useEffect(() => { selectionModeRef.current = selectionMode; }, [selectionMode]); + const emailsRef = React.useRef(emails); + React.useEffect(() => { emailsRef.current = emails; }, [emails]); + const toggleSelect = React.useCallback((id: string) => { setSelectedIds((prev) => { const next = new Set(prev); @@ -161,6 +178,28 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL }); }, []); + const handleRowPress = React.useCallback((id: string) => { + if (selectionModeRef.current) { + toggleSelect(id); + } else { + const email = emailsRef.current.find((e) => e.id === id); + if (email) onEmailPressRef.current?.(email); + } + }, [toggleSelect]); + + const renderEmailRow = React.useCallback( + ({ item }: { item: Email }) => ( + + ), + [selectedIds, selectionMode, handleRowPress, toggleSelect], + ); + const clearSelection = React.useCallback(() => { setSelectedIds(new Set()); }, []); @@ -488,20 +527,9 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL ) : ( item.id} - renderItem={({ item }) => ( - { - if (selectionMode) toggleSelect(item.id); - else onEmailPress?.(item); - }} - onLongPress={() => toggleSelect(item.id)} - /> - )} - ItemSeparatorComponent={() => } + keyExtractor={emailKeyExtractor} + renderItem={renderEmailRow} + ItemSeparatorComponent={EmailRowSeparator} contentContainerStyle={styles.listContent} onEndReached={() => { void loadMoreEmails(); }} onEndReachedThreshold={0.3}