Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ class RenderTaskQueue {

private timeout: NodeJS.Timeout | null = null;

private onIsRenderingChange?: (isRendering: boolean) => void;

constructor(onIsRenderingChange?: (isRendering: boolean) => void) {
this.onIsRenderingChange = onIsRenderingChange;
}

add(info: RenderInfo) {
this.renderInfos.push(info);

Expand All @@ -30,15 +36,18 @@ class RenderTaskQueue {
return;
}
clearTimeout(this.timeout);
this.onIsRenderingChange?.(false);
}

private render() {
const info = this.renderInfos.shift();
if (!info) {
this.isRendering = false;
this.onIsRenderingChange?.(false);
return;
}
this.isRendering = true;
this.onIsRenderingChange?.(true);

this.handler(info);

Expand Down
10 changes: 8 additions & 2 deletions src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ function BaseInvertedFlatList<T>({ref, ...props}: BaseInvertedFlatListProps<T>)
return null;
});
const [isInitialData, setIsInitialData] = useState(true);
const [isQueueRendering, setIsQueueRendering] = useState(false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where do you set this state? I don't see where it ever reaches any other state than the default

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its passed into RenderTaskQueue and there its being updated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh my mistake, thanks


const currentDataIndex = useMemo(() => (currentDataId === null ? 0 : data.findIndex((item, index) => keyExtractor(item, index) === currentDataId)), [currentDataId, data, keyExtractor]);
const displayedData = useMemo(() => {
if (currentDataIndex <= 0) {
Expand All @@ -56,7 +58,7 @@ function BaseInvertedFlatList<T>({ref, ...props}: BaseInvertedFlatListProps<T>)
const dataIndexDifference = data.length - displayedData.length;

// Queue up updates to the displayed data to avoid adding too many at once and cause jumps in the list.
const renderQueue = useMemo(() => new RenderTaskQueue(), []);
const renderQueue = useMemo(() => new RenderTaskQueue(setIsQueueRendering), []);
useEffect(() => {
return () => {
renderQueue.cancel();
Expand Down Expand Up @@ -88,6 +90,10 @@ function BaseInvertedFlatList<T>({ref, ...props}: BaseInvertedFlatListProps<T>)
);

const maintainVisibleContentPosition = useMemo(() => {
if (!initialScrollKey && (!isInitialData || !isQueueRendering)) {
return undefined;
}

const config: ScrollViewProps['maintainVisibleContentPosition'] = {
// This needs to be 1 to avoid using loading views as anchors.
minIndexForVisible: data.length ? Math.min(1, data.length - 1) : 0,
Expand All @@ -98,7 +104,7 @@ function BaseInvertedFlatList<T>({ref, ...props}: BaseInvertedFlatListProps<T>)
}

return config;
}, [data.length, shouldEnableAutoScrollToTopThreshold, isLoadingData, wasLoadingData]);
}, [initialScrollKey, isInitialData, isQueueRendering, data.length, shouldEnableAutoScrollToTopThreshold, isLoadingData, wasLoadingData]);

const listRef = useRef<RNFlatList | null>(null);
useImperativeHandle(ref, () => {
Expand Down
51 changes: 51 additions & 0 deletions tests/unit/RenderTaskQueueTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import RenderTaskQueue from '../../src/components/InvertedFlatList/BaseInvertedFlatList/RenderTaskQueue';

jest.unmock('../../src/components/InvertedFlatList/BaseInvertedFlatList/RenderTaskQueue');

describe('RenderTaskQueue', () => {
beforeEach(() => {
jest.useFakeTimers();
});

afterEach(() => {
jest.clearAllTimers();
jest.useRealTimers();
});

describe('notifyRenderingStateChange callback', () => {
it('should notify rendering state changes when a task completes naturally to track the rendering lifecycle', () => {
// Given a RenderTaskQueue with an isRendering change callback
const mockOnIsRenderingChange = jest.fn();
const queue = new RenderTaskQueue(mockOnIsRenderingChange);

// When a task is added and allowed to complete
queue.add({distanceFromStart: 100});

// Then the callback is invoked with true when rendering starts
expect(mockOnIsRenderingChange).toHaveBeenCalledWith(true);
jest.advanceTimersByTime(500);

// Then the callback is invoked with false when rendering completes
expect(mockOnIsRenderingChange).toHaveBeenCalledTimes(2);
expect(mockOnIsRenderingChange).toHaveBeenCalledWith(false);
});

it('should notify rendering state changes when a task is canceled to ensure proper cleanup', () => {
// Given a RenderTaskQueue with an isRendering change callback
const mockOnIsRenderingChange = jest.fn();
const queue = new RenderTaskQueue(mockOnIsRenderingChange);

// When a task is added but canceled before completion
queue.add({distanceFromStart: 100});
queue.cancel();

// Then the callback is invoked with true when rendering starts
expect(mockOnIsRenderingChange).toHaveBeenCalledWith(true);
jest.advanceTimersByTime(500);

// Then the callback is invoked with false even after canceling to ensure proper cleanup
expect(mockOnIsRenderingChange).toHaveBeenCalledTimes(2);
expect(mockOnIsRenderingChange).toHaveBeenCalledWith(false);
});
});
});
Loading