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:
Benjamin Sutter
2026-05-24 01:53:03 +02:00
parent 86bdf142b4
commit ce67da73b3
5 changed files with 79 additions and 47 deletions
+33 -23
View File
@@ -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' }}>