Skip to content

Commit 231d53a

Browse files
CalixTangmeta-codesync[bot]
authored andcommitted
Move VirtualCollection components into react-native-github (#56256)
Summary: Pull Request resolved: #56256 Moves the core Fling virtual collection components (VirtualRow, VirtualColumn, VirtualCollectionView, and supporting files) from xplat/js/RKJSModules/Libraries/List/ into xplat/js/react-native-github/packages/react-native/src/private/components/virtualcollection/. Updated 40 consumer files to import from 'react-native' instead of Haste and regenerated BUCK files # Changelog: [Internal] Key changes: - Moved 10 files (Virtual, VirtualCollectionView, FlingConstants, VirtualColumn, VirtualColumnGenerator, VirtualRow, VirtualRowGenerator, FlingDebugItemOverlay, getScrollParent, isScrollableNode) into react-native-github - Resolved all fb_internal imports (ReactNativeElement, ReadOnlyElement, ReactNativeFeatureFlags, createHiddenVirtualView) to use relative paths within src/private/ - Converted copyright headers from proprietary to MIT license - Added public re-exports to react-native index.js and index.js.flow (VirtualColumn, VirtualRow, VirtualArray, VirtualCollection, VirtualItem, createVirtualCollectionView, VirtualColumnGenerator, getScrollParent, DEFAULT_INITIAL_NUM_TO_RENDER) Reviewed By: mdvacca, lunaleaps Differential Revision: D98039999 fbshipit-source-id: c0164f6933f19225fafc001cc0049d534032f2b2
1 parent 6747cd2 commit 231d53a

12 files changed

Lines changed: 674 additions & 0 deletions

File tree

packages/react-native/index.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,34 @@ module.exports = {
157157
get unstable_VirtualView() {
158158
return require('./src/private/components/virtualview/VirtualView').default;
159159
},
160+
get unstable_VirtualArray() {
161+
return require('./src/private/components/virtualcollection/Virtual')
162+
.VirtualArray;
163+
},
164+
get unstable_createVirtualCollectionView() {
165+
return require('./src/private/components/virtualcollection/VirtualCollectionView')
166+
.createVirtualCollectionView;
167+
},
168+
get unstable_VirtualColumn() {
169+
return require('./src/private/components/virtualcollection/column/VirtualColumn')
170+
.default;
171+
},
172+
get unstable_VirtualColumnGenerator() {
173+
return require('./src/private/components/virtualcollection/column/VirtualColumnGenerator')
174+
.default;
175+
},
176+
get unstable_VirtualRow() {
177+
return require('./src/private/components/virtualcollection/row/VirtualRow')
178+
.default;
179+
},
180+
get unstable_getScrollParent() {
181+
return require('./src/private/components/virtualcollection/dom/getScrollParent')
182+
.default;
183+
},
184+
get unstable_DEFAULT_INITIAL_NUM_TO_RENDER() {
185+
return require('./src/private/components/virtualcollection/FlingConstants')
186+
.DEFAULT_INITIAL_NUM_TO_RENDER;
187+
},
160188
// #endregion
161189
// #region APIs
162190
get AccessibilityInfo() {

packages/react-native/index.js.flow

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,4 +476,21 @@ export {
476476
} from './src/private/components/virtualview/VirtualView';
477477
export type {ModeChangeEvent} from './src/private/components/virtualview/VirtualView';
478478

479+
export {VirtualArray as unstable_VirtualArray} from './src/private/components/virtualcollection/Virtual';
480+
export type {
481+
Item as unstable_VirtualItem,
482+
VirtualCollection as unstable_VirtualCollection,
483+
} from './src/private/components/virtualcollection/Virtual';
484+
export {createVirtualCollectionView as unstable_createVirtualCollectionView} from './src/private/components/virtualcollection/VirtualCollectionView';
485+
export type {
486+
VirtualCollectionGenerator as unstable_VirtualCollectionGenerator,
487+
VirtualCollectionLayoutComponent as unstable_VirtualCollectionLayoutComponent,
488+
VirtualCollectionViewComponent as unstable_VirtualCollectionViewComponent,
489+
} from './src/private/components/virtualcollection/VirtualCollectionView';
490+
export {default as unstable_VirtualColumn} from './src/private/components/virtualcollection/column/VirtualColumn';
491+
export {default as unstable_VirtualColumnGenerator} from './src/private/components/virtualcollection/column/VirtualColumnGenerator';
492+
export {default as unstable_VirtualRow} from './src/private/components/virtualcollection/row/VirtualRow';
493+
export {default as unstable_getScrollParent} from './src/private/components/virtualcollection/dom/getScrollParent';
494+
export {DEFAULT_INITIAL_NUM_TO_RENDER as unstable_DEFAULT_INITIAL_NUM_TO_RENDER} from './src/private/components/virtualcollection/FlingConstants';
495+
479496
// #endregion
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
import Dimensions from '../../../../Libraries/Utilities/Dimensions';
12+
13+
export const DEFAULT_INITIAL_NUM_TO_RENDER = 7;
14+
15+
export const INITIAL_NUM_TO_RENDER: number = DEFAULT_INITIAL_NUM_TO_RENDER;
16+
17+
export const FALLBACK_ESTIMATED_HEIGHT: number =
18+
Dimensions.get('window').height / DEFAULT_INITIAL_NUM_TO_RENDER;
19+
20+
export const FALLBACK_ESTIMATED_WIDTH: number =
21+
Dimensions.get('window').width / DEFAULT_INITIAL_NUM_TO_RENDER;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict
8+
* @format
9+
*/
10+
11+
/**
12+
* An item to virtualize must be an item. It can optionally have a string `id`
13+
* parameter, but that is not currently represented because it makes the Flow
14+
* types more complicated.
15+
*/
16+
export interface Item {}
17+
18+
/**
19+
* An interface for a collection of items, without requiring that each item be
20+
* eagerly (or lazily) allocated.
21+
*/
22+
export interface VirtualCollection<+T extends Item> {
23+
/**
24+
* The number of items in the collection. This can either be a numeric scalar
25+
* or a getter function that is computed on access. However, it should remain
26+
* constant for the lifetime of this object.
27+
*/
28+
+size: number;
29+
30+
/**
31+
* If an item exists at the supplied index, this should return a consistent
32+
* item for the lifetime of this object. If an item does not exist at the
33+
* supplied index, this should throw an error.
34+
*/
35+
at(index: number): T;
36+
}
37+
38+
/**
39+
* An implementation of `VirtualCollection` that wraps an array. Although easy to
40+
* use, this is not recommended for larger arrays because each element of an
41+
* array is eagerly allocated.
42+
*/
43+
export class VirtualArray<+T extends Item> implements VirtualCollection<T> {
44+
+size: number;
45+
+at: (index: number) => T;
46+
47+
constructor(input: Readonly<$ArrayLike<T>>) {
48+
const array = [...input];
49+
50+
// NOTE: This is implemented this way because Flow does not permit `input`
51+
// to be a read-only instance property (even a private one).
52+
this.size = array.length;
53+
this.at = (index: number): T => {
54+
if (index < 0 || index >= this.size) {
55+
throw new RangeError(
56+
`Cannot get index ${index} from a collection of size ${this.size}`,
57+
);
58+
}
59+
return array[index];
60+
};
61+
}
62+
}
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
import type {ViewStyleProp} from '../../../../Libraries/StyleSheet/StyleSheet';
12+
import type {ModeChangeEvent} from '../virtualview/VirtualView';
13+
import type {Item, VirtualCollection} from './Virtual';
14+
15+
import VirtualView from '../virtualview/VirtualView';
16+
import {
17+
VirtualViewMode,
18+
createHiddenVirtualView,
19+
} from '../virtualview/VirtualView';
20+
import FlingItemOverlay from './debug/FlingItemOverlay';
21+
import * as React from 'react';
22+
import {useCallback, useMemo, useState} from 'react';
23+
24+
export type VirtualCollectionLayoutComponent<TLayoutProps extends {...}> =
25+
component(
26+
children: ReadonlyArray<React.Node>,
27+
spacer: React.Node,
28+
...TLayoutProps
29+
);
30+
31+
export type VirtualCollectionGenerator = Readonly<{
32+
initial: Readonly<{
33+
itemCount: number,
34+
spacerStyle: (itemCount: number) => ViewStyleProp,
35+
}>,
36+
next: (event: ModeChangeEvent) => {
37+
itemCount: number,
38+
spacerStyle: (itemCount: number) => ViewStyleProp,
39+
},
40+
}>;
41+
42+
export type VirtualCollectionViewComponent<TLayoutProps extends {...}> =
43+
component<+TItem extends Item>(
44+
children: (item: TItem, key: string) => React.Node,
45+
items: VirtualCollection<TItem>,
46+
itemToKey?: (TItem) => string,
47+
removeClippedSubviews?: boolean,
48+
testID?: ?string,
49+
...TLayoutProps
50+
);
51+
52+
/**
53+
* Creates a component that virtually renders a collection of items and manages
54+
* lazy rendering, memoization, and pagination. The resulting component accepts
55+
* the following base props:
56+
*
57+
* - `children`: A function maps an item to a React node.
58+
* - `items`: A collection of items to render.
59+
* - `itemToKey`: A function maps an item to a unique key.
60+
*
61+
* The first argument is a layout component that defines layout of the item and
62+
* spacer. It always receives the following props:
63+
*
64+
* - `children`: An array of React nodes (for items rendered so far).
65+
* - `spacer`: A React node (estimates layout for items not yet rendered).
66+
*
67+
* The layout component must render `children` and `spacer`. It can also define
68+
* additional props that will be passed through from the resulting component.
69+
*
70+
* The second argument is a generator that defines the initial rendering and
71+
* pagination behavior. The initial rendering behavior is defined by the
72+
* `initial` property with the following properties:
73+
*
74+
* - `itemCount`: Number of items to render initially.
75+
* - `spacerStyle`: A function that estimates the layout of the spacer. It
76+
* receives the number of items being rendered as an argument.
77+
*
78+
* The pagination behavior is defined by the `next` function that receives a
79+
* `ModeChangeEvent` and then returns an object with the following properties:
80+
*
81+
* - `itemCount`: Number of additional items needed to fill `thresholdRect`.
82+
* - `spacerStyle`: A function that estimates the layout of the spacer. It
83+
* receives the number of items being rendered as an argument.
84+
*
85+
*/
86+
export function createVirtualCollectionView<TLayoutProps extends {...}>(
87+
VirtualLayout: VirtualCollectionLayoutComponent<TLayoutProps>,
88+
{initial, next}: VirtualCollectionGenerator,
89+
): VirtualCollectionViewComponent<TLayoutProps> {
90+
component VirtualCollectionView<+TItem extends Item>(
91+
children: (item: TItem, key: string) => React.Node,
92+
items: VirtualCollection<TItem>,
93+
itemToKey: TItem => string = defaultItemToKey,
94+
removeClippedSubviews: boolean = false,
95+
testID?: ?string,
96+
...layoutProps: TLayoutProps
97+
) {
98+
const [desiredItemCount, setDesiredItemCount] = useState(
99+
Math.ceil(initial.itemCount),
100+
);
101+
102+
const renderItem = useMemoCallback(
103+
useCallback(
104+
(item: TItem) => {
105+
const key = itemToKey(item);
106+
return (
107+
<VirtualView
108+
key={key}
109+
nativeID={key}
110+
removeClippedSubviews={removeClippedSubviews}>
111+
{FlingItemOverlay == null ? null : (
112+
<FlingItemOverlay nativeID={key} />
113+
)}
114+
{children(item, key)}
115+
</VirtualView>
116+
);
117+
},
118+
[children, itemToKey, removeClippedSubviews],
119+
),
120+
);
121+
122+
const mountedItemCount = Math.min(desiredItemCount, items.size);
123+
const mountedItemViews = Array.from(
124+
{length: mountedItemCount},
125+
(_, index) => renderItem(items.at(index)),
126+
);
127+
128+
const virtualItemCount = items.size - mountedItemCount;
129+
const virtualItemSpacer = useMemo(
130+
() =>
131+
virtualItemCount === 0 ? null : (
132+
<VirtualCollectionSpacer
133+
nativeID={`${testID ?? ''}:Spacer`}
134+
virtualItemCount={virtualItemCount}
135+
onRenderMoreItems={(itemCount: number) => {
136+
setDesiredItemCount(
137+
prevElementCount => prevElementCount + itemCount,
138+
);
139+
}}
140+
/>
141+
),
142+
[virtualItemCount, testID],
143+
);
144+
145+
return (
146+
<VirtualLayout {...layoutProps} spacer={virtualItemSpacer}>
147+
{mountedItemViews}
148+
</VirtualLayout>
149+
);
150+
}
151+
152+
function createSpacerView(spacerStyle: (itemCount: number) => ViewStyleProp) {
153+
component SpacerView(
154+
itemCount: number,
155+
ref?: React.RefSetter<React.RefOf<VirtualView> | null>,
156+
...props: Omit<React.PropsOf<VirtualView>, 'ref'>
157+
) {
158+
const HiddenVirtualView = useMemo(
159+
() => createHiddenVirtualView(spacerStyle(itemCount)),
160+
[itemCount],
161+
);
162+
return <HiddenVirtualView ref={ref} {...props} />;
163+
}
164+
return SpacerView;
165+
}
166+
167+
const initialSpacerView = {
168+
SpacerView: createSpacerView(initial.spacerStyle),
169+
};
170+
171+
component VirtualCollectionSpacer(
172+
nativeID: string,
173+
virtualItemCount: number,
174+
175+
onRenderMoreItems: (itemCount: number) => void,
176+
) {
177+
// NOTE: Store `SpacerView` in a wrapper object because otherwise, `useState`
178+
// will confuse `SpacerView` (a component) as being an updater function.
179+
const [{SpacerView}, setSpacerView] = useState(initialSpacerView);
180+
181+
const handleModeChange = (event: ModeChangeEvent) => {
182+
if (event.mode === VirtualViewMode.Hidden) {
183+
// This should never happen; this starts hidden and otherwise unmounts.
184+
return;
185+
}
186+
const {itemCount, spacerStyle} = next(event);
187+
188+
// Refine the estimated item size when computing spacer size.
189+
setSpacerView({
190+
SpacerView: createSpacerView(spacerStyle),
191+
});
192+
193+
// Render more items to fill `thresholdRect`.
194+
onRenderMoreItems(Math.min(Math.ceil(itemCount), virtualItemCount));
195+
};
196+
197+
return (
198+
<SpacerView
199+
itemCount={virtualItemCount}
200+
nativeID={nativeID}
201+
onModeChange={handleModeChange}
202+
/>
203+
);
204+
}
205+
206+
return VirtualCollectionView;
207+
}
208+
209+
hook useMemoCallback<TInput extends interface {}, TOutput>(
210+
callback: TInput => TOutput,
211+
): TInput => TOutput {
212+
return useMemo(() => memoize(callback), [callback]);
213+
}
214+
215+
function memoize<TInput extends interface {}, TOutput>(
216+
callback: TInput => TOutput,
217+
): TInput => TOutput {
218+
const cache = new WeakMap<TInput, TOutput>();
219+
return (input: TInput) => {
220+
let output = cache.get(input);
221+
if (output == null) {
222+
output = callback(input);
223+
cache.set(input, output);
224+
}
225+
return output;
226+
};
227+
}
228+
229+
function defaultItemToKey(item: Item): string {
230+
// $FlowExpectedError[prop-missing] - Flow cannot model this dynamic pattern.
231+
const key = item.key;
232+
if (typeof key !== 'string') {
233+
throw new TypeError(
234+
`Expected 'id' of item to be a string, got: ${typeof key}`,
235+
);
236+
}
237+
return key;
238+
}

0 commit comments

Comments
 (0)