Skip to content

Commit 7f8f5a5

Browse files
committed
time table - updated rendermodell to make sure the renderings are done and not optimized out by react fiber
1 parent 930880e commit 7f8f5a5

File tree

5 files changed

+172
-108
lines changed

5 files changed

+172
-108
lines changed

library/src/components/timetable/TimeTableRows.tsx

Lines changed: 78 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type React from "react"
33
import {
44
createRef,
55
type MouseEvent,
6+
type MutableRefObject,
67
useCallback,
78
useLayoutEffect,
89
useMemo,
@@ -76,8 +77,8 @@ function renderGroupRows<G extends TimeTableGroup, I extends TimeSlotBooking>(
7677
g: number,
7778
refCollection: React.MutableRefObject<HTMLElement>[],
7879
groupRowsRendered: JSX.Element[],
79-
renderedCells: Set<number>,
80-
changedGroupRows: Set<number>,
80+
renderedGroupsRef: MutableRefObject<Set<number>>,
81+
changedGroupRowsRef: MutableRefObject<Set<number>>,
8182
onGroupClick: ((_: G) => void) | undefined,
8283
placeHolderHeight: number,
8384
columnWidth: number,
@@ -107,7 +108,7 @@ function renderGroupRows<G extends TimeTableGroup, I extends TimeSlotBooking>(
107108
groupEntry,
108109
groupRows,
109110
groupEntriesArray,
110-
changedGroupRows,
111+
changedGroupRowsRef,
111112
renderCells,
112113
)
113114
throw new Error("TimeTable - group entry not found")
@@ -130,16 +131,10 @@ function renderGroupRows<G extends TimeTableGroup, I extends TimeSlotBooking>(
130131
refCollection[g] = mref
131132
}
132133
const rendering = g >= renderCells[0] && g <= renderCells[1]
133-
if (rendering) {
134-
renderedCells.add(g)
135-
} else {
136-
renderedCells.delete(g)
137-
}
138-
changedGroupRows.delete(g)
139134

140135
groupRowsRendered[g] = (
141136
<GroupRows<G, I>
142-
key={`${groupEntry.title}${g}-${rendering}`}
137+
key={`${g}-${groupEntry.id}-${rendering}`}
143138
group={groupEntry}
144139
groupNumber={g}
145140
itemRows={rows}
@@ -154,6 +149,10 @@ function renderGroupRows<G extends TimeTableGroup, I extends TimeSlotBooking>(
154149
slotsArray={slotsArray}
155150
timeFrameDay={timeFrameDay}
156151
viewType={viewType}
152+
// side effect props
153+
renderedGroupsRef={renderedGroupsRef}
154+
changedGroupRowsRef={changedGroupRowsRef}
155+
//
157156
/>
158157
)
159158
}
@@ -165,7 +164,6 @@ export default function TimeTableRows<
165164
G extends TimeTableGroup,
166165
I extends TimeSlotBooking,
167166
>({
168-
//entries,
169167
groupRows,
170168
onGroupClick,
171169
onTimeSlotItemClick,
@@ -200,18 +198,21 @@ export default function TimeTableRows<
200198
const changedGroupRows = useRef<Set<number>>(new Set<number>())
201199

202200
// groupRowsRendered is the array of rendered group rows JSX Elements, which is returned from the component
203-
const groupRowsRendered = useRef<JSX.Element[]>([])
201+
//const groupRowsRendered = useRef<JSX.Element[]>([])
202+
const [groupRowsRendered, setGroupRowsRendered] = useState<JSX.Element[]>(
203+
[],
204+
)
204205

205206
const slotsArrayCurrent = useRef(slotsArray)
206207
const viewTypeCurrent = useRef(viewType)
207208
const timeFrameDayCurrent = useRef(timeFrameDay)
208209

209210
// groupRowsRenderedIdx is the index of the group row which is currently rendered using batch rendering
210-
const [groupRowsRenderedIdx, setGroupRowsRenderedIdx] = useState(-1)
211+
//const [groupRowsRenderedIdx, setGroupRowsRenderedIdx] = useState(-1)
211212
// this is a reference to the current groupRowsRenderedIdx to avoid changing the handleIntersections callback on groupRowsRenderedIdx change
212213
// and to know how far we are with the initial rendering... this is needed to know when to start the intersection observer
213214
// and it should be only set to 0 when the group rows change
214-
const groupRowsRenderedIdxRef = useRef(groupRowsRenderedIdx)
215+
//const groupRowsRenderedIdxRef = useRef(groupRowsRenderedIdx)
215216

216217
if (
217218
slotsArrayCurrent.current !== slotsArray ||
@@ -223,13 +224,13 @@ export default function TimeTableRows<
223224
slotsArrayCurrent.current = slotsArray
224225
viewTypeCurrent.current = viewType
225226
timeFrameDayCurrent.current = timeFrameDay
226-
setGroupRowsRenderedIdx(-1)
227-
groupRowsRenderedIdxRef.current = 0
227+
setGroupRowsRendered([])
228228
}
229229

230230
const rateLimiterIntersection = useIdleRateLimitHelper(renderIdleTimeout)
231231
const rateLimiterRendering = useIdleRateLimitHelper(renderIdleTimeout)
232232
const debounceIntersection = useDebounceHelper(intersectionStackDelay)
233+
const allRenderedPlaceholderCommitedToDOM = useRef(false)
233234

234235
// handle intersection is called after intersectionStackDelay ms to avoid too many calls
235236
// and checks which groups are intersecting with the intersection container.
@@ -253,8 +254,10 @@ export default function TimeTableRows<
253254
const bottom = intersectionbb.bottom + rowsMargin * rowHeight
254255

255256
// find the last rendered group row
256-
let lastIdx = groupRowsRenderedIdxRef.current
257+
let lastIdx = refCollection.current.length - 1
258+
allRenderedPlaceholderCommitedToDOM.current = true
257259
while (!refCollection.current[lastIdx]?.current && lastIdx > 0) {
260+
allRenderedPlaceholderCommitedToDOM.current = false
258261
lastIdx--
259262
}
260263

@@ -338,6 +341,7 @@ export default function TimeTableRows<
338341
renderGroupRangeRef.current = newRenderCells
339342
return newRenderCells
340343
}
344+
//console.log("TimeTable - intersected group rows not changed", prev)
341345
return prev
342346
})
343347
}, [intersectionContainerRef.current, headerRef.current, rowHeight])
@@ -351,25 +355,20 @@ export default function TimeTableRows<
351355
setCurrentGroupRows((currentGroupRows) => {
352356
changedGroupRows.current.clear()
353357
if (!groupRows) {
354-
setGroupRowsRenderedIdx(-1)
355-
groupRowsRenderedIdxRef.current = 0
356-
groupRowsRendered.current = []
357358
renderedGroups.current.clear()
358359
refCollection.current = []
360+
setGroupRowsRendered([])
359361
console.log("TimeTable - group rows are null")
360362
return groupRows
361363
}
362364

363-
if (groupRowsRendered.current.length > groupRows.size) {
365+
if (groupRowsRendered.length > groupRows.size) {
364366
// shorten and remove rendered elements array, if too long
365367
console.info(
366-
`Timetable - shorten rendered elements array from ${groupRowsRendered.current.length} to ${groupRows.size}`,
368+
`Timetable - shorten rendered elements array from ${groupRowsRendered.length} to ${groupRows.size}`,
367369
)
368-
groupRowsRendered.current.length = groupRows.size
370+
setGroupRowsRendered(groupRowsRendered.slice(0, groupRows.size))
369371
refCollection.current.length = groupRows.size
370-
if (groupRowsRenderedIdxRef.current >= groupRows.size) {
371-
groupRowsRenderedIdxRef.current = groupRows.size - 1
372-
}
373372
}
374373

375374
// determine when new ones start
@@ -410,6 +409,9 @@ export default function TimeTableRows<
410409
if (updateCounter) {
411410
console.log(
412411
`TimeTable - group rows require updated rendering ${updateCounter}, with first ${changedFound}`,
412+
renderGroupRangeRef.current,
413+
currentGroupRowsRef.current.size,
414+
groupRows.size,
413415
)
414416
}
415417
currentGroupRowsRef.current = groupRows
@@ -454,7 +456,8 @@ export default function TimeTableRows<
454456

455457
//** ------- RENDERING ------ */
456458
const renderBatch = useCallback(() => {
457-
setGroupRowsRenderedIdx((groupRowsRenderedIdx) => {
459+
setGroupRowsRendered((groupRowsRenderedPrev) => {
460+
const groupRowsRendered = [...groupRowsRenderedPrev]
458461
if (changedGroupRows.current.size) {
459462
let counter = 0
460463
if (renderGroupRangeRef.current[0] > -1) {
@@ -474,9 +477,9 @@ export default function TimeTableRows<
474477
currentGroupRowsRef.current,
475478
i,
476479
refCollection.current,
477-
groupRowsRendered.current,
478-
renderedGroups.current,
479-
changedGroupRows.current,
480+
groupRowsRendered,
481+
renderedGroups,
482+
changedGroupRows,
480483
onGroupClick,
481484
placeHolderHeight,
482485
columnWidth,
@@ -489,15 +492,15 @@ export default function TimeTableRows<
489492
)
490493
counter++
491494
if (counter > timeTableGroupRenderBatchSize) {
492-
return groupRowsRenderedIdx - 1
495+
return groupRowsRendered
493496
}
494497
}
495498
}
496499
}
497500
for (const g of changedGroupRows.current) {
498501
if (
499502
g > currentGroupRowsRef.current.size - 1 ||
500-
g > groupRowsRenderedIdxRef.current
503+
g > groupRowsRendered.length - 1
501504
) {
502505
changedGroupRows.current.delete(g)
503506
continue
@@ -508,9 +511,9 @@ export default function TimeTableRows<
508511
currentGroupRowsRef.current,
509512
g,
510513
refCollection.current,
511-
groupRowsRendered.current,
512-
renderedGroups.current,
513-
changedGroupRows.current,
514+
groupRowsRendered,
515+
renderedGroups,
516+
changedGroupRows,
514517
onGroupClick,
515518
placeHolderHeight,
516519
columnWidth,
@@ -523,26 +526,24 @@ export default function TimeTableRows<
523526
)
524527
counter++
525528
if (counter > timeTableGroupRenderBatchSize) {
526-
return groupRowsRenderedIdx - 1
529+
return groupRowsRendered
527530
}
528531
}
529532
}
530533

531-
//normal placeholder rendering
532-
let ret = groupRowsRendered.current.length
533534
let counter = 0
534535
while (
535-
ret < currentGroupRowsRef.current.size &&
536+
groupRowsRendered.length < currentGroupRowsRef.current.size &&
536537
counter < timeTableGroupRenderBatchSize
537538
) {
538539
renderGroupRows(
539540
renderGroupRangeRef.current,
540541
currentGroupRowsRef.current,
541-
ret,
542+
groupRowsRendered.length,
542543
refCollection.current,
543-
groupRowsRendered.current,
544-
renderedGroups.current,
545-
changedGroupRows.current,
544+
groupRowsRendered,
545+
renderedGroups,
546+
changedGroupRows,
546547
onGroupClick,
547548
placeHolderHeight,
548549
columnWidth,
@@ -554,11 +555,9 @@ export default function TimeTableRows<
554555
viewType,
555556
)
556557
++counter
557-
++ret
558558
}
559-
groupRowsRenderedIdxRef.current = ret
560559
rateLimiterIntersection(handleIntersections)
561-
return ret
560+
return groupRowsRendered
562561
})
563562
}, [
564563
onGroupClick,
@@ -576,14 +575,26 @@ export default function TimeTableRows<
576575

577576
if (
578577
changedGroupRows.current.size ||
579-
groupRowsRenderedIdx < groupRows.size - 1 ||
578+
groupRowsRendered.length < groupRows.size ||
580579
renderedGroups.current.size <
581580
renderGroupRangeRef.current[1] - renderGroupRangeRef.current[0] + 1
582581
) {
583582
rateLimiterRendering(renderBatch)
583+
} else {
584+
// final rendering and intersection once all groups are rendered and committed to the DOM
585+
if (!allRenderedPlaceholderCommitedToDOM.current) {
586+
rateLimiterIntersection(handleIntersections)
587+
rateLimiterRendering(renderBatch)
588+
// need to use flush sync in case of only very very few groups that we really render all groups
589+
// handleIntersections sets the allRenderedPlaceholderCommitedToDOM to true if all placeholders are found
590+
/*window.setTimeout(() => {
591+
rateLimiterIntersection(() => flushSync(handleIntersections))
592+
rateLimiterRendering(() => flushSync(renderBatch))
593+
}, 0)*/
594+
}
584595
}
585596

586-
return groupRowsRendered.current
597+
return groupRowsRendered
587598
}
588599

589600
/**
@@ -963,6 +974,9 @@ type GroupRowsProps<G extends TimeTableGroup, I extends TimeSlotBooking> = {
963974
slotsArray: readonly Dayjs[]
964975
timeFrameDay: TimeFrameDay
965976
viewType: TimeTableViewType
977+
// this is a side effect to make sure that only the rendered groups are are set, because React sometimes optimizies the rendering out (so I have to keep track of the rendered groups in the actual invocation)
978+
renderedGroupsRef: React.MutableRefObject<Set<number>>
979+
changedGroupRowsRef: React.MutableRefObject<Set<number>>
966980
}
967981

968982
function GroupRows<G extends TimeTableGroup, I extends TimeSlotBooking>({
@@ -980,7 +994,20 @@ function GroupRows<G extends TimeTableGroup, I extends TimeSlotBooking>({
980994
timeFrameDay,
981995
viewType,
982996
mref,
997+
// ugly side effect props
998+
renderedGroupsRef,
999+
changedGroupRowsRef,
1000+
//
9831001
}: GroupRowsProps<G, I>) {
1002+
// ugly SIDE EFFECTs for now to make sure the rendered groups are set
1003+
changedGroupRowsRef.current.delete(groupNumber)
1004+
if (renderCells) {
1005+
renderedGroupsRef.current.add(groupNumber)
1006+
} else {
1007+
renderedGroupsRef.current.delete(groupNumber)
1008+
}
1009+
//
1010+
9841011
const storeIdent = useTimeTableIdent()
9851012
const timeSlotSelectionDisabled =
9861013
useTTCTimeSlotSelectionDisabled(storeIdent)
@@ -1039,6 +1066,7 @@ function GroupRows<G extends TimeTableGroup, I extends TimeSlotBooking>({
10391066
}`
10401067
: undefined
10411068
}
1069+
key={`${group.id}_h_${renderCells}`}
10421070
>
10431071
{renderCells && (
10441072
<div
@@ -1133,7 +1161,7 @@ function GroupRows<G extends TimeTableGroup, I extends TimeSlotBooking>({
11331161

11341162
tds.push(
11351163
<TableCell<G, I>
1136-
key={`${groupNumber}-${timeSlotNumber}-${viewType}`}
1164+
key={`${group.id}-${groupNumber}-${timeSlotNumber}-${viewType}`}
11371165
timeSlotNumber={timeSlotNumber}
11381166
isLastGroupRow={r === rowCount - 1}
11391167
isFirstRow={r === 0}
@@ -1174,6 +1202,7 @@ function GroupRows<G extends TimeTableGroup, I extends TimeSlotBooking>({
11741202
}}
11751203
data-group-id={group.id}
11761204
data-test={`unrendered-table-row_${group.id}`}
1205+
key={`unrendered-table-row_${group.id}`}
11771206
ref={mref as React.Ref<HTMLTableRowElement>}
11781207
className="box-border m-0"
11791208
>

library/src/utils/idleRateLimit.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export function idleRateLimitHelper(
4949
* The idleRateLimitHelper returns a function, which you can call to execute a callback function.
5050
* However, the callback function will only be executed if the time elapsed since the last call is greater than the specified minimum distance.
5151
* @param minDistanceMS the minimal elapsed time between two calls.
52+
* @param executeAfter if true, the callback will be executed using a timeout after the minimum distance has passed, to make sure (default: true).
5253
* @returns a function that takes a callback function as a parameter.
5354
*/
5455
export function useIdleRateLimitHelper(

0 commit comments

Comments
 (0)