Skip to content

Commit d388ba5

Browse files
committed
perf(mobile): improve filter checkbox responsiveness
1 parent 4030243 commit d388ba5

File tree

1 file changed

+61
-26
lines changed

1 file changed

+61
-26
lines changed

apps/mobile/v1/src/screens/FolderNotes/components/NotesList/FilterSortSheet.tsx

Lines changed: 61 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Ionicons } from '@expo/vector-icons';
22
import { BottomSheetBackdrop, BottomSheetBackdropProps, BottomSheetModal, BottomSheetView } from '@gorhom/bottom-sheet';
33
import { GlassView } from 'expo-glass-effect';
4-
import React, { forwardRef, memo, useCallback, useMemo } from 'react';
4+
import * as Haptics from 'expo-haptics';
5+
import React, { forwardRef, memo, startTransition, useCallback, useEffect, useMemo, useState } from 'react';
56
import { Pressable, StyleSheet, Text, View } from 'react-native';
67

78
import { useTheme } from '@/src/theme';
@@ -30,7 +31,7 @@ interface FilterSortSheetProps {
3031
hasActiveFilters: boolean;
3132
}
3233

33-
// Memoized filter checkbox component for faster toggling
34+
// Memoized filter checkbox component with optimistic local state for instant feedback
3435
const FilterCheckbox = memo(function FilterCheckbox({
3536
label,
3637
isActive,
@@ -48,25 +49,42 @@ const FilterCheckbox = memo(function FilterCheckbox({
4849
foregroundColor: string;
4950
borderColor: string;
5051
}) {
52+
// Optimistic local state for instant visual feedback
53+
const [localActive, setLocalActive] = useState(isActive);
54+
55+
// Sync with parent state when it catches up
56+
useEffect(() => {
57+
setLocalActive(isActive);
58+
}, [isActive]);
59+
60+
const handlePress = useCallback(() => {
61+
// Instant visual update
62+
setLocalActive(prev => !prev);
63+
// Haptic feedback
64+
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
65+
// Trigger actual state update (deferred)
66+
onToggle();
67+
}, [onToggle]);
68+
5169
return (
5270
<Pressable
5371
style={[
5472
styles.filterOptionGrid,
55-
{ backgroundColor: isActive ? 'rgba(59, 130, 246, 0.1)' : 'transparent', borderColor },
73+
{ backgroundColor: localActive ? 'rgba(59, 130, 246, 0.1)' : 'transparent', borderColor },
5674
]}
57-
onPress={onToggle}
75+
onPress={handlePress}
5876
>
5977
<Ionicons
60-
name={isActive ? 'checkbox' : 'square-outline'}
78+
name={localActive ? 'checkbox' : 'square-outline'}
6179
size={20}
62-
color={isActive ? primaryColor : mutedColor}
80+
color={localActive ? primaryColor : mutedColor}
6381
/>
6482
<Text style={[styles.filterOptionTextGrid, { color: foregroundColor }]}>{label}</Text>
6583
</Pressable>
6684
);
6785
});
6886

69-
// Memoized sort radio button component
87+
// Memoized sort radio button component with optimistic local state
7088
const SortRadio = memo(function SortRadio({
7189
label,
7290
isActive,
@@ -82,15 +100,32 @@ const SortRadio = memo(function SortRadio({
82100
mutedColor: string;
83101
foregroundColor: string;
84102
}) {
103+
// Optimistic local state for instant visual feedback
104+
const [localActive, setLocalActive] = useState(isActive);
105+
106+
// Sync with parent state when it catches up
107+
useEffect(() => {
108+
setLocalActive(isActive);
109+
}, [isActive]);
110+
111+
const handlePress = useCallback(() => {
112+
// Instant visual update
113+
setLocalActive(true);
114+
// Haptic feedback
115+
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
116+
// Trigger actual state update (deferred)
117+
onSelect();
118+
}, [onSelect]);
119+
85120
return (
86121
<Pressable
87-
style={[styles.filterOption, { backgroundColor: isActive ? 'rgba(59, 130, 246, 0.1)' : 'transparent' }]}
88-
onPress={onSelect}
122+
style={[styles.filterOption, { backgroundColor: localActive ? 'rgba(59, 130, 246, 0.1)' : 'transparent' }]}
123+
onPress={handlePress}
89124
>
90125
<Ionicons
91-
name={isActive ? 'radio-button-on' : 'radio-button-off'}
126+
name={localActive ? 'radio-button-on' : 'radio-button-off'}
92127
size={24}
93-
color={isActive ? primaryColor : mutedColor}
128+
color={localActive ? primaryColor : mutedColor}
94129
/>
95130
<Text style={[styles.filterOptionText, { color: foregroundColor }]}>{label}</Text>
96131
</Pressable>
@@ -116,22 +151,22 @@ export const FilterSortSheet = forwardRef<BottomSheetModal, FilterSortSheetProps
116151
[]
117152
);
118153

119-
// Memoized toggle handlers
120-
const toggleAttachments = useCallback(() => setFilterConfig(prev => ({ ...prev, showAttachmentsOnly: !prev.showAttachmentsOnly })), [setFilterConfig]);
121-
const toggleCode = useCallback(() => setFilterConfig(prev => ({ ...prev, showCodeOnly: !prev.showCodeOnly })), [setFilterConfig]);
122-
const toggleDiagram = useCallback(() => setFilterConfig(prev => ({ ...prev, showDiagramOnly: !prev.showDiagramOnly })), [setFilterConfig]);
123-
const toggleHidden = useCallback(() => setFilterConfig(prev => ({ ...prev, showHiddenOnly: !prev.showHiddenOnly })), [setFilterConfig]);
124-
const toggleNote = useCallback(() => setFilterConfig(prev => ({ ...prev, showNoteOnly: !prev.showNoteOnly })), [setFilterConfig]);
125-
const togglePublic = useCallback(() => setFilterConfig(prev => ({ ...prev, showPublicOnly: !prev.showPublicOnly })), [setFilterConfig]);
126-
const toggleSheet = useCallback(() => setFilterConfig(prev => ({ ...prev, showSheetOnly: !prev.showSheetOnly })), [setFilterConfig]);
127-
const toggleStarred = useCallback(() => setFilterConfig(prev => ({ ...prev, showStarredOnly: !prev.showStarredOnly })), [setFilterConfig]);
128-
const clearFilters = useCallback(() => setFilterConfig({ showAttachmentsOnly: false, showCodeOnly: false, showDiagramOnly: false, showHiddenOnly: false, showNoteOnly: false, showPublicOnly: false, showSheetOnly: false, showStarredOnly: false }), [setFilterConfig]);
154+
// Memoized toggle handlers - wrapped in startTransition for responsive UI
155+
const toggleAttachments = useCallback(() => startTransition(() => setFilterConfig(prev => ({ ...prev, showAttachmentsOnly: !prev.showAttachmentsOnly }))), [setFilterConfig]);
156+
const toggleCode = useCallback(() => startTransition(() => setFilterConfig(prev => ({ ...prev, showCodeOnly: !prev.showCodeOnly }))), [setFilterConfig]);
157+
const toggleDiagram = useCallback(() => startTransition(() => setFilterConfig(prev => ({ ...prev, showDiagramOnly: !prev.showDiagramOnly }))), [setFilterConfig]);
158+
const toggleHidden = useCallback(() => startTransition(() => setFilterConfig(prev => ({ ...prev, showHiddenOnly: !prev.showHiddenOnly }))), [setFilterConfig]);
159+
const toggleNote = useCallback(() => startTransition(() => setFilterConfig(prev => ({ ...prev, showNoteOnly: !prev.showNoteOnly }))), [setFilterConfig]);
160+
const togglePublic = useCallback(() => startTransition(() => setFilterConfig(prev => ({ ...prev, showPublicOnly: !prev.showPublicOnly }))), [setFilterConfig]);
161+
const toggleSheet = useCallback(() => startTransition(() => setFilterConfig(prev => ({ ...prev, showSheetOnly: !prev.showSheetOnly }))), [setFilterConfig]);
162+
const toggleStarred = useCallback(() => startTransition(() => setFilterConfig(prev => ({ ...prev, showStarredOnly: !prev.showStarredOnly }))), [setFilterConfig]);
163+
const clearFilters = useCallback(() => startTransition(() => setFilterConfig({ showAttachmentsOnly: false, showCodeOnly: false, showDiagramOnly: false, showHiddenOnly: false, showNoteOnly: false, showPublicOnly: false, showSheetOnly: false, showStarredOnly: false })), [setFilterConfig]);
129164

130-
// Memoized sort handlers
131-
const sortByUpdated = useCallback(() => setSortConfig({ option: 'updated', direction: 'desc' }), [setSortConfig]);
132-
const sortByCreated = useCallback(() => setSortConfig({ option: 'created', direction: 'desc' }), [setSortConfig]);
133-
const sortByTitleAsc = useCallback(() => setSortConfig({ option: 'title', direction: 'asc' }), [setSortConfig]);
134-
const sortByTitleDesc = useCallback(() => setSortConfig({ option: 'title', direction: 'desc' }), [setSortConfig]);
165+
// Memoized sort handlers - wrapped in startTransition for responsive UI
166+
const sortByUpdated = useCallback(() => startTransition(() => setSortConfig({ option: 'updated', direction: 'desc' })), [setSortConfig]);
167+
const sortByCreated = useCallback(() => startTransition(() => setSortConfig({ option: 'created', direction: 'desc' })), [setSortConfig]);
168+
const sortByTitleAsc = useCallback(() => startTransition(() => setSortConfig({ option: 'title', direction: 'asc' })), [setSortConfig]);
169+
const sortByTitleDesc = useCallback(() => startTransition(() => setSortConfig({ option: 'title', direction: 'desc' })), [setSortConfig]);
135170

136171
const dismissSheet = useCallback(() => (ref as React.RefObject<BottomSheetModal>).current?.dismiss(), [ref]);
137172

0 commit comments

Comments
 (0)