Card görünüm loading problemi

This commit is contained in:
Sedat Öztürk 2026-01-05 22:30:23 +03:00
parent 928696a6c9
commit 56a35c2e73
2 changed files with 151 additions and 39 deletions

View file

@ -40,10 +40,20 @@ const Card = (props: CardProps) => {
const [pageSizeOptions, setPageSizeOptions] = useState<Option[]>([])
const [gridDataSource, setGridDataSource] = useState<CustomStore<any, any>>()
const { getLookupDataSource } = useLookupDataSource({ listFormCode })
// Memoize getLookupDataSource to prevent recalculation on each render
const memoizedGetLookupDataSource = useCallback(
(editorOptions: any, colData: any, row?: any) => getLookupDataSource(editorOptions, colData, row),
[getLookupDataSource]
)
const [layoutCount, setLayoutCount] = useState(4)
const [searchText, setSearchText] = useState('')
const [prevValue, setPrevValue] = useState('')
const [loading, setLoading] = useState(false)
// Progressive rendering state
const [renderedCount, setRenderedCount] = useState(0)
const [isProgressiveRendering, setIsProgressiveRendering] = useState(false)
const { checkPermission } = usePermission()
const isPwaMode = usePWA()
const [extraFilters, setExtraFilters] = useState<GridExtraFilterState[]>([])
@ -103,7 +113,7 @@ const Card = (props: CardProps) => {
setCurrentPage(page)
}
const toggleSort = (columnName: string) => {
const toggleSort = useCallback((columnName: string) => {
const newParams = new URLSearchParams(urlSearchParams.toString())
if (sortColumn === columnName) {
@ -118,9 +128,9 @@ const Card = (props: CardProps) => {
setUrlSearchParams(newParams)
setCurrentPage(1) // Reset to first page when sorting
}
}, [sortColumn, sortOrder, urlSearchParams])
const handleSelectAll = async (checked: boolean) => {
const handleSelectAll = useCallback(async (checked: boolean) => {
if (checked) {
const keyField = gridDto?.gridOptions.keyFieldName
const selectAllMode = gridDto?.gridOptions?.selectionDto?.selectAllMode?.toLowerCase()
@ -155,9 +165,9 @@ const Card = (props: CardProps) => {
} else {
setSelectedKeys(new Set())
}
}
}, [gridDto, gridDataSource, totalCount, data])
const handleCardSelection = (key: any, checked: boolean) => {
const handleCardSelection = useCallback((key: any, checked: boolean) => {
const newSelection = new Set(selectedKeys)
if (checked) {
if (selectionMode === 'single') {
@ -168,25 +178,26 @@ const Card = (props: CardProps) => {
newSelection.delete(key)
}
setSelectedKeys(newSelection)
}
}, [selectedKeys, selectionMode])
const handleCardClick = (key: any, index: number, event: React.MouseEvent) => {
const handleCardClick = useCallback((key: any, index: number, event: React.MouseEvent) => {
setFocusedCardIndex(index)
if (selectionMode !== 'none' && event.ctrlKey) {
handleCardSelection(key, !selectedKeys.has(key))
}
}
}, [selectionMode, handleCardSelection, selectedKeys])
const handleCardDoubleClick = (key: any, row: any) => {
const handleCardDoubleClick = useCallback((key: any, row: any) => {
// Navigate to edit page on double click
const navigate = window.location
if (!gridDto?.gridOptions.editingOptionDto?.allowUpdating) return
window.open(
ROUTES_ENUM.protected.admin.formEdit
.replace(':listFormCode', listFormCode)
.replace(':id', key),
isPwaMode ? '_self' : '_blank'
)
}
}, [gridDto, listFormCode, isPwaMode])
const handleExport = async (format: 'xlsx' | 'csv' | 'pdf', selectedOnly: boolean = false) => {
try {
@ -288,7 +299,7 @@ const Card = (props: CardProps) => {
}
}
const handleKeyDown = (e: KeyboardEvent) => {
const handleKeyDown = useCallback((e: KeyboardEvent) => {
if (data.length === 0) return
const keyField = gridDto?.gridOptions.keyFieldName
@ -342,7 +353,7 @@ const Card = (props: CardProps) => {
}
break
}
}
}, [data, gridDto, focusedCardIndex, layoutCount, handleCardDoubleClick, selectionMode, handleCardSelection, selectedKeys])
const onFilter = useCallback(
(value?: string) => {
@ -413,6 +424,8 @@ const Card = (props: CardProps) => {
const loadData = useCallback(() => {
if (!gridDataSource) return
setLoading(true)
setIsProgressiveRendering(false)
setRenderedCount(0)
const loadOptions = {
skip: (currentPage - 1) * pageSize,
@ -426,6 +439,12 @@ const Card = (props: CardProps) => {
setData(res.data)
setTotalCount(res.totalCount || 0)
setLoading(false)
// Veri yüklendi, şimdi progressive rendering başlat
if (res.data && res.data.length > 0) {
setIsProgressiveRendering(true)
setRenderedCount(0)
}
})
.catch(() => {
setLoading(false)
@ -442,7 +461,7 @@ const Card = (props: CardProps) => {
setSelectedKeys(new Set())
}
}
}, [gridDataSource, loadData, currentPage, gridDto])
}, [gridDataSource, loadData, currentPage])
useEffect(() => {
if (!gridDto) return
@ -457,10 +476,29 @@ const Card = (props: CardProps) => {
}, [gridDto, listFormCode, searchParams])
useEffect(() => {
if (data.length > 0) {
window.scrollTo({ top: 0, behavior: 'smooth' })
// Sadece yeni data yüklendiginde scroll yap - smooth yerine auto kullanılarak performans artırılıyor
if (data.length > 0 && currentPage === 1) {
window.scrollTo({ top: 0, behavior: 'auto' })
}
}, [data])
}, [currentPage])
// Progressive rendering effect - kartları küçük batch'lerde render et
useEffect(() => {
if (!isProgressiveRendering || renderedCount >= data.length) {
if (renderedCount >= data.length && isProgressiveRendering) {
setIsProgressiveRendering(false)
}
return
}
// Her seferde 3 kart render et - daha küçük batch size daha smooth experience
const batchSize = 3
const timeoutId = setTimeout(() => {
setRenderedCount(prev => Math.min(prev + batchSize, data.length))
}, 10) // 10ms gecikme - UI'ın nefes almasını sağlar
return () => clearTimeout(timeoutId)
}, [isProgressiveRendering, renderedCount, data.length])
if (!gridDto) return null
@ -683,14 +721,15 @@ const Card = (props: CardProps) => {
</p>
</div>
) : (
<div
className="bg-transparent grid gap-4"
style={{
gridTemplateColumns: `repeat(${layoutCount}, minmax(0, 1fr))`
}}
>
{gridDataSource &&
data.map((row, idx) => {
<>
<div
className="bg-transparent grid gap-4"
style={{
gridTemplateColumns: `repeat(${layoutCount}, minmax(0, 1fr))`
}}
>
{gridDataSource &&
data.slice(0, isProgressiveRendering ? renderedCount : data.length).map((row, idx) => {
const keyField = gridDto.gridOptions.keyFieldName
const rowId = row[keyField!]
const isSelected = selectedKeys.has(rowId)
@ -707,7 +746,7 @@ const Card = (props: CardProps) => {
listFormCode={listFormCode}
dataSource={gridDataSource}
refreshData={loadData}
getCachedLookupDataSource={getLookupDataSource}
getCachedLookupDataSource={memoizedGetLookupDataSource}
isSelected={isSelected}
isFocused={isFocused}
isHovered={isHovered}
@ -720,7 +759,18 @@ const Card = (props: CardProps) => {
/>
)
})}
</div>
</div>
{/* Progressive rendering sırasında loading göster */}
{isProgressiveRendering && renderedCount < data.length && (
<div className="text-center py-4">
<div className="inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500"></div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
Yükleniyor... ({renderedCount}/{data.length})
</p>
</div>
)}
</>
)}
{gridDto.gridOptions.pagerOptionDto?.visible && totalCount > pageSize && (

View file

@ -3,7 +3,7 @@ import { useLocalization } from "@/utils/hooks/useLocalization"
import { usePermission } from "@/utils/hooks/usePermission"
import { usePWA } from "@/utils/hooks/usePWA"
import CustomStore from "devextreme/data/custom_store"
import { useMemo, useRef, useState, forwardRef } from "react"
import { useMemo, useRef, useState, forwardRef, memo, useEffect } from "react"
import { useNavigate } from "react-router-dom"
import { GroupItem } from 'devextreme/ui/form'
import { PermissionResults, SimpleItemWithColData } from "../form/types"
@ -66,15 +66,48 @@ const CardItem = forwardRef<HTMLDivElement, CardItemProps>((
}, [gridDto])
const [formData, setFormData] = useState(row)
const refForm = useRef<FormRef>(null)
const cardElementRef = useRef<HTMLDivElement | null>(null)
const navigate = useNavigate()
const { translate } = useLocalization()
const { checkPermission } = usePermission()
const isPwaMode = usePWA()
// Lazy load form with Intersection Observer - sadece görünür kartları render et
const [shouldRenderForm, setShouldRenderForm] = useState(false)
useEffect(() => {
const element = cardElementRef.current
if (!element) return
// Intersection Observer ile görünürlük kontrolü
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// Kart görünür hale geldiğinde form'u render et
setShouldRenderForm(true)
// Bir kez render edildikten sonra observer'ı kaldır
observer.disconnect()
}
})
},
{
rootMargin: '50px', // 50px önceden yüklemeye başla
threshold: 0.01, // %1 görünür olduğunda tetikle
}
)
observer.observe(element)
return () => {
observer.disconnect()
}
}, [])
const keyField = gridDto.gridOptions.keyFieldName
const rowId = row[keyField!]
// Form Items
// Form Items - memoized to prevent recalculation on every render
const formItems: GroupItem[] = useMemo(() => {
if (!gridDto) return []
@ -165,7 +198,16 @@ const CardItem = forwardRef<HTMLDivElement, CardItemProps>((
return (
<div
ref={ref}
ref={(element) => {
// Forward ref to parent
if (typeof ref === 'function') {
ref(element)
} else if (ref) {
ref.current = element
}
// Also save to local ref for Intersection Observer
cardElementRef.current = element
}}
tabIndex={tabIndex}
onClick={onClick}
onDoubleClick={onDoubleClick}
@ -233,14 +275,23 @@ const CardItem = forwardRef<HTMLDivElement, CardItemProps>((
)}
</div>
<div className="flex-grow">
<FormDevExpress
listFormCode={listFormCode}
mode="view"
refForm={refForm}
formData={formData}
formItems={formItems}
setFormData={setFormData}
/>
{shouldRenderForm ? (
<FormDevExpress
listFormCode={listFormCode}
mode="view"
refForm={refForm}
formData={formData}
formItems={formItems}
setFormData={setFormData}
/>
) : (
<div className="p-4">
<div className="animate-pulse space-y-2">
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/2"></div>
</div>
</div>
)}
</div>
</div>
)
@ -248,4 +299,15 @@ const CardItem = forwardRef<HTMLDivElement, CardItemProps>((
CardItem.displayName = 'CardItem'
export default CardItem
// Memoize to prevent unnecessary re-renders when parent re-renders
export default memo(CardItem, (prevProps, nextProps) => {
// Custom comparison to prevent re-render when not needed
return (
prevProps.row === nextProps.row &&
prevProps.isSelected === nextProps.isSelected &&
prevProps.isFocused === nextProps.isFocused &&
prevProps.isHovered === nextProps.isHovered &&
prevProps.gridDto === nextProps.gridDto &&
prevProps.listFormCode === nextProps.listFormCode
)
})