Skip to content

Commit

Permalink
Virtualizer: Reduce state updates and fix intersection ratio detection (
Browse files Browse the repository at this point in the history
  • Loading branch information
Mitch-At-Work authored Oct 25, 2024
1 parent c547276 commit 5b86f90
Show file tree
Hide file tree
Showing 10 changed files with 91 additions and 67 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "fix: Enable virtualizer to fall back to most recent IO event if none intersecting",
"packageName": "@fluentui/react-virtualizer",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export const useDynamicVirtualizerMeasure: <TElement extends HTMLElement>(virtua
bufferSize: number;
scrollRef: (instance: TElement | null) => void;
containerSizeRef: React_2.RefObject<number>;
updateScrollPosition: (scrollPosition: number) => void;
};

// @public
Expand Down Expand Up @@ -145,8 +146,6 @@ export const virtualizerClassNames: SlotClassNames<VirtualizerSlots>;
export type VirtualizerContextProps = {
contextIndex: number;
setContextIndex: (index: number) => void;
contextPosition?: number;
setContextPosition?: (index: number) => void;
childProgressiveSizes?: React_2.MutableRefObject<number[]>;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,7 @@ export type VirtualizerConfigProps = {

/**
* Enables users to override the intersectionObserverRoot.
* RECOMMEND: DO NOT PASS THIS IN, as it can cause side effects
* when overlapping with other scroll views
* We recommend passing this in for accurate distance assessment in IO
*/
scrollViewRef?: React.MutableRefObject<HTMLElement | null>;

Expand Down Expand Up @@ -195,6 +194,12 @@ export type VirtualizerConfigProps = {
* Virtualizer Measure hooks provide a suitable reference.
*/
containerSizeRef: RefObject<number>;

/**
* A callback that enables updating scroll position for calculating required dynamic lengths,
* this should be passed in from useDynamicVirtualizerMeasure
*/
updateScrollPosition?: (position: number) => void;
};

export type VirtualizerProps = ComponentProps<Partial<VirtualizerSlots>> & VirtualizerConfigProps;
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,18 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta
containerSizeRef,
scrollViewRef,
enableScrollLoad,
updateScrollPosition,
} = props;

/* The context is optional, it's useful for injecting additional index logic, or performing uniform state updates*/
const _virtualizerContext = useVirtualizerContextState_unstable(virtualizerContext);

// We use this ref as a constant source to access the virtualizer's state imperatively
const actualIndexRef = useRef<number>(_virtualizerContext.contextIndex);
const flaggedIndex = useRef<number | null>(null);

const flaggedIndex = useRef<number | null>(null);
const actualIndex = _virtualizerContext.contextIndex;

// Just in case our ref gets out of date vs the context during a re-render
if (_virtualizerContext.contextIndex !== actualIndexRef.current) {
actualIndexRef.current = _virtualizerContext.contextIndex;
Expand All @@ -49,10 +51,10 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta
);

// Store ref to before padding element
const beforeElementRef = useRef<Element | null>(null);
const beforeElementRef = useRef<HTMLElement | null>(null);

// Store ref to before padding element
const afterElementRef = useRef<Element | null>(null);
const afterElementRef = useRef<HTMLElement | null>(null);

// We need to store an array to track dynamic sizes, we can use this to incrementally update changes
const childSizes = useRef<number[]>(new Array<number>(getItemSize ? numItems : 0));
Expand Down Expand Up @@ -306,18 +308,17 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta
return;
}

// Grab latest entry that is intersecting
const latestEntry =
entries.length === 1
? entries[0]
: entries
.sort((entry1, entry2) => entry2.time - entry1.time)
.find(entry => {
return entry.intersectionRatio > 0;
});

if (!latestEntry || !latestEntry.isIntersecting) {
// If we don't find an intersecting area, ignore for now.
if (entries.length === 0) {
// No entries found, return.
return;
}
// Find the latest entry that is intersecting
const sortedEntries = entries.sort((entry1, entry2) => entry2.time - entry1.time);
const latestEntry = sortedEntries.find(entry => {
return entry.isIntersecting;
});

if (!latestEntry) {
return;
}

Expand Down Expand Up @@ -364,14 +365,15 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta
} else if (latestEntry.target === beforeElementRef.current) {
// Get before buffers position
measurementPos = calculateBefore();

// Get exact intersection position based on overflow size (how far into window did we scroll IO?)
const overflowAmount =
axis === 'vertical' ? latestEntry.intersectionRect.height : latestEntry.intersectionRect.width;

// Minus from original before position
measurementPos -= overflowAmount;
// Ignore buffer size (IO offset)
measurementPos += bufferSize;

// Calculate how far past the window bounds we are (this will be zero if IO is within window)
const hOverflow = latestEntry.boundingClientRect.bottom - latestEntry.intersectionRect.bottom;
const hOverflowReversed = latestEntry.boundingClientRect.top - latestEntry.intersectionRect.top;
Expand Down Expand Up @@ -400,30 +402,34 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta

// Safety limits
const newStartIndex = Math.min(Math.max(startIndex, 0), maxIndex);

flushSync(() => {
_virtualizerContext.setContextPosition(measurementPos);
// Callback to allow measure functions to check virtualizer length
if (newStartIndex + virtualizerLength >= numItems && actualIndex + virtualizerLength >= numItems) {
// We've already hit the end, no need to update state.
return;
}
updateScrollPosition?.(measurementPos);
if (actualIndex !== newStartIndex) {
batchUpdateNewIndex(newStartIndex);
}
});
},
[
_virtualizerContext,
actualIndex,
virtualizerLength,
axis,
batchUpdateNewIndex,
bufferItems,
reversed,
numItems,
bufferSize,
bufferItems,
containerSizeRef,
updateScrollPosition,
batchUpdateNewIndex,
calculateAfter,
calculateBefore,
calculateTotalSize,
containerSizeRef,
getIndexFromScrollPosition,
numItems,
reversed,
updateCurrentItemSizes,
virtualizerLength,
],
),
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,16 @@ export function useVirtualizerScrollViewDynamic_unstable(
[sizeTrackingArray, props.itemSize, sizeUpdateCount],
);

const { virtualizerLength, bufferItems, bufferSize, scrollRef, containerSizeRef } = useDynamicVirtualizerMeasure({
defaultItemSize: props.itemSize,
direction: props.axis ?? 'vertical',
getItemSize: props.getItemSize ?? getChildSizeAuto,
virtualizerContext: contextState,
numItems: props.numItems,
bufferItems: _bufferItems,
bufferSize: _bufferSize,
});
const { virtualizerLength, bufferItems, bufferSize, scrollRef, containerSizeRef, updateScrollPosition } =
useDynamicVirtualizerMeasure({
defaultItemSize: props.itemSize,
direction: props.axis ?? 'vertical',
getItemSize: props.getItemSize ?? getChildSizeAuto,
virtualizerContext: contextState,
numItems: props.numItems,
bufferItems: _bufferItems,
bufferSize: _bufferSize,
});

const _imperativeVirtualizerRef = useMergedRefs(React.useRef<VirtualizerDataRef>(null), imperativeVirtualizerRef);

Expand All @@ -76,7 +77,7 @@ export function useVirtualizerScrollViewDynamic_unstable(
if (virtualizerLengthRef.current !== virtualizerLength) {
virtualizerLengthRef.current = virtualizerLength;
}
const scrollViewRef = useMergedRefs(props.scrollViewRef, scrollRef, paginationRef) as React.RefObject<HTMLDivElement>;
const scrollViewRef = useMergedRefs(props.scrollViewRef, scrollRef, paginationRef);
const scrollCallbackRef = React.useRef<null | ((index: number) => void)>(null);

useImperativeHandle(
Expand All @@ -97,7 +98,7 @@ export function useVirtualizerScrollViewDynamic_unstable(
index,
itemSizes: _imperativeVirtualizerRef.current?.nodeSizes,
totalSize,
scrollViewRef,
scrollViewRef: scrollViewRef as React.RefObject<HTMLDivElement>,
axis,
reversed,
behavior,
Expand Down Expand Up @@ -127,6 +128,8 @@ export function useVirtualizerScrollViewDynamic_unstable(
imperativeVirtualizerRef: _imperativeVirtualizerRef,
onRenderedFlaggedIndex: handleRenderedIndex,
containerSizeRef,
scrollViewRef,
updateScrollPosition,
});

const measureObject = useMeasureList(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { useIsomorphicLayoutEffect, useMergedRefs } from '@fluentui/react-utilit
import * as React from 'react';
import { VirtualizerMeasureDynamicProps } from './hooks.types';
import { useResizeObserverRef_unstable } from './useResizeObserverRef';
import { useRef } from 'react';
import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';

/**
Expand All @@ -16,6 +15,7 @@ export const useDynamicVirtualizerMeasure = <TElement extends HTMLElement>(
bufferSize: number;
scrollRef: (instance: TElement | null) => void;
containerSizeRef: React.RefObject<number>;
updateScrollPosition: (scrollPosition: number) => void;
} => {
const {
defaultItemSize,
Expand All @@ -26,8 +26,6 @@ export const useDynamicVirtualizerMeasure = <TElement extends HTMLElement>(
bufferSize,
virtualizerContext,
} = virtualizerProps;
const indexRef = useRef<number>(virtualizerContext.contextIndex);
indexRef.current = virtualizerContext.contextIndex;

const [state, setState] = React.useState({
virtualizerLength: 0,
Expand All @@ -36,6 +34,7 @@ export const useDynamicVirtualizerMeasure = <TElement extends HTMLElement>(
});

const containerSizeRef = React.useRef<number>(0);
const scrollPosition = React.useRef<number>(0);
const { virtualizerLength, virtualizerBufferItems, virtualizerBufferSize } = state;

const { targetDocument } = useFluent();
Expand Down Expand Up @@ -63,18 +62,20 @@ export const useDynamicVirtualizerMeasure = <TElement extends HTMLElement>(
let i = 0;
let length = 0;

const startIndex = virtualizerContext.contextIndex;
const sizeToBeat = containerSizeRef.current + virtualizerBufferSize * 2;
while (indexSizer <= sizeToBeat && i + virtualizerContext.contextIndex < numItems) {
const iItemSize = getItemSize(indexRef.current + i);

while (indexSizer <= sizeToBeat && i + startIndex < numItems) {
const iItemSize = getItemSize(startIndex + i);
if (virtualizerContext.childProgressiveSizes.current.length < numItems) {
/* We are in unknown territory, either an initial render or an update
in virtualizer item length has occurred.
We need to let the new items render first then we can accurately assess.*/
return virtualizerLength - virtualizerBufferSize * 2;
}

const currentScrollPos = virtualizerContext.contextPosition;
const currentItemPos = virtualizerContext.childProgressiveSizes.current[indexRef.current + i] - iItemSize;
const currentScrollPos = scrollPosition.current;
const currentItemPos = virtualizerContext.childProgressiveSizes.current[startIndex + i] - iItemSize;

if (currentScrollPos > currentItemPos + iItemSize) {
// The item isn't in view, ignore for now.
Expand Down Expand Up @@ -102,8 +103,9 @@ export const useDynamicVirtualizerMeasure = <TElement extends HTMLElement>(
/*
* This is how far we deviate into the bufferItems to detect a redraw.
*/
const newBufferSize = bufferSize ?? Math.max(defaultItemSize / 2.0, 1);
const newBufferSize = bufferSize ?? Math.max(defaultItemSize / 2, 1);
const totalLength = length + newBufferItems * 2;

setState({
virtualizerLength: totalLength,
virtualizerBufferSize: newBufferSize,
Expand All @@ -122,7 +124,6 @@ export const useDynamicVirtualizerMeasure = <TElement extends HTMLElement>(
virtualizerBufferSize,
virtualizerContext.childProgressiveSizes,
virtualizerContext.contextIndex,
virtualizerContext.contextPosition,
virtualizerLength,
],
);
Expand Down Expand Up @@ -151,11 +152,21 @@ export const useDynamicVirtualizerMeasure = <TElement extends HTMLElement>(
}
}, [handleScrollResize, numItems, virtualizerContext.contextIndex, virtualizerLength]);

const updateScrollPosition = React.useCallback(
(_scrollPosition: number) => {
scrollPosition.current = _scrollPosition;
// Check if our vLength's need recalculating
handleScrollResize(scrollRef);
},
[handleScrollResize, scrollRef],
);

return {
virtualizerLength,
bufferItems: virtualizerBufferItems,
bufferSize: virtualizerBufferSize,
scrollRef,
containerSizeRef,
updateScrollPosition,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export const useVirtualizerContextState_unstable = (
): DynamicVirtualizerContextProps => {
const virtualizerContext = useVirtualizerContext_unstable();
const [_contextIndex, _setContextIndex] = useState<number>(-1);
const [_contextPosition, _setContextPosition] = useState<number>(0);
const childProgressiveSizes = useRef<number[]>([]);

/* We respect any wrapped providers while also ensuring defaults or passed through
Expand All @@ -27,12 +26,9 @@ export const useVirtualizerContextState_unstable = (
() => ({
contextIndex: passedContext?.contextIndex ?? virtualizerContext?.contextIndex ?? _contextIndex,
setContextIndex: passedContext?.setContextIndex ?? virtualizerContext?.setContextIndex ?? _setContextIndex,
contextPosition: passedContext?.contextPosition ?? virtualizerContext?.contextPosition ?? _contextPosition,
setContextPosition:
passedContext?.setContextPosition ?? virtualizerContext?.setContextPosition ?? _setContextPosition,
childProgressiveSizes,
}),
[_contextIndex, _contextPosition, passedContext, virtualizerContext],
[_contextIndex, passedContext, virtualizerContext],
);

return context;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@ export type VirtualizerContextProps = {
contextIndex: number;
setContextIndex: (index: number) => void;
/*
* These option props are used in dynamic virtualizer
* These optional props are used in dynamic virtualizer
*/
contextPosition?: number;
setContextPosition?: (index: number) => void;
childProgressiveSizes?: React.MutableRefObject<number[]>;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ const useStyles = makeStyles({

export const Dynamic = () => {
const [currentIndex, setCurrentIndex] = React.useState(-1);
const [currentPosition, setCurrentPosition] = React.useState(0);
const childProgressiveSizes = React.useRef<number[]>([]);
const [flag, toggleFlag] = React.useState(false);
const styles = useStyles();
Expand Down Expand Up @@ -65,17 +64,16 @@ export const Dynamic = () => {
const contextState: DynamicVirtualizerContextProps = {
contextIndex: currentIndex,
setContextIndex: setCurrentIndex,
contextPosition: currentPosition,
setContextPosition: setCurrentPosition,
childProgressiveSizes,
};

const { virtualizerLength, bufferItems, bufferSize, scrollRef, containerSizeRef } = useDynamicVirtualizerMeasure({
defaultItemSize: 100,
getItemSize: getSizeForIndex,
numItems: childLength,
virtualizerContext: contextState,
});
const { virtualizerLength, bufferItems, bufferSize, scrollRef, containerSizeRef, updateScrollPosition } =
useDynamicVirtualizerMeasure({
defaultItemSize: 100,
getItemSize: getSizeForIndex,
numItems: childLength,
virtualizerContext: contextState,
});

return (
<VirtualizerContextProvider value={contextState}>
Expand All @@ -89,6 +87,7 @@ export const Dynamic = () => {
itemSize={100}
containerSizeRef={containerSizeRef}
virtualizerContext={contextState}
updateScrollPosition={updateScrollPosition}
>
{useCallback(
(index: number) => {
Expand Down
Loading

0 comments on commit 5b86f90

Please sign in to comment.