From 38f476938dd8861c394a22f304b99ef483b3cef8 Mon Sep 17 00:00:00 2001 From: Matt Rabe Date: Sat, 24 Jan 2026 08:17:25 -1000 Subject: [PATCH] Debounce filters. Closes #152 --- apps/mobile/src/app/(tabs)/fertilizers.tsx | 4 +++- apps/mobile/src/app/(tabs)/index.tsx | 4 +++- apps/mobile/src/app/(tabs)/plants.tsx | 4 +++- apps/mobile/src/hooks/useDebounce.ts | 27 ++++++++++++++++++++++ 4 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 apps/mobile/src/hooks/useDebounce.ts diff --git a/apps/mobile/src/app/(tabs)/fertilizers.tsx b/apps/mobile/src/app/(tabs)/fertilizers.tsx index 674473c..8c11f16 100644 --- a/apps/mobile/src/app/(tabs)/fertilizers.tsx +++ b/apps/mobile/src/app/(tabs)/fertilizers.tsx @@ -15,6 +15,7 @@ import { SwipeToDelete } from '../../components/SwipeToDelete' import { useAlert } from '../../contexts/AlertContext' +import { useDebounce } from '../../hooks/useDebounce' import { useRefreshOnFocus } from '../../hooks/useRefetchOnFocus' import { trpc } from '../../trpc' @@ -36,6 +37,7 @@ export function FertilizersScreen() { const [expandedIds, setExpandedIds] = React.useState>(new Set()) const [filterModalVisible, setFilterModalVisible] = React.useState(false) const [searchQuery, setSearchQuery] = React.useState('') + const debouncedSearchQuery = useDebounce(searchQuery.trim(), 300) useLayoutEffect(() => { navigation.setOptions({ @@ -79,7 +81,7 @@ export function FertilizersScreen() { isRefetching, refetch, } = trpc.fertilizers.list.useQuery({ - q: searchQuery || undefined, + q: debouncedSearchQuery || undefined, sortBy: [ { field: 'name', direction: 'asc' }, ], diff --git a/apps/mobile/src/app/(tabs)/index.tsx b/apps/mobile/src/app/(tabs)/index.tsx index adb2224..5fca6c9 100644 --- a/apps/mobile/src/app/(tabs)/index.tsx +++ b/apps/mobile/src/app/(tabs)/index.tsx @@ -15,6 +15,7 @@ import { SnoozeChoreModal } from '../../components/SnoozeChoreModal' import { useAlert } from '../../contexts/AlertContext' +import { useDebounce } from '../../hooks/useDebounce' import { useRefreshOnFocus } from '../../hooks/useRefetchOnFocus' import { trpc } from '../../trpc' @@ -34,6 +35,7 @@ function ToDoScreen() { const [snoozeChore, setSnoozeChore] = useState['chores'][0] | null>(null) const [filterModalVisible, setFilterModalVisible] = useState(false) const [searchQuery, setSearchQuery] = useState('') + const debouncedSearchQuery = useDebounce(searchQuery.trim(), 300) useLayoutEffect(() => { navigation.setOptions({ @@ -60,7 +62,7 @@ function ToDoScreen() { } = trpc.chores.list.useQuery({ includeDoneItems: true, // load all items from API, filter out done items in the client includeNotYetDueItems: true, // load all items from API, filter out future items in the client - q: searchQuery || undefined, + q: debouncedSearchQuery || undefined, sortBy: [ { field: 'nextDate', direction: 'asc' }, { field: 'plant', direction: 'asc' }, diff --git a/apps/mobile/src/app/(tabs)/plants.tsx b/apps/mobile/src/app/(tabs)/plants.tsx index 71d9944..da76f91 100644 --- a/apps/mobile/src/app/(tabs)/plants.tsx +++ b/apps/mobile/src/app/(tabs)/plants.tsx @@ -21,6 +21,7 @@ import { SwipeToDelete } from '../../components/SwipeToDelete' import { useAlert } from '../../contexts/AlertContext' +import { useDebounce } from '../../hooks/useDebounce' import { useRefreshOnFocus } from '../../hooks/useRefetchOnFocus' import { trpc, type Endpoints } from '../../trpc' @@ -106,6 +107,7 @@ export function PlantsScreen() { const [editingPlantSpecies, setEditingPlantSpecies] = React.useState(null) const [filterModalVisible, setFilterModalVisible] = React.useState(false) const [searchQuery, setSearchQuery] = React.useState('') + const debouncedSearchQuery = useDebounce(searchQuery.trim(), 300) useLayoutEffect(() => { navigation.setOptions({ @@ -130,7 +132,7 @@ export function PlantsScreen() { isRefetching, refetch, } = trpc.plants.list.useQuery({ - q: searchQuery || undefined, + q: debouncedSearchQuery || undefined, sortBy: [ { field: 'name', direction: 'asc' }, ], diff --git a/apps/mobile/src/hooks/useDebounce.ts b/apps/mobile/src/hooks/useDebounce.ts new file mode 100644 index 0000000..df53e11 --- /dev/null +++ b/apps/mobile/src/hooks/useDebounce.ts @@ -0,0 +1,27 @@ +import { useEffect, useState } from 'react' + +/** + * Debounce any changing value. + * + * Example: + * const debouncedSearch = useDebounce(search, 300) + */ +export function useDebounce(value: T, delayMs = 300) { + const [debouncedValue, setDebouncedValue] = useState(value) + + useEffect(() => { + // No delay: update immediately + if (delayMs <= 0) { + setDebouncedValue(value) + return + } + + const timeout = setTimeout(() => { + setDebouncedValue(value) + }, delayMs) + + return () => clearTimeout(timeout) + }, [delayMs, value]) + + return debouncedValue +}