From 85a40a76bfe149806e9142d8e346e85826284caf Mon Sep 17 00:00:00 2001 From: Dave Date: Tue, 8 Sep 2026 22:15:31 +0200 Subject: [PATCH] fix(collections): keep stop expanded while working through its items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a driver collects or reports a problem on an item, the supplier group they were working through used to collapse — forcing them to re-expand it for every single item in the stop. Root cause was a useCallback dep cycle: - loadItems had items.length in its dep array - handleCollect / handleProblem filter the processed item out of state - items.length decreasing rebuilt loadItems (new identity) - the consuming useEffect([loadItems]) re-fired loadItems - loadItems's setExpandedSuppliers(new Set()) wiped every group Fix: mirror items.length into a useRef so loadItems can read the current count for the loading-skeleton decision without subscribing to it. loadItems now only rebuilds when statusFilter changes (mount, filter chip tap, refresh), so processing an item no longer resets the expansion state. Drivers can work through every item in a stop without re-expanding. Behavior preserved on real refreshes / tab switches / filter changes — those still reset to collapsed, which matches the new 'flat list of stops on open' UX. --- app/page.tsx | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 3b0f3a8..71005da 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect, useCallback, useMemo } from 'react'; +import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { useRouter } from 'next/navigation'; import { OrderItem } from '@/lib/types'; import { useAuth } from '@/components/AuthProvider'; @@ -100,6 +100,21 @@ export default function CollectionsPage() { // collapsed independently of the supplier they're inside. const [expandedPOs, setExpandedPOs] = useState>(new Set()); + // Mirror items.length into a ref so loadItems can decide whether to + // flash the loading skeleton WITHOUT making items.length a callback + // dependency. If items.length WERE a dep, every successful collect + // (which filters the collected item out of state) would rebuild + // loadItems, the auto-fire useEffect below would re-run loadItems, + // and the setExpandedSuppliers(new Set()) call inside it would + // collapse the supplier the driver is currently working through — + // forcing them to re-expand for every single item. The ref lets us + // keep the "don't flash a skeleton when items are already on screen" + // behavior while breaking the collapse loop. + const itemsLengthRef = useRef(0); + useEffect(() => { + itemsLengthRef.current = items.length; + }, [items.length]); + useEffect(() => { if (!authLoading && !isLoggedIn) router.push('/login/'); }, [isLoggedIn, authLoading, router]); @@ -116,9 +131,10 @@ export default function CollectionsPage() { const loadItems = useCallback(async (showLoading = true) => { if (!isAuthenticated()) return; // Only show the loading skeleton when we genuinely have nothing - // to render. If cached data is on screen, the re-fetch happens - // silently and the user sees fresh data appear without a flash. - if (showLoading && items.length === 0) setIsLoading(true); + // to render. Read items.length from the ref so this stays a + // current-value check, not a subscription — see the comment on + // itemsLengthRef above for why. + if (showLoading && itemsLengthRef.current === 0) setIsLoading(true); setError(''); try { let data: OrderItem[] = []; @@ -162,7 +178,7 @@ const loadItems = useCallback(async (showLoading = true) => { setIsLoading(false); setIsRefreshing(false); } - }, [statusFilter, items.length]); + }, [statusFilter]); useEffect(() => { loadItems(); }, [loadItems]);