perf: make filter dropdowns feel instant with useDeferredValue

Use React's useDeferredValue to decouple UI responsiveness from
expensive data reprocessing. Checkboxes now update immediately on
click; filterData/filterAdsData/filterBsrData and all downstream
useMemos run as a low-priority background pass. Added local optimistic
state in MultiSelectDropdown as a belt-and-suspenders layer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Vidal Wolf
2026-03-11 09:02:24 +01:00
co-authored by Claude Sonnet 4.6
parent 8e907f1dff
commit dcf77cad2d
2 changed files with 52 additions and 48 deletions
+15 -14
View File
@@ -1,5 +1,5 @@
import React, { useState, useMemo, useEffect, useCallback, Suspense, lazy } from 'react';
import React, { useState, useMemo, useEffect, useCallback, Suspense, lazy, useDeferredValue } from 'react';
import FileUpload from './components/FileUpload';
import Dashboard from './components/Dashboard';
import FilterBar from './components/FilterBar';
@@ -82,6 +82,7 @@ const App: React.FC = () => {
woc: [],
bulkSearch: '',
});
const deferredFilters = useDeferredValue(filters);
const top50Mode = useMemo(() => {
const hasUK = filters.customer.includes('Amazon UK');
@@ -525,8 +526,8 @@ const App: React.FC = () => {
return metaMap;
}, [rawData]);
const filteredData = useMemo(() => filterData(rawData, filters, stockMap, vendorStockMap, top50Mode), [rawData, filters, stockMap, vendorStockMap, top50Mode]);
const filteredAdsData = useMemo(() => filterAdsData(adsData, filters, globalAsinMetadata, stockMap, vendorStockMap, top50Mode), [adsData, filters, globalAsinMetadata, stockMap, vendorStockMap, top50Mode]);
const filteredData = useMemo(() => filterData(rawData, deferredFilters, stockMap, vendorStockMap, top50Mode), [rawData, deferredFilters, stockMap, vendorStockMap, top50Mode]);
const filteredAdsData = useMemo(() => filterAdsData(adsData, deferredFilters, globalAsinMetadata, stockMap, vendorStockMap, top50Mode), [adsData, deferredFilters, globalAsinMetadata, stockMap, vendorStockMap, top50Mode]);
const aggregatedData = useMemo(() => aggregateData(filteredData), [filteredData]);
// Calculate country-aware Top 50 Best Sellers for 2025
@@ -565,8 +566,8 @@ const App: React.FC = () => {
// Derived Data for Vendor (BSR)
const filteredBsrData = useMemo(() => {
return filterBsrData(bsrData, filters, globalAsinMetadata, stockMap, vendorStockMap, top50Mode);
}, [bsrData, filters, globalAsinMetadata, stockMap, vendorStockMap, top50Mode]);
return filterBsrData(bsrData, deferredFilters, globalAsinMetadata, stockMap, vendorStockMap, top50Mode);
}, [bsrData, deferredFilters, globalAsinMetadata, stockMap, vendorStockMap, top50Mode]);
// Combine Sales & Ads Data dynamically based on current filters
const combinedAdsData = useMemo(() => {
@@ -595,7 +596,7 @@ const App: React.FC = () => {
// Only apply the "Max Week" cutoff if the user has NOT explicitly selected a specific time period.
// If Month or Week filters are active, we show exactly what was asked (e.g., Full Year 2024).
// If no time filters are active, we default to YTD (Like-for-Like) comparison.
const isTimeFilterActive = filters.month.length > 0 || filters.week.length > 0;
const isTimeFilterActive = deferredFilters.month.length > 0 || deferredFilters.week.length > 0;
if (!combinedAdsData) return [];
@@ -607,25 +608,25 @@ const App: React.FC = () => {
if (!item.week) return true;
return item.week <= gridContext.maxWeek;
});
}, [combinedAdsData, gridContext.maxWeek, filters.month, filters.week]);
}, [combinedAdsData, gridContext.maxWeek, deferredFilters.month, deferredFilters.week]);
// Derived Data for Dashboard (YTD Filtered - Isolated)
// Dashboard needs aggregated data respecting the max week limit of current year.
// Dashboard needs aggregated data respecting the max week limit of current year...
// UNLESS the user explicitly filters for a period.
const ytdFilteredData = useMemo(() => {
const isTimeFilterActive = filters.month.length > 0 || filters.week.length > 0;
const isTimeFilterActive = deferredFilters.month.length > 0 || deferredFilters.week.length > 0;
if (isTimeFilterActive) return filteredData;
return filteredData.filter(r => r.week <= gridContext.maxWeek);
}, [filteredData, gridContext.maxWeek, filters.month, filters.week]);
}, [filteredData, gridContext.maxWeek, deferredFilters.month, deferredFilters.week]);
const ytdFilteredAdsData = useMemo(() => {
const isTimeFilterActive = filters.month.length > 0 || filters.week.length > 0;
const isTimeFilterActive = deferredFilters.month.length > 0 || deferredFilters.week.length > 0;
if (isTimeFilterActive) return filteredAdsData;
return filteredAdsData.filter(r => r.week <= gridContext.maxWeek);
}, [filteredAdsData, gridContext.maxWeek, filters.month, filters.week]);
}, [filteredAdsData, gridContext.maxWeek, deferredFilters.month, deferredFilters.week]);
const ytdAggregatedData = useMemo(() => {
return aggregateData(ytdFilteredData);
@@ -637,7 +638,7 @@ const App: React.FC = () => {
// Derive Context Data (Product Line Context when drilling down)
const contextAggregatedData = useMemo(() => {
// Check if we are filtering by specific items (SKU, ASIN, Title)
const hasItemFilters = filters.sku.length > 0 || filters.asin.length > 0 || filters.title.length > 0;
const hasItemFilters = deferredFilters.sku.length > 0 || deferredFilters.asin.length > 0 || deferredFilters.title.length > 0;
if (!hasItemFilters || filteredData.length === 0) {
return null;
@@ -648,7 +649,7 @@ const App: React.FC = () => {
// 2. Create a "broad" filter: Keep Year/Customer/Month, but CLEAR Item filters, and restrict to these Lines
const contextFilters: FilterState = {
...filters,
...deferredFilters,
line: activeLines, // Force these lines
sku: [], // Clear specific item filters
asin: [],
@@ -660,7 +661,7 @@ const App: React.FC = () => {
const broadData = filterData(rawData, contextFilters);
return aggregateData(broadData);
}, [rawData, filters, filteredData]);
}, [rawData, deferredFilters, filteredData]);
// Derive Options for Filter Dropdowns