perf: memoize expensive list computations + memo on grid/list items
Results.tsx: - useMemo: filter + sort in one pass (was 7 separate array iterations per render) - useMemo: platform/maison/future/missingData counts in single for-loop - Fix: move queryClient.invalidateQueries from render body into useEffect Properties.tsx: - useMemo: wrap applyFilters() call (was full copy+sort on every render) - useMemo: compute matchReady/criticalGaps/lowConfidence/staleOrOutdated/ allMissingFields in a single for-loop (was 5 separate filter passes) ReminderFeed.tsx: - useMemo: wrap applyFilters() call - useCallback: resetFilters (passed to ReminderEmptyState) PropertyIntelligenceCard, ReminderListRow: - React.memo: grid/list items no longer re-render when unrelated parent state changes (e.g. selectedId, filter UI state) tsc --noEmit passes with zero errors Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react'
|
||||
import { useState, useMemo, useEffect } from 'react'
|
||||
import { Box, Button, Card, Typography } from '@mui/material'
|
||||
import { useNavigate, useLocation } from 'react-router'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
@@ -61,36 +61,46 @@ export default function Results() {
|
||||
|
||||
const activeNeed = allNeeds.find(n => n.id === effectiveNeedId) ?? allNeeds[0]
|
||||
|
||||
// Ensure query cache is invalidated when a new need was just created
|
||||
if (activeNeedIdFromNav) {
|
||||
queryClient.invalidateQueries({ queryKey: ['needs'] })
|
||||
}
|
||||
// Invalidate when navigating from NeedBuilder — must be in an effect, not render body
|
||||
useEffect(() => {
|
||||
if (activeNeedIdFromNav) {
|
||||
queryClient.invalidateQueries({ queryKey: ['needs'] })
|
||||
}
|
||||
}, [activeNeedIdFromNav, queryClient])
|
||||
|
||||
const { data: results = [], isLoading, error } = useUnifiedResults(effectiveNeedId)
|
||||
|
||||
const isStaff = currentUser?.role === 'PROPERTY_MANAGER' || currentUser?.role === 'ORGANIZATION_ADMIN'
|
||||
|
||||
const filtered = results.filter(r => {
|
||||
if (r.resultType === 'FUTURE_AVAILABILITY') return showFutureAvailability
|
||||
if (r.resultType === 'VERIFIED_PORTFOLIO') {
|
||||
if (!showOwnProperties) return false
|
||||
return filterSource === 'ALL' || filterSource === 'PLATFORM'
|
||||
// Counts over the full result set — single pass, stable across filter changes
|
||||
const { platformCount, maisonWorkCount, futureCount, missingDataCount } = useMemo(() => {
|
||||
let platform = 0, maison = 0, future = 0, missing = 0
|
||||
for (const r of results) {
|
||||
if (r.resultType === 'VERIFIED_PORTFOLIO' || r.resultType === 'EXTERNAL_MARKET') platform++
|
||||
else if (r.resultType === 'MAISON_WORK') maison++
|
||||
else if (r.resultType === 'FUTURE_AVAILABILITY') future++
|
||||
if ('match' in r && Array.isArray((r as { match?: { missingData?: unknown[] } }).match?.missingData) &&
|
||||
((r as { match?: { missingData?: unknown[] } }).match?.missingData?.length ?? 0) > 0) missing++
|
||||
}
|
||||
if (filterSource === 'ALL') return true
|
||||
if (filterSource === 'PLATFORM') return r.resultType === 'EXTERNAL_MARKET'
|
||||
return r.resultType === filterSource // MAISON_WORK
|
||||
})
|
||||
return { platformCount: platform, maisonWorkCount: maison, futureCount: future, missingDataCount: missing }
|
||||
}, [results])
|
||||
|
||||
const sorted = sortResults(filtered, sortBy)
|
||||
// Filter + sort in one memo — only reruns when inputs change
|
||||
const sorted = useMemo(() => {
|
||||
const filtered = results.filter(r => {
|
||||
if (r.resultType === 'FUTURE_AVAILABILITY') return showFutureAvailability
|
||||
if (r.resultType === 'VERIFIED_PORTFOLIO') {
|
||||
if (!showOwnProperties) return false
|
||||
return filterSource === 'ALL' || filterSource === 'PLATFORM'
|
||||
}
|
||||
if (filterSource === 'ALL') return true
|
||||
if (filterSource === 'PLATFORM') return r.resultType === 'EXTERNAL_MARKET'
|
||||
return r.resultType === filterSource
|
||||
})
|
||||
return sortResults(filtered, sortBy)
|
||||
}, [results, filterSource, sortBy, showFutureAvailability, showOwnProperties])
|
||||
|
||||
const platformCount = results.filter(r => r.resultType === 'VERIFIED_PORTFOLIO' || r.resultType === 'EXTERNAL_MARKET').length
|
||||
const maisonWorkCount = results.filter(r => r.resultType === 'MAISON_WORK').length
|
||||
const futureCount = results.filter(r => r.resultType === 'FUTURE_AVAILABILITY').length
|
||||
const strongCount = filtered.filter(r => r.matchScore >= 80).length
|
||||
const missingDataCount = results.filter(r =>
|
||||
'match' in r && Array.isArray((r as { match?: { missingData?: unknown[] } }).match?.missingData) &&
|
||||
((r as { match?: { missingData?: unknown[] } }).match?.missingData?.length ?? 0) > 0
|
||||
).length
|
||||
const strongCount = useMemo(() => sorted.filter(r => r.matchScore >= 80).length, [sorted])
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
||||
|
||||
Reference in New Issue
Block a user