Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions src/__tests__/ViewHolderCollection.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React, { createRef } from "react";
import { Text } from "react-native";
import "@quilted/react-testing/matchers";
import { render } from "@quilted/react-testing";

import {
ViewHolderCollection,
ViewHolderCollectionRef,
} from "../recyclerview/ViewHolderCollection";
import { RVLayout } from "../recyclerview/layout-managers/LayoutManager";

/**
* Renders a ViewHolderCollection whose layout table covers only the first
* `layoutCount` indices, mirroring the window between a layout-table shrink
* and the render-stack prune that follows it.
*/
const renderCollection = (config: {
dataLength: number;
renderStackIndices: number[];
layoutCount: number;
}) => {
const { dataLength, renderStackIndices, layoutCount } = config;

const data = Array.from(
{ length: dataLength },
(_, index) => `item-${index}`
);
const renderStack = new Map(
renderStackIndices.map((index) => [`key-${index}`, { index }])
);

const getLayout = (index: number): RVLayout | undefined =>
index < layoutCount
? { x: 0, y: index * 50, width: 400, height: 50 }
: undefined;

return render(
<ViewHolderCollection
data={data}
renderStack={renderStack}
getLayout={getLayout}
viewHolderCollectionRef={createRef<ViewHolderCollectionRef>()}
refHolder={new Map()}
onSizeChanged={jest.fn()}
renderItem={({ item }) => <Text>{String(item)}</Text>}
extraData={undefined}
getChildContainerLayout={() => ({ width: 400, height: 500 })}
onCommitLayoutEffect={jest.fn()}
onCommitEffect={jest.fn()}
horizontal={false}
getAdjustmentMargin={() => 0}
currentStickyIndex={-1}
hideStickyHeaderRelatedCell={false}
isInLastRow={() => false}
inverted={false}
/>
);
};

describe("ViewHolderCollection", () => {
it("renders every entry while the layout table covers the render stack", () => {
const collection = renderCollection({
dataLength: 10,
renderStackIndices: [0, 1, 2, 3, 4],
layoutCount: 10,
});

expect(collection.findAll(Text)).toHaveLength(5);
});

it("renders only the entries that still have a layout after a shrink", () => {
// The stack still holds indices up to 24 from before the shrink, while
// data and layouts have already been truncated to 10. Without the guard
// the stale entries render as empty rows off the end of the list.
const collection = renderCollection({
dataLength: 10,
renderStackIndices: Array.from({ length: 25 }, (_, index) => index),
layoutCount: 10,
});

expect(collection.findAll(Text).map((node) => node.text)).toStrictEqual(
Array.from({ length: 10 }, (_, index) => `item-${index}`)
);
});
});
2 changes: 1 addition & 1 deletion src/recyclerview/RecyclerView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ const RecyclerViewComponent = <T,>(
data={data}
horizontal={horizontal}
renderStack={recyclerViewManager.getRenderStack()}
getLayout={(index) => recyclerViewManager.getLayout(index)}
getLayout={(index) => recyclerViewManager.tryGetLayout(index)}
getAdjustmentMargin={() => {
if (!shouldRenderFromBottom || !recyclerViewManager.hasLayout()) {
return 0;
Expand Down
16 changes: 13 additions & 3 deletions src/recyclerview/ViewHolderCollection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ export interface ViewHolderCollectionProps<TItem> {
data: FlashListProps<TItem>["data"];
/** Map of indices to React keys for each rendered item */
renderStack: Map<string, { index: number }>;
/** Function to get layout information for a specific index */
getLayout: (index: number) => RVLayout;
/** Function to get layout information for a specific index, or undefined
* when the index has no layout yet */
getLayout: (index: number) => RVLayout | undefined;
/** Ref to control layout updates from parent components */
viewHolderCollectionRef: React.Ref<ViewHolderCollectionRef>;
/** Map to store refs for each ViewHolder instance */
Expand Down Expand Up @@ -176,6 +177,15 @@ export const ViewHolderCollection = <TItem,>(
{containerLayout &&
hasData &&
Array.from(renderStack.entries(), ([reactKey, { index }]) => {
const layout = getLayout(index);
// The render stack can outlive a layout-table shrink: modifyLayout
// truncates layouts before the stack is pruned, and the pruning pass
// is skipped when the engaged range is unchanged. Skip the stale
// entry until the next sync restores the pairing, rather than
// reading past the end of the layout table.
if (!layout) {
return null;
}
const item = data[index];
// Suppress separators for items in the last row to prevent
// height mismatch. The last data item has no separator (no
Expand All @@ -192,7 +202,7 @@ export const ViewHolderCollection = <TItem,>(
item={item}
trailingItem={trailingItem}
layout={{
...getLayout(index),
...layout,
}}
refHolder={refHolder}
onSizeChanged={onSizeChanged}
Expand Down
Loading