FRE-600: Fix code review blockers

- Consolidated duplicate UndoManagers to single instance
- Fixed connection promise to only resolve on 'connected' status
- Fixed WebSocketProvider import (WebsocketProvider)
- Added proper doc.destroy() cleanup
- Renamed isPresenceInitialized property to avoid conflict

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-04-25 00:08:01 -04:00
parent 65b552bb08
commit 7c684a42cc
48450 changed files with 5679671 additions and 383 deletions

View File

@@ -0,0 +1,155 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/
import invariant from 'invariant';
export type CellRegion = {
first: number,
last: number,
isSpacer: boolean,
};
export class CellRenderMask {
_numCells: number;
_regions: Array<CellRegion>;
constructor(numCells: number) {
invariant(
numCells >= 0,
'CellRenderMask must contain a non-negative number os cells',
);
this._numCells = numCells;
if (numCells === 0) {
this._regions = [];
} else {
this._regions = [
{
first: 0,
last: numCells - 1,
isSpacer: true,
},
];
}
}
enumerateRegions(): ReadonlyArray<CellRegion> {
return this._regions;
}
addCells(cells: {first: number, last: number}): void {
invariant(
cells.first >= 0 &&
cells.first < this._numCells &&
cells.last >= -1 &&
cells.last < this._numCells &&
cells.last >= cells.first - 1,
'CellRenderMask.addCells called with invalid cell range',
);
// VirtualizedList uses inclusive ranges, where zero-count states are
// possible. E.g. [0, -1] for no cells, starting at 0.
if (cells.last < cells.first) {
return;
}
const [firstIntersect, firstIntersectIdx] = this._findRegion(cells.first);
const [lastIntersect, lastIntersectIdx] = this._findRegion(cells.last);
// Fast-path if the cells to add are already all present in the mask. We
// will otherwise need to do some mutation.
if (firstIntersectIdx === lastIntersectIdx && !firstIntersect.isSpacer) {
return;
}
// We need to replace the existing covered regions with 1-3 new regions
// depending whether we need to split spacers out of overlapping regions.
const newLeadRegion: Array<CellRegion> = [];
const newTailRegion: Array<CellRegion> = [];
const newMainRegion: CellRegion = {
...cells,
isSpacer: false,
};
if (firstIntersect.first < newMainRegion.first) {
if (firstIntersect.isSpacer) {
newLeadRegion.push({
first: firstIntersect.first,
last: newMainRegion.first - 1,
isSpacer: true,
});
} else {
newMainRegion.first = firstIntersect.first;
}
}
if (lastIntersect.last > newMainRegion.last) {
if (lastIntersect.isSpacer) {
newTailRegion.push({
first: newMainRegion.last + 1,
last: lastIntersect.last,
isSpacer: true,
});
} else {
newMainRegion.last = lastIntersect.last;
}
}
const replacementRegions: Array<CellRegion> = [
...newLeadRegion,
newMainRegion,
...newTailRegion,
];
const numRegionsToDelete = lastIntersectIdx - firstIntersectIdx + 1;
this._regions.splice(
firstIntersectIdx,
numRegionsToDelete,
...replacementRegions,
);
}
numCells(): number {
return this._numCells;
}
equals(other: CellRenderMask): boolean {
return (
this._numCells === other._numCells &&
this._regions.length === other._regions.length &&
this._regions.every(
(region, i) =>
region.first === other._regions[i].first &&
region.last === other._regions[i].last &&
region.isSpacer === other._regions[i].isSpacer,
)
);
}
_findRegion(cellIdx: number): [CellRegion, number] {
let firstIdx = 0;
let lastIdx = this._regions.length - 1;
while (firstIdx <= lastIdx) {
const middleIdx = Math.floor((firstIdx + lastIdx) / 2);
const middleRegion = this._regions[middleIdx];
if (cellIdx >= middleRegion.first && cellIdx <= middleRegion.last) {
return [middleRegion, middleIdx];
} else if (cellIdx < middleRegion.first) {
lastIdx = middleIdx - 1;
} else if (cellIdx > middleRegion.last) {
firstIdx = middleIdx + 1;
}
}
invariant(false, `A region was not found containing cellIdx ${cellIdx}`);
}
}

View File

@@ -0,0 +1,72 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/
import invariant from 'invariant';
export default class ChildListCollection<TList> {
_cellKeyToChildren: Map<string, Set<TList>> = new Map();
_childrenToCellKey: Map<TList, string> = new Map();
add(list: TList, cellKey: string): void {
invariant(
!this._childrenToCellKey.has(list),
'Trying to add already present child list',
);
const cellLists = this._cellKeyToChildren.get(cellKey) ?? new Set();
cellLists.add(list);
this._cellKeyToChildren.set(cellKey, cellLists);
this._childrenToCellKey.set(list, cellKey);
}
remove(list: TList): void {
const cellKey = this._childrenToCellKey.get(list);
invariant(cellKey != null, 'Trying to remove non-present child list');
this._childrenToCellKey.delete(list);
const cellLists = this._cellKeyToChildren.get(cellKey);
invariant(cellLists, '_cellKeyToChildren should contain cellKey');
cellLists.delete(list);
if (cellLists.size === 0) {
this._cellKeyToChildren.delete(cellKey);
}
}
forEach(fn: TList => void): void {
for (const listSet of this._cellKeyToChildren.values()) {
for (const list of listSet) {
fn(list);
}
}
}
forEachInCell(cellKey: string, fn: TList => void): void {
const listSet = this._cellKeyToChildren.get(cellKey) ?? [];
for (const list of listSet) {
fn(list);
}
}
anyInCell(cellKey: string, fn: TList => boolean): boolean {
const listSet = this._cellKeyToChildren.get(cellKey) ?? [];
for (const list of listSet) {
if (fn(list)) {
return true;
}
}
return false;
}
size(): number {
return this._childrenToCellKey.size;
}
}

View File

@@ -0,0 +1,255 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import type {CellMetricProps} from './ListMetricsAggregator';
import ListMetricsAggregator from './ListMetricsAggregator';
export type FillRateInfo = Info;
class Info {
any_blank_count: number = 0;
any_blank_ms: number = 0;
any_blank_speed_sum: number = 0;
mostly_blank_count: number = 0;
mostly_blank_ms: number = 0;
pixels_blank: number = 0;
pixels_sampled: number = 0;
pixels_scrolled: number = 0;
total_time_spent: number = 0;
sample_count: number = 0;
}
const DEBUG = false;
let _listeners: Array<(Info) => void> = [];
let _minSampleCount = 10;
/* $FlowFixMe[constant-condition] Error discovered during Constant Condition
* roll out. See https://fburl.com/workplace/1v97vimq. */
let _sampleRate = DEBUG ? 1 : null;
/**
* A helper class for detecting when the maximem fill rate of `VirtualizedList` is exceeded.
* By default the sampling rate is set to zero and this will do nothing. If you want to collect
* samples (e.g. to log them), make sure to call `FillRateHelper.setSampleRate(0.0-1.0)`.
*
* Listeners and sample rate are global for all `VirtualizedList`s - typical usage will combine with
* `SceneTracker.getActiveScene` to determine the context of the events.
*/
class FillRateHelper {
_anyBlankStartTime: ?number = null;
_enabled = false;
_listMetrics: ListMetricsAggregator;
_info: Info = new Info();
_mostlyBlankStartTime: ?number = null;
_samplesStartTime: ?number = null;
static addListener(callback: FillRateInfo => void): {
remove: () => void,
...
} {
if (_sampleRate === null) {
console.warn('Call `FillRateHelper.setSampleRate` before `addListener`.');
}
_listeners.push(callback);
return {
remove: () => {
_listeners = _listeners.filter(listener => callback !== listener);
},
};
}
static setSampleRate(sampleRate: number) {
_sampleRate = sampleRate;
}
static setMinSampleCount(minSampleCount: number) {
_minSampleCount = minSampleCount;
}
constructor(listMetrics: ListMetricsAggregator) {
this._listMetrics = listMetrics;
this._enabled = (_sampleRate || 0) > Math.random();
this._resetData();
}
activate() {
if (this._enabled && this._samplesStartTime == null) {
/* $FlowFixMe[constant-condition] Error discovered during Constant
* Condition roll out. See https://fburl.com/workplace/1v97vimq. */
DEBUG && console.debug('FillRateHelper: activate');
this._samplesStartTime = global.performance.now();
}
}
deactivateAndFlush() {
if (!this._enabled) {
return;
}
const start = this._samplesStartTime; // const for flow
if (start == null) {
/* $FlowFixMe[constant-condition] Error discovered during Constant
* Condition roll out. See https://fburl.com/workplace/1v97vimq. */
DEBUG &&
console.debug('FillRateHelper: bail on deactivate with no start time');
return;
}
if (this._info.sample_count < _minSampleCount) {
// Don't bother with under-sampled events.
this._resetData();
return;
}
const total_time_spent = global.performance.now() - start;
const info: any = {
...this._info,
total_time_spent,
};
/* $FlowFixMe[constant-condition] Error discovered during Constant
* Condition roll out. See https://fburl.com/workplace/1v97vimq. */
if (DEBUG) {
const derived = {
avg_blankness: this._info.pixels_blank / this._info.pixels_sampled,
avg_speed: this._info.pixels_scrolled / (total_time_spent / 1000),
avg_speed_when_any_blank:
this._info.any_blank_speed_sum / this._info.any_blank_count,
any_blank_per_min:
this._info.any_blank_count / (total_time_spent / 1000 / 60),
any_blank_time_frac: this._info.any_blank_ms / total_time_spent,
mostly_blank_per_min:
this._info.mostly_blank_count / (total_time_spent / 1000 / 60),
mostly_blank_time_frac: this._info.mostly_blank_ms / total_time_spent,
};
for (const key in derived) {
// $FlowFixMe[prop-missing]
// $FlowFixMe[invalid-computed-prop]
derived[key] = Math.round(1000 * derived[key]) / 1000;
}
console.debug('FillRateHelper deactivateAndFlush: ', {derived, info});
}
_listeners.forEach(listener => listener(info));
this._resetData();
}
computeBlankness(
props: {
...CellMetricProps,
initialNumToRender?: ?number,
...
},
cellsAroundViewport: {
first: number,
last: number,
...
},
scrollMetrics: {
dOffset: number,
offset: number,
velocity: number,
visibleLength: number,
...
},
): number {
if (
!this._enabled ||
props.getItemCount(props.data) === 0 ||
cellsAroundViewport.last < cellsAroundViewport.first ||
this._samplesStartTime == null
) {
return 0;
}
const {dOffset, offset, velocity, visibleLength} = scrollMetrics;
// Denominator metrics that we track for all events - most of the time there is no blankness and
// we want to capture that.
this._info.sample_count++;
this._info.pixels_sampled += Math.round(visibleLength);
this._info.pixels_scrolled += Math.round(Math.abs(dOffset));
const scrollSpeed = Math.round(Math.abs(velocity) * 1000); // px / sec
// Whether blank now or not, record the elapsed time blank if we were blank last time.
const now = global.performance.now();
if (this._anyBlankStartTime != null) {
this._info.any_blank_ms += now - this._anyBlankStartTime;
}
this._anyBlankStartTime = null;
if (this._mostlyBlankStartTime != null) {
this._info.mostly_blank_ms += now - this._mostlyBlankStartTime;
}
this._mostlyBlankStartTime = null;
let blankTop = 0;
let first = cellsAroundViewport.first;
let firstFrame = this._listMetrics.getCellMetrics(first, props);
while (
first <= cellsAroundViewport.last &&
(!firstFrame || !firstFrame.isMounted)
) {
firstFrame = this._listMetrics.getCellMetrics(first, props);
first++;
}
// Only count blankTop if we aren't rendering the first item, otherwise we will count the header
// as blank.
if (firstFrame && first > 0) {
blankTop = Math.min(
visibleLength,
Math.max(0, firstFrame.offset - offset),
);
}
let blankBottom = 0;
let last = cellsAroundViewport.last;
let lastFrame = this._listMetrics.getCellMetrics(last, props);
while (
last >= cellsAroundViewport.first &&
(!lastFrame || !lastFrame.isMounted)
) {
lastFrame = this._listMetrics.getCellMetrics(last, props);
last--;
}
// Only count blankBottom if we aren't rendering the last item, otherwise we will count the
// footer as blank.
if (lastFrame && last < props.getItemCount(props.data) - 1) {
const bottomEdge = lastFrame.offset + lastFrame.length;
blankBottom = Math.min(
visibleLength,
Math.max(0, offset + visibleLength - bottomEdge),
);
}
const pixels_blank = Math.round(blankTop + blankBottom);
const blankness = pixels_blank / visibleLength;
if (blankness > 0) {
this._anyBlankStartTime = now;
this._info.any_blank_speed_sum += scrollSpeed;
this._info.any_blank_count++;
this._info.pixels_blank += pixels_blank;
if (blankness > 0.5) {
this._mostlyBlankStartTime = now;
this._info.mostly_blank_count++;
}
} else if (scrollSpeed < 0.01 || Math.abs(dOffset) < 1) {
this.deactivateAndFlush();
}
return blankness;
}
enabled(): boolean {
return this._enabled;
}
_resetData() {
this._anyBlankStartTime = null;
this._info = new Info();
this._mostlyBlankStartTime = null;
this._samplesStartTime = null;
}
}
export default FillRateHelper;

View File

@@ -0,0 +1,330 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {VirtualizedListProps} from './VirtualizedListProps';
import type {LayoutRectangle} from 'react-native';
import {keyExtractor as defaultKeyExtractor} from './VirtualizeUtils';
import invariant from 'invariant';
export type CellMetrics = {
/**
* Index of the item in the list
*/
index: number,
/**
* Length of the cell along the scrolling axis
*/
length: number,
/**
* Distance between this cell and the start of the list along the scrolling
* axis
*/
offset: number,
/**
* Whether the cell is last known to be mounted
*/
isMounted: boolean,
};
// TODO: `inverted` can be incorporated here if it is moved to an order
// based implementation instead of transform.
export type ListOrientation = {
horizontal: boolean,
rtl: boolean,
};
/**
* Subset of VirtualizedList props needed to calculate cell metrics
*/
export type CellMetricProps = {
data: VirtualizedListProps['data'],
getItemCount: VirtualizedListProps['getItemCount'],
getItem: VirtualizedListProps['getItem'],
getItemLayout?: VirtualizedListProps['getItemLayout'],
keyExtractor?: VirtualizedListProps['keyExtractor'],
...
};
/**
* Provides an interface to query information about the metrics of a list and its cells.
*/
export default class ListMetricsAggregator {
_averageCellLength = 0;
_cellMetrics: Map<string, CellMetrics> = new Map();
_contentLength: ?number;
_highestMeasuredCellIndex = 0;
_measuredCellsLength = 0;
_measuredCellsCount = 0;
_orientation: ListOrientation = {
horizontal: false,
rtl: false,
};
/**
* Notify the ListMetricsAggregator that a cell has been laid out.
*
* @returns whether the cell layout has changed since last notification
*/
notifyCellLayout({
cellIndex,
cellKey,
orientation,
layout,
}: {
cellIndex: number,
cellKey: string,
orientation: ListOrientation,
layout: LayoutRectangle,
}): boolean {
this._invalidateIfOrientationChanged(orientation);
const next: CellMetrics = {
index: cellIndex,
length: this._selectLength(layout),
isMounted: true,
offset: this.flowRelativeOffset(layout),
};
const curr = this._cellMetrics.get(cellKey);
if (!curr || next.offset !== curr.offset || next.length !== curr.length) {
if (curr) {
const dLength = next.length - curr.length;
this._measuredCellsLength += dLength;
} else {
this._measuredCellsLength += next.length;
this._measuredCellsCount += 1;
}
this._averageCellLength =
this._measuredCellsLength / this._measuredCellsCount;
this._cellMetrics.set(cellKey, next);
this._highestMeasuredCellIndex = Math.max(
this._highestMeasuredCellIndex,
cellIndex,
);
return true;
} else {
curr.isMounted = true;
return false;
}
}
/**
* Notify ListMetricsAggregator that a cell has been unmounted.
*/
notifyCellUnmounted(cellKey: string): void {
const curr = this._cellMetrics.get(cellKey);
if (curr) {
curr.isMounted = false;
}
}
/**
* Notify ListMetricsAggregator that the lists content container has been laid out.
*/
notifyListContentLayout({
orientation,
layout,
}: {
orientation: ListOrientation,
layout: Readonly<{width: number, height: number}>,
}): void {
this._invalidateIfOrientationChanged(orientation);
this._contentLength = this._selectLength(layout);
}
/**
* Return the average length of the cells which have been measured
*/
getAverageCellLength(): number {
return this._averageCellLength;
}
/**
* Return the highest measured cell index (or 0 if nothing has been measured
* yet)
*/
getHighestMeasuredCellIndex(): number {
return this._highestMeasuredCellIndex;
}
/**
* Returns the exact metrics of a cell if it has already been laid out,
* otherwise an estimate based on the average length of previously measured
* cells
*/
getCellMetricsApprox(index: number, props: CellMetricProps): CellMetrics {
const frame = this.getCellMetrics(index, props);
if (frame && frame.index === index) {
// check for invalid frames due to row re-ordering
return frame;
} else {
let offset;
const highestMeasuredCellIndex = this.getHighestMeasuredCellIndex();
if (highestMeasuredCellIndex < index) {
// If any of the cells before this one have been laid out already, we
// should use that information in the estimations.
// This is important because if the list has a header, the initial cell
// will have a larger offset that we should take into account here.
const highestMeasuredCellFrame = this.getCellMetrics(
highestMeasuredCellIndex,
props,
);
if (highestMeasuredCellFrame) {
offset =
highestMeasuredCellFrame.offset +
highestMeasuredCellFrame.length +
this._averageCellLength * (index - highestMeasuredCellIndex - 1);
}
}
if (offset == null) {
offset = this._averageCellLength * index;
}
const {data, getItemCount} = props;
invariant(
index >= 0 && index < getItemCount(data),
'Tried to get frame for out of range index ' + index,
);
return {
length: this._averageCellLength,
offset,
index,
isMounted: false,
};
}
}
/**
* Returns the exact metrics of a cell if it has already been laid out
*/
getCellMetrics(index: number, props: CellMetricProps): ?CellMetrics {
const {data, getItem, getItemCount, getItemLayout} = props;
invariant(
index >= 0 && index < getItemCount(data),
'Tried to get metrics for out of range cell index ' + index,
);
const keyExtractor = props.keyExtractor ?? defaultKeyExtractor;
const frame = this._cellMetrics.get(
keyExtractor(getItem(data, index), index),
);
if (frame && frame.index === index) {
return frame;
}
if (getItemLayout) {
const {length, offset} = getItemLayout(data, index);
// TODO: `isMounted` is used for both "is exact layout" and "has been
// unmounted". Should be refactored.
return {index, length, offset, isMounted: true};
}
return null;
}
/**
* Gets an approximate offset to an item at a given index. Supports
* fractional indices.
*/
getCellOffsetApprox(index: number, props: CellMetricProps): number {
if (Number.isInteger(index)) {
return this.getCellMetricsApprox(index, props).offset;
} else {
const frameMetrics = this.getCellMetricsApprox(Math.floor(index), props);
const remainder = index - Math.floor(index);
return frameMetrics.offset + remainder * frameMetrics.length;
}
}
/**
* Returns the length of all ScrollView content along the scrolling axis.
*/
getContentLength(): number {
return this._contentLength ?? 0;
}
/**
* Whether a content length has been observed
*/
hasContentLength(): boolean {
return this._contentLength != null;
}
/**
* Finds the flow-relative offset (e.g. starting from the left in LTR, but
* right in RTL) from a layout box.
*/
flowRelativeOffset(
layout: LayoutRectangle,
referenceContentLength?: ?number,
): number {
const {horizontal, rtl} = this._orientation;
if (horizontal && rtl) {
const contentLength = referenceContentLength ?? this._contentLength;
invariant(
contentLength != null,
'ListMetricsAggregator must be notified of list content layout before resolving offsets',
);
return (
contentLength -
(this._selectOffset(layout) + this._selectLength(layout))
);
} else {
return this._selectOffset(layout);
}
}
/**
* Converts a flow-relative offset to a cartesian offset
*/
cartesianOffset(flowRelativeOffset: number): number {
const {horizontal, rtl} = this._orientation;
if (horizontal && rtl) {
invariant(
this._contentLength != null,
'ListMetricsAggregator must be notified of list content layout before resolving offsets',
);
return this._contentLength - flowRelativeOffset;
} else {
return flowRelativeOffset;
}
}
_invalidateIfOrientationChanged(orientation: ListOrientation): void {
if (orientation.rtl !== this._orientation.rtl) {
this._cellMetrics.clear();
}
if (orientation.horizontal !== this._orientation.horizontal) {
this._averageCellLength = 0;
this._highestMeasuredCellIndex = 0;
this._measuredCellsLength = 0;
this._measuredCellsCount = 0;
}
this._orientation = orientation;
}
_selectLength({
width,
height,
}: Readonly<{width: number, height: number, ...}>): number {
return this._orientation.horizontal ? width : height;
}
_selectOffset({x, y}: Readonly<{x: number, y: number, ...}>): number {
return this._orientation.horizontal ? x : y;
}
}

View File

@@ -0,0 +1,86 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/
import invariant from 'invariant';
import * as React from 'react';
/**
* `setState` is called asynchronously, and should not rely on the value of
* `this.props` or `this.state`:
* https://react.dev/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
*
* SafePureComponent adds runtime enforcement, to catch cases where these
* variables are read in a state updater function, instead of the ones passed
* in.
*/
export default class StateSafePureComponent<
Props,
State: interface {},
> extends React.PureComponent<Props, State> {
_inAsyncStateUpdate = false;
constructor(props: Props) {
super(props);
this._installSetStateHooks();
}
// $FlowFixMe[incompatible-type]
setState<K: keyof State>(
partialState: ?(Pick<State, K> | ((State, Props) => ?Pick<State, K>)),
callback?: () => unknown,
): void {
if (typeof partialState === 'function') {
super.setState((state, props) => {
this._inAsyncStateUpdate = true;
let ret;
try {
ret = partialState(state, props);
} catch (err) {
throw err;
} finally {
this._inAsyncStateUpdate = false;
}
return ret;
}, callback);
} else {
super.setState(partialState, callback);
}
}
_installSetStateHooks() {
const that = this;
let {props, state} = this;
Object.defineProperty(this, 'props', {
get() {
invariant(
!that._inAsyncStateUpdate,
'"this.props" should not be accessed during state updates',
);
return props;
},
set(newProps: Props) {
props = newProps;
},
});
Object.defineProperty(this, 'state', {
get() {
invariant(
!that._inAsyncStateUpdate,
'"this.state" should not be acceessed during state updates',
);
return state;
},
set(newState: State) {
state = newState;
},
});
}
}

View File

@@ -0,0 +1,352 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import type {CellMetricProps} from './ListMetricsAggregator';
import ListMetricsAggregator from './ListMetricsAggregator';
const invariant = require('invariant');
export type ViewToken = {
item: any,
key: string,
index: ?number,
isViewable: boolean,
section?: any,
...
};
export type ViewabilityConfigCallbackPair = {
viewabilityConfig: ViewabilityConfig,
onViewableItemsChanged: (info: {
viewableItems: Array<ViewToken>,
changed: Array<ViewToken>,
...
}) => void,
...
};
export type ViewabilityConfigCallbackPairs =
Array<ViewabilityConfigCallbackPair>;
export type ViewabilityConfig = Readonly<{
/**
* Minimum amount of time (in milliseconds) that an item must be physically viewable before the
* viewability callback will be fired. A high number means that scrolling through content without
* stopping will not mark the content as viewable.
*/
minimumViewTime?: number,
/**
* Percent of viewport that must be covered for a partially occluded item to count as
* "viewable", 0-100. Fully visible items are always considered viewable. A value of 0 means
* that a single pixel in the viewport makes the item viewable, and a value of 100 means that
* an item must be either entirely visible or cover the entire viewport to count as viewable.
*/
viewAreaCoveragePercentThreshold?: number,
/**
* Similar to `viewAreaPercentThreshold`, but considers the percent of the item that is visible,
* rather than the fraction of the viewable area it covers.
*/
itemVisiblePercentThreshold?: number,
/**
* Nothing is considered viewable until the user scrolls or `recordInteraction` is called after
* render.
*/
waitForInteraction?: boolean,
}>;
/**
* A Utility class for calculating viewable items based on current metrics like scroll position and
* layout.
*
* An item is said to be in a "viewable" state when any of the following
* is true for longer than `minimumViewTime` milliseconds (after an interaction if `waitForInteraction`
* is true):
*
* - Occupying >= `viewAreaCoveragePercentThreshold` of the view area XOR fraction of the item
* visible in the view area >= `itemVisiblePercentThreshold`.
* - Entirely visible on screen
*/
class ViewabilityHelper {
_config: ViewabilityConfig;
_hasInteracted: boolean = false;
_timers: Set<number> = new Set();
_viewableIndices: Array<number> = [];
_viewableItems: Map<string, ViewToken> = new Map();
constructor(
config: ViewabilityConfig = {viewAreaCoveragePercentThreshold: 0},
) {
this._config = config;
}
/**
* Cleanup, e.g. on unmount. Clears any pending timers.
*/
dispose() {
/* $FlowFixMe[incompatible-type] (>=0.63.0 site=react_native_fb) This
* comment suppresses an error found when Flow v0.63 was deployed. To see
* the error delete this comment and run Flow. */
this._timers.forEach(clearTimeout);
}
/**
* Determines which items are viewable based on the current metrics and config.
*/
computeViewableItems(
props: CellMetricProps,
scrollOffset: number,
viewportHeight: number,
listMetrics: ListMetricsAggregator,
// Optional optimization to reduce the scan size
renderRange?: {
first: number,
last: number,
...
},
): Array<number> {
const itemCount = props.getItemCount(props.data);
const {itemVisiblePercentThreshold, viewAreaCoveragePercentThreshold} =
this._config;
const viewAreaMode = viewAreaCoveragePercentThreshold != null;
const viewablePercentThreshold = viewAreaMode
? viewAreaCoveragePercentThreshold
: itemVisiblePercentThreshold;
invariant(
viewablePercentThreshold != null &&
(itemVisiblePercentThreshold != null) !==
(viewAreaCoveragePercentThreshold != null),
'Must set exactly one of itemVisiblePercentThreshold or viewAreaCoveragePercentThreshold',
);
const viewableIndices = [];
if (itemCount === 0) {
return viewableIndices;
}
let firstVisible = -1;
const {first, last} = renderRange || {first: 0, last: itemCount - 1};
if (last >= itemCount) {
console.warn(
'Invalid render range computing viewability ' +
JSON.stringify({renderRange, itemCount}),
);
return [];
}
for (let idx = first; idx <= last; idx++) {
const metrics = listMetrics.getCellMetrics(idx, props);
if (!metrics) {
continue;
}
const top = Math.floor(metrics.offset - scrollOffset);
const bottom = Math.floor(top + metrics.length);
if (top < viewportHeight && bottom > 0) {
firstVisible = idx;
if (
_isViewable(
viewAreaMode,
viewablePercentThreshold,
top,
bottom,
viewportHeight,
metrics.length,
)
) {
viewableIndices.push(idx);
}
} else if (firstVisible >= 0) {
break;
}
}
return viewableIndices;
}
/**
* Figures out which items are viewable and how that has changed from before and calls
* `onViewableItemsChanged` as appropriate.
*/
onUpdate(
props: CellMetricProps,
scrollOffset: number,
viewportHeight: number,
listMetrics: ListMetricsAggregator,
createViewToken: (
index: number,
isViewable: boolean,
props: CellMetricProps,
) => ViewToken,
onViewableItemsChanged: ({
viewableItems: Array<ViewToken>,
changed: Array<ViewToken>,
...
}) => void,
// Optional optimization to reduce the scan size
renderRange?: {
first: number,
last: number,
...
},
): void {
const itemCount = props.getItemCount(props.data);
if (
(this._config.waitForInteraction && !this._hasInteracted) ||
itemCount === 0 ||
!listMetrics.getCellMetrics(0, props)
) {
return;
}
let viewableIndices: Array<number> = [];
if (itemCount) {
viewableIndices = this.computeViewableItems(
props,
scrollOffset,
viewportHeight,
listMetrics,
renderRange,
);
}
if (
this._viewableIndices.length === viewableIndices.length &&
this._viewableIndices.every((v, ii) => v === viewableIndices[ii])
) {
// We might get a lot of scroll events where visibility doesn't change and we don't want to do
// extra work in those cases.
return;
}
this._viewableIndices = viewableIndices;
if (this._config.minimumViewTime) {
const handle: TimeoutID = setTimeout(() => {
/* $FlowFixMe[incompatible-type] (>=0.63.0 site=react_native_fb) This
* comment suppresses an error found when Flow v0.63 was deployed. To
* see the error delete this comment and run Flow. */
this._timers.delete(handle);
this._onUpdateSync(
props,
viewableIndices,
onViewableItemsChanged,
createViewToken,
);
}, this._config.minimumViewTime);
/* $FlowFixMe[incompatible-type] (>=0.63.0 site=react_native_fb) This
* comment suppresses an error found when Flow v0.63 was deployed. To see
* the error delete this comment and run Flow. */
this._timers.add(handle);
} else {
this._onUpdateSync(
props,
viewableIndices,
onViewableItemsChanged,
createViewToken,
);
}
}
/**
* clean-up cached _viewableIndices to evaluate changed items on next update
*/
resetViewableIndices() {
this._viewableIndices = [];
}
/**
* Records that an interaction has happened even if there has been no scroll.
*/
recordInteraction() {
this._hasInteracted = true;
}
_onUpdateSync(
props: CellMetricProps,
viewableIndicesToCheck: Array<number>,
onViewableItemsChanged: ({
changed: Array<ViewToken>,
viewableItems: Array<ViewToken>,
...
}) => void,
createViewToken: (
index: number,
isViewable: boolean,
props: CellMetricProps,
) => ViewToken,
) {
// Filter out indices that have gone out of view since this call was scheduled.
viewableIndicesToCheck = viewableIndicesToCheck.filter(ii =>
this._viewableIndices.includes(ii),
);
const prevItems = this._viewableItems;
const nextItems = new Map(
viewableIndicesToCheck.map(ii => {
const viewable = createViewToken(ii, true, props);
return [viewable.key, viewable];
}),
);
const changed = [];
for (const [key, viewable] of nextItems) {
if (!prevItems.has(key)) {
changed.push(viewable);
}
}
for (const [key, viewable] of prevItems) {
if (!nextItems.has(key)) {
changed.push({...viewable, isViewable: false});
}
}
if (changed.length > 0) {
this._viewableItems = nextItems;
onViewableItemsChanged({
viewableItems: Array.from(nextItems.values()),
changed,
viewabilityConfig: this._config,
});
}
}
}
function _isViewable(
viewAreaMode: boolean,
viewablePercentThreshold: number,
top: number,
bottom: number,
viewportHeight: number,
itemLength: number,
): boolean {
if (_isEntirelyVisible(top, bottom, viewportHeight)) {
return true;
} else {
const pixels = _getPixelsVisible(top, bottom, viewportHeight);
const percent =
100 * (viewAreaMode ? pixels / viewportHeight : pixels / itemLength);
return percent >= viewablePercentThreshold;
}
}
function _getPixelsVisible(
top: number,
bottom: number,
viewportHeight: number,
): number {
const visibleHeight = Math.min(bottom, viewportHeight) - Math.max(top, 0);
return Math.max(0, visibleHeight);
}
function _isEntirelyVisible(
top: number,
bottom: number,
viewportHeight: number,
): boolean {
return top >= 0 && bottom <= viewportHeight && bottom > top;
}
export default ViewabilityHelper;

View File

@@ -0,0 +1,256 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import type ListMetricsAggregator from './ListMetricsAggregator';
import type {CellMetricProps} from './ListMetricsAggregator';
import * as ReactNativeFeatureFlags from 'react-native/src/private/featureflags/ReactNativeFeatureFlags';
/**
* Used to find the indices of the frames that overlap the given offsets. Useful for finding the
* items that bound different windows of content, such as the visible area or the buffered overscan
* area.
*/
export function elementsThatOverlapOffsets(
offsets: Array<number>,
props: CellMetricProps,
listMetrics: ListMetricsAggregator,
zoomScale: number = 1,
): Array<number> {
const itemCount = props.getItemCount(props.data);
const result = [];
for (let offsetIndex = 0; offsetIndex < offsets.length; offsetIndex++) {
const currentOffset = offsets[offsetIndex];
let left = 0;
let right = itemCount - 1;
while (left <= right) {
const mid = left + Math.floor((right - left) / 2);
const frame = listMetrics.getCellMetricsApprox(mid, props);
const scaledOffsetStart = frame.offset * zoomScale;
const scaledOffsetEnd = (frame.offset + frame.length) * zoomScale;
// We want the first frame that contains the offset, with inclusive bounds. Thus, for the
// first frame the scaledOffsetStart is inclusive, while for other frames it is exclusive.
if (
(mid === 0 && currentOffset < scaledOffsetStart) ||
(mid !== 0 && currentOffset <= scaledOffsetStart)
) {
right = mid - 1;
} else if (currentOffset > scaledOffsetEnd) {
left = mid + 1;
} else {
result[offsetIndex] = mid;
break;
}
}
}
return result;
}
/**
* Computes the number of elements in the `next` range that are new compared to the `prev` range.
* Handy for calculating how many new items will be rendered when the render window changes so we
* can restrict the number of new items render at once so that content can appear on the screen
* faster.
*/
export function newRangeCount(
prev: {
first: number,
last: number,
...
},
next: {
first: number,
last: number,
...
},
): number {
return (
next.last -
next.first +
1 -
Math.max(
0,
1 + Math.min(next.last, prev.last) - Math.max(next.first, prev.first),
)
);
}
/**
* Custom logic for determining which items should be rendered given the current frame and scroll
* metrics, as well as the previous render state. The algorithm may evolve over time, but generally
* prioritizes the visible area first, then expands that with overscan regions ahead and behind,
* biased in the direction of scroll.
*/
export function computeWindowedRenderLimits(
props: CellMetricProps,
maxToRenderPerBatch: number,
windowSize: number,
prev: {
first: number,
last: number,
},
listMetrics: ListMetricsAggregator,
scrollMetrics: {
dt: number,
offset: number,
velocity: number,
visibleLength: number,
zoomScale: number,
...
},
): {
first: number,
last: number,
} {
const itemCount = props.getItemCount(props.data);
if (itemCount === 0) {
return {first: 0, last: -1};
}
const {offset, velocity, visibleLength, zoomScale = 1} = scrollMetrics;
// Start with visible area, then compute maximum overscan region by expanding from there, biased
// in the direction of scroll. Total overscan area is capped, which should cap memory consumption
// too.
const visibleBegin = Math.max(0, offset);
const visibleEnd = visibleBegin + visibleLength;
const overscanLength = (windowSize - 1) * visibleLength;
// Considering velocity seems to introduce more churn than it's worth.
const leadFactor = 0.5; // Math.max(0, Math.min(1, velocity / 25 + 0.5));
const fillPreference =
velocity > 1 ? 'after' : velocity < -1 ? 'before' : 'none';
const overscanBegin = Math.max(
0,
visibleBegin - (1 - leadFactor) * overscanLength,
);
const overscanEnd = Math.max(0, visibleEnd + leadFactor * overscanLength);
const lastItemOffset =
listMetrics.getCellMetricsApprox(itemCount - 1, props).offset * zoomScale;
if (lastItemOffset < overscanBegin) {
// Entire list is before our overscan window
return {
first: Math.max(0, itemCount - 1 - maxToRenderPerBatch),
last: itemCount - 1,
};
}
// Find the indices that correspond to the items at the render boundaries we're targeting.
let [overscanFirst, first, last, overscanLast] = elementsThatOverlapOffsets(
[overscanBegin, visibleBegin, visibleEnd, overscanEnd],
props,
listMetrics,
zoomScale,
);
overscanFirst = overscanFirst == null ? 0 : overscanFirst;
first = first == null ? Math.max(0, overscanFirst) : first;
overscanLast = overscanLast == null ? itemCount - 1 : overscanLast;
last =
last == null
? Math.min(overscanLast, first + maxToRenderPerBatch - 1)
: last;
const visible = {first, last};
// We want to limit the number of new cells we're rendering per batch so that we can fill the
// content on the screen quickly. If we rendered the entire overscan window at once, the user
// could be staring at white space for a long time waiting for a bunch of offscreen content to
// render.
let newCellCount = newRangeCount(prev, visible);
while (true) {
if (first <= overscanFirst && last >= overscanLast) {
// If we fill the entire overscan range, we're done.
break;
}
const maxNewCells = newCellCount >= maxToRenderPerBatch;
let firstWillAddMore;
let lastWillAddMore;
if (ReactNativeFeatureFlags.fixVirtualizeListCollapseWindowSize()) {
firstWillAddMore = first <= prev.first;
lastWillAddMore = last >= prev.last;
} else {
firstWillAddMore = first <= prev.first || first > prev.last;
lastWillAddMore = last >= prev.last || last < prev.first;
}
const firstShouldIncrement =
first > overscanFirst && (!maxNewCells || !firstWillAddMore);
const lastShouldIncrement =
last < overscanLast && (!maxNewCells || !lastWillAddMore);
if (maxNewCells && !firstShouldIncrement && !lastShouldIncrement) {
// We only want to stop if we've hit maxNewCells AND we cannot increment first or last
// without rendering new items. This let's us preserve as many already rendered items as
// possible, reducing render churn and keeping the rendered overscan range as large as
// possible.
break;
}
if (
firstShouldIncrement &&
!(fillPreference === 'after' && lastShouldIncrement && lastWillAddMore)
) {
if (firstWillAddMore) {
newCellCount++;
}
first--;
}
if (
lastShouldIncrement &&
!(fillPreference === 'before' && firstShouldIncrement && firstWillAddMore)
) {
if (lastWillAddMore) {
newCellCount++;
}
last++;
}
}
if (
!(
last >= first &&
first >= 0 &&
last < itemCount &&
first >= overscanFirst &&
last <= overscanLast &&
first <= visible.first &&
last >= visible.last
)
) {
throw new Error(
'Bad window calculation ' +
JSON.stringify({
first,
last,
itemCount,
overscanFirst,
overscanLast,
visible,
}),
);
}
return {first, last};
}
export function keyExtractor(item: any, index: number): string {
if (typeof item === 'object' && item?.key != null) {
return item.key;
}
if (typeof item === 'object' && item?.id != null) {
return item.id;
}
return String(index);
}

View File

@@ -0,0 +1,397 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import type * as React from 'react';
import type {
StyleProp,
ViewStyle,
ScrollViewProps,
LayoutChangeEvent,
View,
ScrollResponderMixin,
ScrollView,
} from 'react-native';
export interface ViewToken<ItemT = any> {
item: ItemT;
key: string;
index: number | null;
isViewable: boolean;
section?: any | undefined;
}
export interface ViewabilityConfig {
/**
* Minimum amount of time (in milliseconds) that an item must be physically viewable before the
* viewability callback will be fired. A high number means that scrolling through content without
* stopping will not mark the content as viewable.
*/
minimumViewTime?: number | undefined;
/**
* Percent of viewport that must be covered for a partially occluded item to count as
* "viewable", 0-100. Fully visible items are always considered viewable. A value of 0 means
* that a single pixel in the viewport makes the item viewable, and a value of 100 means that
* an item must be either entirely visible or cover the entire viewport to count as viewable.
*/
viewAreaCoveragePercentThreshold?: number | undefined;
/**
* Similar to `viewAreaCoveragePercentThreshold`, but considers the percent of the item that is visible,
* rather than the fraction of the viewable area it covers.
*/
itemVisiblePercentThreshold?: number | undefined;
/**
* Nothing is considered viewable until the user scrolls or `recordInteraction` is called after
* render.
*/
waitForInteraction?: boolean | undefined;
}
export interface ViewabilityConfigCallbackPair {
viewabilityConfig: ViewabilityConfig;
onViewableItemsChanged:
| ((info: {
viewableItems: Array<ViewToken>;
changed: Array<ViewToken>;
}) => void)
| null;
}
export type ViewabilityConfigCallbackPairs = ViewabilityConfigCallbackPair[];
/**
* @see https://reactnative.dev/docs/flatlist#props
*/
export interface ListRenderItemInfo<ItemT> {
item: ItemT;
index: number;
separators: {
highlight: () => void;
unhighlight: () => void;
updateProps: (select: 'leading' | 'trailing', newProps: any) => void;
};
}
export type ListRenderItem<ItemT> = (
info: ListRenderItemInfo<ItemT>,
) => React.ReactElement | null;
export interface CellRendererProps<ItemT> {
cellKey: string;
children: React.ReactNode;
index: number;
item: ItemT;
onFocusCapture?: ((event: FocusEvent) => void) | undefined;
onLayout?: ((event: LayoutChangeEvent) => void) | undefined;
style: StyleProp<ViewStyle> | undefined;
}
/**
* @see https://reactnative.dev/docs/virtualizedlist
*/
export class VirtualizedList<ItemT> extends React.Component<
VirtualizedListProps<ItemT>
> {
scrollToEnd: (params?: {animated?: boolean | undefined}) => void;
scrollToIndex: (params: {
animated?: boolean | undefined;
index: number;
viewOffset?: number | undefined;
viewPosition?: number | undefined;
}) => void;
scrollToItem: (params: {
animated?: boolean | undefined;
item: ItemT;
viewOffset?: number | undefined;
viewPosition?: number | undefined;
}) => void;
/**
* Scroll to a specific content pixel offset in the list.
* Param `offset` expects the offset to scroll to. In case of horizontal is true, the
* offset is the x-value, in any other case the offset is the y-value.
* Param `animated` (true by default) defines whether the list should do an animation while scrolling.
*/
scrollToOffset: (params: {
animated?: boolean | undefined;
offset: number;
}) => void;
recordInteraction: () => void;
getScrollRef: () =>
| React.ComponentRef<typeof ScrollView>
| React.ComponentRef<typeof View>
| null;
getScrollResponder: () => ScrollResponderMixin | null;
}
/**
* @see https://reactnative.dev/docs/virtualizedlist#props
*/
export interface VirtualizedListProps<ItemT>
extends VirtualizedListWithoutRenderItemProps<ItemT> {
renderItem: ListRenderItem<ItemT> | null | undefined;
}
export interface VirtualizedListWithoutRenderItemProps<ItemT>
extends ScrollViewProps {
/**
* Rendered in between each item, but not at the top or bottom
*/
ItemSeparatorComponent?:
| React.ComponentType<any>
| React.ReactElement
| null
| undefined;
/**
* Rendered when the list is empty. Can be a React Component Class, a render function, or
* a rendered element.
*/
ListEmptyComponent?:
| React.ComponentType<any>
| React.ReactElement
| null
| undefined;
/**
* Rendered at the bottom of all the items. Can be a React Component Class, a render function, or
* a rendered element.
*/
ListFooterComponent?:
| React.ComponentType<any>
| React.ReactElement
| null
| undefined;
/**
* Styling for internal View for ListFooterComponent
*/
ListFooterComponentStyle?: StyleProp<ViewStyle> | undefined;
/**
* Rendered at the top of all the items. Can be a React Component Class, a render function, or
* a rendered element.
*/
ListHeaderComponent?:
| React.ComponentType<any>
| React.ReactElement
| null
| undefined;
/**
* Styling for internal View for ListHeaderComponent
*/
ListHeaderComponentStyle?: StyleProp<ViewStyle> | undefined;
/**
* The default accessor functions assume this is an Array<{key: string}> but you can override
* getItem, getItemCount, and keyExtractor to handle any type of index-based data.
*/
data?: any | undefined;
/**
* `debug` will turn on extra logging and visual overlays to aid with debugging both usage and
* implementation, but with a significant perf hit.
*/
debug?: boolean | undefined;
/**
* DEPRECATED: Virtualization provides significant performance and memory optimizations, but fully
* unmounts react instances that are outside of the render window. You should only need to disable
* this for debugging purposes.
*/
disableVirtualization?: boolean | undefined;
/**
* A marker property for telling the list to re-render (since it implements `PureComponent`). If
* any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the
* `data` prop, stick it here and treat it immutably.
*/
extraData?: any | undefined;
/**
* A generic accessor for extracting an item from any sort of data blob.
*/
getItem?: ((data: any, index: number) => ItemT) | undefined;
/**
* Determines how many items are in the data blob.
*/
getItemCount?: ((data: any) => number) | undefined;
getItemLayout?:
| ((
data: any,
index: number,
) => {
length: number;
offset: number;
index: number;
})
| undefined;
horizontal?: boolean | null | undefined;
/**
* How many items to render in the initial batch. This should be enough to fill the screen but not
* much more. Note these items will never be unmounted as part of the windowed rendering in order
* to improve perceived performance of scroll-to-top actions.
*/
initialNumToRender?: number | undefined;
/**
* Instead of starting at the top with the first item, start at `initialScrollIndex`. This
* disables the "scroll to top" optimization that keeps the first `initialNumToRender` items
* always rendered and immediately renders the items starting at this initial index. Requires
* `getItemLayout` to be implemented.
*/
initialScrollIndex?: number | null | undefined;
/**
* Reverses the direction of scroll. Uses scale transforms of -1.
*/
inverted?: boolean | null | undefined;
keyExtractor?: ((item: ItemT, index: number) => string) | undefined;
/**
* The maximum number of items to render in each incremental render batch. The more rendered at
* once, the better the fill rate, but responsiveness may suffer because rendering content may
* interfere with responding to button taps or other interactions.
*/
maxToRenderPerBatch?: number | undefined;
/**
* Called once when the scroll position gets within within `onEndReachedThreshold`
* from the logical end of the list.
*/
onEndReached?: ((info: {distanceFromEnd: number}) => void) | null | undefined;
/**
* How far from the end (in units of visible length of the list) the trailing edge of the
* list must be from the end of the content to trigger the `onEndReached` callback.
* Thus, a value of 0.5 will trigger `onEndReached` when the end of the content is
* within half the visible length of the list.
*/
onEndReachedThreshold?: number | null | undefined;
onLayout?: ((event: LayoutChangeEvent) => void) | undefined;
/**
* If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make
* sure to also set the `refreshing` prop correctly.
*/
onRefresh?: (() => void) | null | undefined;
/**
* Used to handle failures when scrolling to an index that has not been measured yet.
* Recommended action is to either compute your own offset and `scrollTo` it, or scroll as far
* as possible and then try again after more items have been rendered.
*/
onScrollToIndexFailed?:
| ((info: {
index: number;
highestMeasuredFrameIndex: number;
averageItemLength: number;
}) => void)
| undefined;
/**
* Called once when the scroll position gets within within `onStartReachedThreshold`
* from the logical start of the list.
*/
onStartReached?:
| ((info: {distanceFromStart: number}) => void)
| null
| undefined;
/**
* How far from the start (in units of visible length of the list) the leading edge of the
* list must be from the start of the content to trigger the `onStartReached` callback.
* Thus, a value of 0.5 will trigger `onStartReached` when the start of the content is
* within half the visible length of the list.
*/
onStartReachedThreshold?: number | null | undefined;
/**
* Called when the viewability of rows changes, as defined by the
* `viewabilityConfig` prop.
*/
onViewableItemsChanged?:
| ((info: {
viewableItems: Array<ViewToken<ItemT>>;
changed: Array<ViewToken<ItemT>>;
}) => void)
| null
| undefined;
/**
* Set this when offset is needed for the loading indicator to show correctly.
* @platform android
*/
progressViewOffset?: number | undefined;
/**
* Set this true while waiting for new data from a refresh.
*/
refreshing?: boolean | null | undefined;
/**
* Note: may have bugs (missing content) in some circumstances - use at your own risk.
*
* This may improve scroll performance for large lists.
*/
removeClippedSubviews?: boolean | undefined;
/**
* Render a custom scroll component, e.g. with a differently styled `RefreshControl`.
*/
renderScrollComponent?:
| ((props: ScrollViewProps) => React.ReactElement<ScrollViewProps>)
| undefined;
/**
* Amount of time between low-pri item render batches, e.g. for rendering items quite a ways off
* screen. Similar fill rate/responsiveness tradeoff as `maxToRenderPerBatch`.
*/
updateCellsBatchingPeriod?: number | undefined;
viewabilityConfig?: ViewabilityConfig | undefined;
viewabilityConfigCallbackPairs?: ViewabilityConfigCallbackPairs | undefined;
/**
* Determines the maximum number of items rendered outside of the visible area, in units of
* visible lengths. So if your list fills the screen, then `windowSize={21}` (the default) will
* render the visible screen area plus up to 10 screens above and 10 below the viewport. Reducing
* this number will reduce memory consumption and may improve performance, but will increase the
* chance that fast scrolling may reveal momentary blank areas of unrendered content.
*/
windowSize?: number | undefined;
/**
* CellRendererComponent allows customizing how cells rendered by
* `renderItem`/`ListItemComponent` are wrapped when placed into the
* underlying ScrollView. This component must accept event handlers which
* notify VirtualizedList of changes within the cell.
*/
CellRendererComponent?:
| React.ComponentType<CellRendererProps<ItemT>>
| null
| undefined;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,253 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
import type {CellRendererProps, ListRenderItem} from './VirtualizedListProps';
import type {
FocusEvent,
LayoutChangeEvent,
StyleProp,
ViewStyle,
} from 'react-native';
import {VirtualizedListCellContextProvider} from './VirtualizedListContext.js';
import invariant from 'invariant';
import * as React from 'react';
import {isValidElement} from 'react';
import {StyleSheet, View} from 'react-native';
export type Props<ItemT> = {
CellRendererComponent?: ?React.ComponentType<CellRendererProps<ItemT>>,
ItemSeparatorComponent?: ?(
| React.ComponentType<any | {highlighted: boolean, leadingItem: ?ItemT}>
| React.MixedElement
),
ListItemComponent?: ?(React.ComponentType<any> | React.MixedElement),
cellKey: string,
horizontal: ?boolean,
index: number,
inversionStyle: StyleProp<ViewStyle>,
item: ItemT,
onCellLayout?: (
event: LayoutChangeEvent,
cellKey: string,
index: number,
) => void,
onCellFocusCapture?: (cellKey: string) => void,
onUnmount: (cellKey: string) => void,
onUpdateSeparators: (
cellKeys: Array<?string>,
props: Partial<SeparatorProps<ItemT>>,
) => void,
prevCellKey: ?string,
renderItem?: ?ListRenderItem<ItemT>,
...
};
type SeparatorProps<ItemT> = Readonly<{
highlighted: boolean,
leadingItem: ?ItemT,
}>;
type State<ItemT> = {
separatorProps: SeparatorProps<ItemT>,
...
};
export default class CellRenderer<ItemT> extends React.PureComponent<
Props<ItemT>,
State<ItemT>,
> {
state: State<ItemT> = {
separatorProps: {
highlighted: false,
leadingItem: this.props.item,
},
};
static getDerivedStateFromProps<StaticItemT>(
props: Props<StaticItemT>,
prevState: State<StaticItemT>,
): ?State<StaticItemT> {
if (props.item !== prevState.separatorProps.leadingItem) {
return {
separatorProps: {
...prevState.separatorProps,
leadingItem: props.item,
},
};
}
return null;
}
// TODO: consider factoring separator stuff out of VirtualizedList into FlatList since it's not
// reused by SectionList and we can keep VirtualizedList simpler.
// $FlowFixMe[missing-local-annot]
_separators = {
highlight: () => {
const {cellKey, prevCellKey} = this.props;
this.props.onUpdateSeparators([cellKey, prevCellKey], {
highlighted: true,
});
},
unhighlight: () => {
const {cellKey, prevCellKey} = this.props;
this.props.onUpdateSeparators([cellKey, prevCellKey], {
highlighted: false,
});
},
updateProps: (
select: 'leading' | 'trailing',
newProps: SeparatorProps<ItemT>,
) => {
const {cellKey, prevCellKey} = this.props;
this.props.onUpdateSeparators(
[select === 'leading' ? prevCellKey : cellKey],
newProps,
);
},
};
updateSeparatorProps(newProps: SeparatorProps<ItemT>) {
this.setState(state => ({
separatorProps: {...state.separatorProps, ...newProps},
}));
}
componentWillUnmount() {
this.props.onUnmount(this.props.cellKey);
}
_onLayout = (nativeEvent: LayoutChangeEvent): void => {
this.props.onCellLayout?.(
nativeEvent,
this.props.cellKey,
this.props.index,
);
};
_onCellFocusCapture = (e: FocusEvent): void => {
this.props.onCellFocusCapture?.(this.props.cellKey);
};
_renderElement(
renderItem: ?ListRenderItem<ItemT>,
ListItemComponent: any,
item: ItemT,
index: number,
): React.Node {
if (renderItem && ListItemComponent) {
console.warn(
'VirtualizedList: Both ListItemComponent and renderItem props are present. ListItemComponent will take' +
' precedence over renderItem.',
);
}
if (ListItemComponent) {
return (
<ListItemComponent
item={item}
index={index}
separators={this._separators}
/>
);
}
if (renderItem) {
return renderItem({
item,
index,
separators: this._separators,
});
}
invariant(
false,
'VirtualizedList: Either ListItemComponent or renderItem props are required but none were found.',
);
}
render(): React.Node {
const {
CellRendererComponent,
ItemSeparatorComponent,
ListItemComponent,
cellKey,
horizontal,
item,
index,
inversionStyle,
onCellLayout,
renderItem,
} = this.props;
const element = this._renderElement(
renderItem,
ListItemComponent,
item,
index,
);
// NOTE: that when this is a sticky header, `onLayout` will get automatically extracted and
// called explicitly by `ScrollViewStickyHeader`.
const itemSeparator: React.Node = isValidElement(ItemSeparatorComponent)
? // $FlowFixMe[incompatible-type]
ItemSeparatorComponent
: // $FlowFixMe[incompatible-type]
ItemSeparatorComponent && (
// $FlowFixMe[incompatible-type]
// $FlowFixMe[not-a-component]
<ItemSeparatorComponent {...this.state.separatorProps} />
);
const cellStyle = inversionStyle
? horizontal
? [styles.rowReverse, inversionStyle]
: [styles.columnReverse, inversionStyle]
: horizontal
? [styles.row, inversionStyle]
: inversionStyle;
const result = !CellRendererComponent ? (
<View
style={cellStyle}
onFocusCapture={this._onCellFocusCapture}
{...(onCellLayout && {onLayout: this._onLayout})}>
{element}
{itemSeparator}
</View>
) : (
<CellRendererComponent
cellKey={cellKey}
index={index}
item={item}
style={cellStyle}
onFocusCapture={this._onCellFocusCapture}
{...(onCellLayout && {onLayout: this._onLayout})}>
{element}
{itemSeparator}
</CellRendererComponent>
);
return (
<VirtualizedListCellContextProvider cellKey={this.props.cellKey}>
{result}
</VirtualizedListCellContextProvider>
);
}
}
const styles = StyleSheet.create({
row: {
flexDirection: 'row',
},
rowReverse: {
flexDirection: 'row-reverse',
},
columnReverse: {
flexDirection: 'column-reverse',
},
});

View File

@@ -0,0 +1,111 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type VirtualizedList from './VirtualizedList';
import * as React from 'react';
import {createContext, useContext, useMemo} from 'react';
type Context = Readonly<{
cellKey: ?string,
getScrollMetrics: () => {
contentLength: number,
dOffset: number,
dt: number,
offset: number,
timestamp: number,
velocity: number,
visibleLength: number,
zoomScale: number,
},
horizontal: ?boolean,
getOutermostParentListRef: () => VirtualizedList,
registerAsNestedChild: ({cellKey: string, ref: VirtualizedList}) => void,
unregisterAsNestedChild: ({ref: VirtualizedList}) => void,
}>;
export const VirtualizedListContext: React.Context<?Context> =
createContext(null);
if (__DEV__) {
VirtualizedListContext.displayName = 'VirtualizedListContext';
}
/**
* Resets the context. Intended for use by portal-like components (e.g. Modal).
*/
export function VirtualizedListContextResetter({
children,
}: {
children: React.Node,
}): React.Node {
return (
<VirtualizedListContext.Provider value={null}>
{children}
</VirtualizedListContext.Provider>
);
}
/**
* Sets the context with memoization. Intended to be used by `VirtualizedList`.
*/
export function VirtualizedListContextProvider({
children,
value,
}: {
children: React.Node,
value: Context,
}): React.Node {
// Avoid setting a newly created context object if the values are identical.
const context = useMemo(
() => ({
cellKey: null,
getScrollMetrics: value.getScrollMetrics,
horizontal: value.horizontal,
getOutermostParentListRef: value.getOutermostParentListRef,
registerAsNestedChild: value.registerAsNestedChild,
unregisterAsNestedChild: value.unregisterAsNestedChild,
}),
[
value.getScrollMetrics,
value.horizontal,
value.getOutermostParentListRef,
value.registerAsNestedChild,
value.unregisterAsNestedChild,
],
);
return (
<VirtualizedListContext.Provider value={context}>
{children}
</VirtualizedListContext.Provider>
);
}
/**
* Sets the `cellKey`. Intended to be used by `VirtualizedList` for each cell.
*/
export function VirtualizedListCellContextProvider({
cellKey,
children,
}: {
cellKey: string,
children: React.Node,
}): React.Node {
// Avoid setting a newly created context object if the values are identical.
const currContext = useContext(VirtualizedListContext);
const context = useMemo(
() => (currContext == null ? null : {...currContext, cellKey}),
[currContext, cellKey],
);
return (
<VirtualizedListContext.Provider value={context}>
{children}
</VirtualizedListContext.Provider>
);
}

View File

@@ -0,0 +1,334 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
import type {
ViewabilityConfig,
ViewabilityConfigCallbackPair,
ViewToken,
} from './ViewabilityHelper';
import type {
FocusEvent,
LayoutChangeEvent,
ScrollViewProps,
StyleProp,
ViewStyle,
} from 'react-native';
import * as React from 'react';
export type Item = any;
export type Separators = {
highlight: () => void,
unhighlight: () => void,
updateProps: (select: 'leading' | 'trailing', newProps: Object) => void,
...
};
export type ListRenderItemInfo<ItemT> = {
item: ItemT,
index: number,
separators: Separators,
...
};
export type CellRendererProps<ItemT> = Readonly<{
cellKey: string,
children: React.Node,
index: number,
item: ItemT,
onFocusCapture?: (event: FocusEvent) => void,
onLayout?: (event: LayoutChangeEvent) => void,
style: StyleProp<ViewStyle>,
}>;
export type ListRenderItem<ItemT> = (
info: ListRenderItemInfo<ItemT>,
) => React.Node;
type RequiredVirtualizedListProps = {
/**
* The default accessor functions assume this is an Array<{key: string} | {id: string}> but you can override
* getItem, getItemCount, and keyExtractor to handle any type of index-based data.
*/
data?: any,
/**
* A generic accessor for extracting an item from any sort of data blob.
*/
getItem: (data: any, index: number) => ?Item,
/**
* Determines how many items are in the data blob.
*/
getItemCount: (data: any) => number,
};
type OptionalVirtualizedListProps = {
renderItem?: ?ListRenderItem<Item>,
/**
* `debug` will turn on extra logging and visual overlays to aid with debugging both usage and
* implementation, but with a significant perf hit.
*/
debug?: ?boolean,
/**
* DEPRECATED: Virtualization provides significant performance and memory optimizations, but fully
* unmounts react instances that are outside of the render window. You should only need to disable
* this for debugging purposes. Defaults to false.
*/
disableVirtualization?: ?boolean,
/**
* A marker property for telling the list to re-render (since it implements `PureComponent`). If
* any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the
* `data` prop, stick it here and treat it immutably.
*/
extraData?: any,
// e.g. height, y
getItemLayout?: (
data: any,
index: number,
) => {
length: number,
offset: number,
index: number,
...
},
horizontal?: ?boolean,
/**
* How many items to render in the initial batch. This should be enough to fill the screen but not
* much more. Note these items will never be unmounted as part of the windowed rendering in order
* to improve perceived performance of scroll-to-top actions.
*/
initialNumToRender?: ?number,
/**
* Instead of starting at the top with the first item, start at `initialScrollIndex`. This
* disables the "scroll to top" optimization that keeps the first `initialNumToRender` items
* always rendered and immediately renders the items starting at this initial index. Requires
* `getItemLayout` to be implemented.
*/
initialScrollIndex?: ?number,
/**
* Reverses the direction of scroll. Uses scale transforms of -1.
*/
inverted?: ?boolean,
keyExtractor?: ?(item: Item, index: number) => string,
/**
* CellRendererComponent allows customizing how cells rendered by
* `renderItem`/`ListItemComponent` are wrapped when placed into the
* underlying ScrollView. This component must accept event handlers which
* notify VirtualizedList of changes within the cell.
*/
CellRendererComponent?: ?React.ComponentType<CellRendererProps<Item>>,
/**
* Rendered in between each item, but not at the top or bottom. By default, `highlighted` and
* `leadingItem` props are provided. `renderItem` provides `separators.highlight`/`unhighlight`
* which will update the `highlighted` prop, but you can also add custom props with
* `separators.updateProps`.
*/
ItemSeparatorComponent?: ?(React.ComponentType<any> | React.MixedElement),
/**
* Takes an item from `data` and renders it into the list. Example usage:
*
* <FlatList
* ItemSeparatorComponent={Platform.OS !== 'android' && ({highlighted}) => (
* <View style={[style.separator, highlighted && {marginLeft: 0}]} />
* )}
* data={[{title: 'Title Text', key: 'item1'}]}
* ListItemComponent={({item, separators}) => (
* <TouchableHighlight
* onPress={() => this._onPress(item)}
* onShowUnderlay={separators.highlight}
* onHideUnderlay={separators.unhighlight}>
* <View style={{backgroundColor: 'white'}}>
* <Text>{item.title}</Text>
* </View>
* </TouchableHighlight>
* )}
* />
*
* Provides additional metadata like `index` if you need it, as well as a more generic
* `separators.updateProps` function which let's you set whatever props you want to change the
* rendering of either the leading separator or trailing separator in case the more common
* `highlight` and `unhighlight` (which set the `highlighted: boolean` prop) are insufficient for
* your use-case.
*/
ListItemComponent?: ?(React.ComponentType<any> | React.MixedElement),
/**
* Rendered when the list is empty. Can be a React Component Class, a render function, or
* a rendered element.
*/
ListEmptyComponent?: ?(React.ComponentType<any> | React.MixedElement),
/**
* Rendered at the bottom of all the items. Can be a React Component Class, a render function, or
* a rendered element.
*/
ListFooterComponent?: ?(React.ComponentType<any> | React.MixedElement),
/**
* Styling for internal View for ListFooterComponent
*/
ListFooterComponentStyle?: StyleProp<ViewStyle>,
/**
* Rendered at the top of all the items. Can be a React Component Class, a render function, or
* a rendered element.
*/
ListHeaderComponent?: ?(React.ComponentType<any> | React.MixedElement),
/**
* Styling for internal View for ListHeaderComponent
*/
ListHeaderComponentStyle?: StyleProp<ViewStyle>,
/**
* The maximum number of items to render in each incremental render batch. The more rendered at
* once, the better the fill rate, but responsiveness may suffer because rendering content may
* interfere with responding to button taps or other interactions.
*/
maxToRenderPerBatch?: ?number,
/**
* Called once when the scroll position gets within within `onEndReachedThreshold`
* from the logical end of the list.
*/
onEndReached?: ?(info: {distanceFromEnd: number, ...}) => void,
/**
* How far from the end (in units of visible length of the list) the trailing edge of the
* list must be from the end of the content to trigger the `onEndReached` callback.
* Thus, a value of 0.5 will trigger `onEndReached` when the end of the content is
* within half the visible length of the list.
*/
onEndReachedThreshold?: ?number,
/**
* If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make
* sure to also set the `refreshing` prop correctly.
*/
onRefresh?: ?() => void,
/**
* Used to handle failures when scrolling to an index that has not been measured yet. Recommended
* action is to either compute your own offset and `scrollTo` it, or scroll as far as possible and
* then try again after more items have been rendered.
*/
onScrollToIndexFailed?: ?(info: {
index: number,
highestMeasuredFrameIndex: number,
averageItemLength: number,
...
}) => void,
/**
* Called once when the scroll position gets within within `onStartReachedThreshold`
* from the logical start of the list.
*/
onStartReached?: ?(info: {distanceFromStart: number, ...}) => void,
/**
* How far from the start (in units of visible length of the list) the leading edge of the
* list must be from the start of the content to trigger the `onStartReached` callback.
* Thus, a value of 0.5 will trigger `onStartReached` when the start of the content is
* within half the visible length of the list.
*/
onStartReachedThreshold?: ?number,
/**
* Called when the viewability of rows changes, as defined by the
* `viewabilityConfig` prop.
*/
onViewableItemsChanged?: ?(info: {
viewableItems: Array<ViewToken>,
changed: Array<ViewToken>,
...
}) => void,
persistentScrollbar?: ?boolean,
/**
* Set this when offset is needed for the loading indicator to show correctly.
*/
progressViewOffset?: number,
/**
* A custom refresh control element. When set, it overrides the default
* <RefreshControl> component built internally. The onRefresh and refreshing
* props are also ignored. Only works for vertical VirtualizedList.
*/
refreshControl?: ?React.MixedElement,
/**
* Set this true while waiting for new data from a refresh.
*/
refreshing?: ?boolean,
/**
* Note: may have bugs (missing content) in some circumstances - use at your own risk.
*
* This may improve scroll performance for large lists.
*/
removeClippedSubviews?: boolean,
/**
* Render a custom scroll component, e.g. with a differently styled `RefreshControl`.
*/
renderScrollComponent?: (props: ScrollViewProps) => React.MixedElement,
/**
* Amount of time between low-pri item render batches, e.g. for rendering items quite a ways off
* screen. Similar fill rate/responsiveness tradeoff as `maxToRenderPerBatch`.
*/
updateCellsBatchingPeriod?: ?number,
/**
* See `ViewabilityHelper` for flow type and further documentation.
*/
viewabilityConfig?: ViewabilityConfig,
/**
* List of ViewabilityConfig/onViewableItemsChanged pairs. A specific onViewableItemsChanged
* will be called when its corresponding ViewabilityConfig's conditions are met.
*/
viewabilityConfigCallbackPairs?: Array<ViewabilityConfigCallbackPair>,
/**
* Determines the maximum number of items rendered outside of the visible area, in units of
* visible lengths. So if your list fills the screen, then `windowSize={21}` (the default) will
* render the visible screen area plus up to 10 screens above and 10 below the viewport. Reducing
* this number will reduce memory consumption and may improve performance, but will increase the
* chance that fast scrolling may reveal momentary blank areas of unrendered content.
*/
windowSize?: ?number,
};
export type VirtualizedListProps = {
...ScrollViewProps,
...RequiredVirtualizedListProps,
...OptionalVirtualizedListProps,
};
/**
* Default Props Helper Functions
* Use the following helper functions for default values
*/
// horizontalOrDefault(this.props.horizontal)
export function horizontalOrDefault(horizontal: ?boolean): boolean {
return horizontal ?? false;
}
// initialNumToRenderOrDefault(this.props.initialNumToRender)
export function initialNumToRenderOrDefault(
initialNumToRender: ?number,
): number {
return initialNumToRender ?? 10;
}
// maxToRenderPerBatchOrDefault(this.props.maxToRenderPerBatch)
export function maxToRenderPerBatchOrDefault(
maxToRenderPerBatch: ?number,
): number {
return maxToRenderPerBatch ?? 10;
}
// onStartReachedThresholdOrDefault(this.props.onStartReachedThreshold)
export function onStartReachedThresholdOrDefault(
onStartReachedThreshold: ?number,
): number {
return onStartReachedThreshold ?? 2;
}
// onEndReachedThresholdOrDefault(this.props.onEndReachedThreshold)
export function onEndReachedThresholdOrDefault(
onEndReachedThreshold: ?number,
): number {
return onEndReachedThreshold ?? 2;
}
// windowSizeOrDefault(this.props.windowSize)
export function windowSizeOrDefault(windowSize: ?number): number {
return windowSize ?? 21;
}

View File

@@ -0,0 +1,665 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
import type {ViewToken} from './ViewabilityHelper';
import type {VirtualizedListProps} from './VirtualizedListProps';
import VirtualizedList from './VirtualizedList';
import {keyExtractor as defaultKeyExtractor} from './VirtualizeUtils';
import invariant from 'invariant';
import * as React from 'react';
import {useEffect, useState} from 'react';
type DefaultVirtualizedSectionT = {
data: any,
[key: string]: any,
};
export type SectionData<SectionItemT, SectionT = DefaultVirtualizedSectionT> =
| (Readonly<SectionBase<SectionItemT, SectionT>> & SectionT)
| (SectionBase<SectionItemT, SectionT> & SectionT)
| SectionT;
export type SectionBase<SectionItemT, SectionT = DefaultVirtualizedSectionT> = {
/**
* The data for rendering items in this section.
*/
data: ReadonlyArray<SectionItemT>,
/**
* Optional key to keep track of section re-ordering. If you don't plan on re-ordering sections,
* the array index will be used by default.
*/
key?: string,
// Optional props will override list-wide props just for this section.
renderItem?: ?(info: {
item: SectionItemT,
index: number,
section: SectionData<SectionItemT, SectionT>,
separators: {
highlight: () => void,
unhighlight: () => void,
updateProps: (select: 'leading' | 'trailing', newProps: Object) => void,
...
},
...
}) => null | React.MixedElement,
ItemSeparatorComponent?: ?(React.ComponentType<any> | React.MixedElement),
keyExtractor?: (item: ?SectionItemT, index?: ?number) => string,
...
};
type RequiredVirtualizedSectionListProps<
ItemT,
SectionT = DefaultVirtualizedSectionT,
> = {
sections: ReadonlyArray<SectionData<ItemT, SectionT>>,
};
type OptionalVirtualizedSectionListProps<
ItemT,
SectionT = DefaultVirtualizedSectionT,
> = {
/**
* Default renderer for every item in every section.
*/
renderItem?: (info: {
item: ItemT,
index: number,
section: SectionT,
separators: {
highlight: () => void,
unhighlight: () => void,
updateProps: (select: 'leading' | 'trailing', newProps: Object) => void,
...
},
...
}) => null | React.Node,
/**
* Rendered at the top of each section. These stick to the top of the `ScrollView` by default on
* iOS. See `stickySectionHeadersEnabled`.
*/
renderSectionHeader?: ?(info: {section: SectionT, ...}) => null | React.Node,
/**
* Rendered at the bottom of each section.
*/
renderSectionFooter?: ?(info: {section: SectionT, ...}) => null | React.Node,
/**
* Rendered at the top and bottom of each section (note this is different from
* `ItemSeparatorComponent` which is only rendered between items). These are intended to separate
* sections from the headers above and below and typically have the same highlight response as
* `ItemSeparatorComponent`. Also receives `highlighted`, `[leading/trailing][Item/Separator]`,
* and any custom props from `separators.updateProps`.
*/
SectionSeparatorComponent?: ?React.ComponentType<any>,
/**
* Makes section headers stick to the top of the screen until the next one pushes it off. Only
* enabled by default on iOS because that is the platform standard there.
*/
stickySectionHeadersEnabled?: boolean,
onEndReached?: ?({distanceFromEnd: number, ...}) => void,
};
export type VirtualizedSectionListProps<
ItemT,
SectionT = DefaultVirtualizedSectionT,
> = {
...RequiredVirtualizedSectionListProps<ItemT, SectionT>,
...OptionalVirtualizedSectionListProps<ItemT, SectionT>,
...Omit<VirtualizedListProps, 'data' | 'renderItem'>,
};
export type ScrollToLocationParamsType = {
animated?: ?boolean,
itemIndex: number,
sectionIndex: number,
viewOffset?: number,
viewPosition?: number,
};
type State = {childProps: VirtualizedListProps, ...};
/**
* Right now this just flattens everything into one list and uses VirtualizedList under the
* hood. The only operation that might not scale well is concatting the data arrays of all the
* sections when new props are received, which should be plenty fast for up to ~10,000 items.
*/
class VirtualizedSectionList<
ItemT,
SectionT: SectionBase<
ItemT,
DefaultVirtualizedSectionT,
> = DefaultVirtualizedSectionT,
> extends React.PureComponent<
VirtualizedSectionListProps<ItemT, SectionT>,
State,
> {
scrollToLocation(params: ScrollToLocationParamsType) {
let index = params.itemIndex;
for (let i = 0; i < params.sectionIndex; i++) {
index += this.props.getItemCount(this.props.sections[i].data) + 2;
}
let viewOffset = params.viewOffset || 0;
if (this._listRef == null) {
return;
}
const listRef = this._listRef;
if (params.itemIndex > 0 && this.props.stickySectionHeadersEnabled) {
const frame = listRef
.__getListMetrics()
.getCellMetricsApprox(index - params.itemIndex, listRef.props);
viewOffset += frame.length;
}
const toIndexParams: {
animated?: ?boolean,
index: number,
viewOffset?: number,
viewPosition?: number,
...
} = {
...params,
viewOffset,
index,
};
// $FlowFixMe[incompatible-use]
this._listRef.scrollToIndex(toIndexParams);
}
getListRef(): ?VirtualizedList {
return this._listRef;
}
render(): React.Node {
const {
ItemSeparatorComponent, // don't pass through, rendered with renderItem
SectionSeparatorComponent,
renderItem: _renderItem,
renderSectionFooter,
renderSectionHeader,
sections: _sections,
stickySectionHeadersEnabled,
...passThroughProps
} = this.props;
const listHeaderOffset = this.props.ListHeaderComponent ? 1 : 0;
const stickyHeaderIndices = this.props.stickySectionHeadersEnabled
? ([]: Array<number>)
: undefined;
let itemCount = 0;
for (const section of this.props.sections) {
// Track the section header indices
if (stickyHeaderIndices != null) {
stickyHeaderIndices.push(itemCount + listHeaderOffset);
}
// Add two for the section header and footer.
itemCount += 2;
itemCount += this.props.getItemCount(section.data);
}
const renderItem = this._renderItem(itemCount);
return (
<VirtualizedList
{...passThroughProps}
keyExtractor={this._keyExtractor}
stickyHeaderIndices={stickyHeaderIndices}
renderItem={renderItem}
data={this.props.sections}
getItem={(sections, index) =>
this._getItem(this.props, sections, index)
}
getItemCount={() => itemCount}
onViewableItemsChanged={
this.props.onViewableItemsChanged
? this._onViewableItemsChanged
: undefined
}
ref={this._captureRef}
/>
);
}
_getItem(
props: VirtualizedSectionListProps<ItemT, SectionT>,
sections: ?ReadonlyArray<SectionData<ItemT, SectionT>>,
index: number,
): ?ItemT {
if (!sections) {
return null;
}
let itemIdx = index - 1;
for (let i = 0; i < sections.length; i++) {
const section = sections[i];
const sectionData = section.data;
const itemCount = props.getItemCount(sectionData);
if (itemIdx === -1 || itemIdx === itemCount) {
// We intend for there to be overflow by one on both ends of the list.
// This will be for headers and footers. When returning a header or footer
// item the section itself is the item.
// $FlowFixMe[incompatible-type]
return section;
} else if (itemIdx < itemCount) {
// If we are in the bounds of the list's data then return the item.
return props.getItem(sectionData, itemIdx);
} else {
itemIdx -= itemCount + 2; // Add two for the header and footer
}
}
return null;
}
// $FlowFixMe[missing-local-annot]
_keyExtractor = (item: ItemT, index: number) => {
const info = this._subExtractor(index);
return (info && info.key) || String(index);
};
_subExtractor(index: number): ?{
section: SectionData<ItemT, SectionT>,
// Key of the section or combined key for section + item
key: string,
// Relative index within the section
index: ?number,
// True if this is the section header
header?: ?boolean,
leadingItem?: ?ItemT,
leadingSection?: ?SectionData<ItemT, SectionT>,
trailingItem?: ?ItemT,
trailingSection?: ?SectionData<ItemT, SectionT>,
...
} {
let itemIndex = index;
const {getItem, getItemCount, keyExtractor, sections} = this.props;
for (let i = 0; i < sections.length; i++) {
const section = sections[i];
const sectionData = section.data;
const key = section.key || String(i);
itemIndex -= 1; // The section adds an item for the header
if (itemIndex >= getItemCount(sectionData) + 1) {
itemIndex -= getItemCount(sectionData) + 1; // The section adds an item for the footer.
} else if (itemIndex === -1) {
return {
section,
key: key + ':header',
index: null,
header: true,
trailingSection: sections[i + 1],
};
} else if (itemIndex === getItemCount(sectionData)) {
return {
section,
key: key + ':footer',
index: null,
header: false,
trailingSection: sections[i + 1],
};
} else {
const extractor =
section.keyExtractor || keyExtractor || defaultKeyExtractor;
return {
section,
key:
key + ':' + extractor(getItem(sectionData, itemIndex), itemIndex),
index: itemIndex,
leadingItem: getItem(sectionData, itemIndex - 1),
leadingSection: sections[i - 1],
trailingItem: getItem(sectionData, itemIndex + 1),
trailingSection: sections[i + 1],
};
}
}
}
_convertViewable = (viewable: ViewToken): ?ViewToken => {
invariant(viewable.index != null, 'Received a broken ViewToken');
const info = this._subExtractor(viewable.index);
if (!info) {
return null;
}
const keyExtractorWithNullableIndex = info.section.keyExtractor;
const keyExtractorWithNonNullableIndex =
this.props.keyExtractor || defaultKeyExtractor;
const key =
keyExtractorWithNullableIndex != null
? keyExtractorWithNullableIndex(viewable.item, info.index)
: keyExtractorWithNonNullableIndex(viewable.item, info.index ?? 0);
return {
...viewable,
index: info.index,
key,
section: info.section,
};
};
_onViewableItemsChanged = ({
viewableItems,
changed,
}: {
viewableItems: Array<ViewToken>,
changed: Array<ViewToken>,
...
}) => {
const onViewableItemsChanged = this.props.onViewableItemsChanged;
if (onViewableItemsChanged != null) {
onViewableItemsChanged({
viewableItems: viewableItems
.map(this._convertViewable, this)
.filter(Boolean),
changed: changed.map(this._convertViewable, this).filter(Boolean),
});
}
};
_renderItem =
(listItemCount: number): $FlowFixMe =>
// eslint-disable-next-line react/no-unstable-nested-components
({item, index}: {item: ItemT, index: number, ...}) => {
const info = this._subExtractor(index);
if (!info) {
return null;
}
const infoIndex = info.index;
if (infoIndex == null) {
const {section} = info;
if (info.header === true) {
const {renderSectionHeader} = this.props;
return renderSectionHeader ? renderSectionHeader({section}) : null;
} else {
const {renderSectionFooter} = this.props;
return renderSectionFooter ? renderSectionFooter({section}) : null;
}
} else {
const renderItem = info.section.renderItem || this.props.renderItem;
const SeparatorComponent = this._getSeparatorComponent(
index,
info,
listItemCount,
);
invariant(renderItem, 'no renderItem!');
return (
<ItemWithSeparator
SeparatorComponent={SeparatorComponent}
LeadingSeparatorComponent={
infoIndex === 0 ? this.props.SectionSeparatorComponent : undefined
}
cellKey={info.key}
index={infoIndex}
item={item}
leadingItem={info.leadingItem}
leadingSection={info.leadingSection}
prevCellKey={(this._subExtractor(index - 1) || {}).key}
// Callback to provide updateHighlight for this item
setSelfHighlightCallback={this._setUpdateHighlightFor}
setSelfUpdatePropsCallback={this._setUpdatePropsFor}
// Provide child ability to set highlight/updateProps for previous item using prevCellKey
updateHighlightFor={this._updateHighlightFor}
updatePropsFor={this._updatePropsFor}
renderItem={renderItem}
section={info.section}
trailingItem={info.trailingItem}
trailingSection={info.trailingSection}
inverted={!!this.props.inverted}
/>
);
}
};
_updatePropsFor = (cellKey: string, value: any) => {
const updateProps = this._updatePropsMap[cellKey];
if (updateProps != null) {
updateProps(value);
}
};
_updateHighlightFor = (cellKey: string, value: boolean) => {
const updateHighlight = this._updateHighlightMap[cellKey];
if (updateHighlight != null) {
updateHighlight(value);
}
};
_setUpdateHighlightFor = (
cellKey: string,
updateHighlightFn: ?(boolean) => void,
) => {
if (updateHighlightFn != null) {
this._updateHighlightMap[cellKey] = updateHighlightFn;
} else {
// $FlowFixMe[prop-missing]
delete this._updateHighlightFor[cellKey];
}
};
_setUpdatePropsFor = (cellKey: string, updatePropsFn: ?(boolean) => void) => {
if (updatePropsFn != null) {
this._updatePropsMap[cellKey] = updatePropsFn;
} else {
delete this._updatePropsMap[cellKey];
}
};
_getSeparatorComponent(
index: number,
info?: ?Object,
listItemCount: number,
): ?(React.ComponentType<any> | React.MixedElement) {
info = info || this._subExtractor(index);
if (!info) {
return null;
}
const ItemSeparatorComponent =
info.section.ItemSeparatorComponent || this.props.ItemSeparatorComponent;
const {SectionSeparatorComponent} = this.props;
const isLastItemInList = index === listItemCount - 1;
const isLastItemInSection =
info.index === this.props.getItemCount(info.section.data) - 1;
if (SectionSeparatorComponent && isLastItemInSection) {
return SectionSeparatorComponent;
}
if (ItemSeparatorComponent && !isLastItemInSection && !isLastItemInList) {
return ItemSeparatorComponent;
}
return null;
}
_updateHighlightMap: {[string]: (boolean) => void} = {};
_updatePropsMap: {[string]: void | (boolean => void)} = {};
_listRef: ?VirtualizedList;
_captureRef = (ref: null | VirtualizedList) => {
this._listRef = ref;
};
}
type ItemWithSeparatorCommonProps<ItemT> = Readonly<{
leadingItem: ?ItemT,
leadingSection: ?Object,
section: Object,
trailingItem: ?ItemT,
trailingSection: ?Object,
}>;
type ItemWithSeparatorProps<ItemT> = Readonly<{
...ItemWithSeparatorCommonProps<ItemT>,
LeadingSeparatorComponent: ?(React.ComponentType<any> | React.MixedElement),
SeparatorComponent: ?(React.ComponentType<any> | React.MixedElement),
cellKey: string,
index: number,
item: ItemT,
setSelfHighlightCallback: (
cellKey: string,
updateFn: ?(boolean) => void,
) => void,
setSelfUpdatePropsCallback: (
cellKey: string,
updateFn: ?(boolean) => void,
) => void,
prevCellKey?: ?string,
updateHighlightFor: (prevCellKey: string, value: boolean) => void,
updatePropsFor: (prevCellKey: string, value: Object) => void,
renderItem: Function,
inverted: boolean,
}>;
function ItemWithSeparator<ItemT>(
props: ItemWithSeparatorProps<ItemT>,
): React.Node {
const {
LeadingSeparatorComponent,
// this is the trailing separator and is associated with this item
SeparatorComponent,
cellKey,
prevCellKey,
setSelfHighlightCallback,
updateHighlightFor,
setSelfUpdatePropsCallback,
updatePropsFor,
item,
index,
section,
inverted,
} = props;
const [leadingSeparatorHiglighted, setLeadingSeparatorHighlighted] =
useState(false);
const [separatorHighlighted, setSeparatorHighlighted] = useState(false);
const [leadingSeparatorProps, setLeadingSeparatorProps] = useState<
ItemWithSeparatorCommonProps<ItemT>,
>({
leadingItem: props.leadingItem,
leadingSection: props.leadingSection,
section: props.section,
trailingItem: props.item,
trailingSection: props.trailingSection,
});
const [separatorProps, setSeparatorProps] = useState<
ItemWithSeparatorCommonProps<ItemT>,
>({
leadingItem: props.item,
leadingSection: props.leadingSection,
section: props.section,
trailingItem: props.trailingItem,
trailingSection: props.trailingSection,
});
useEffect(() => {
setSelfHighlightCallback(cellKey, setSeparatorHighlighted);
// $FlowFixMe[incompatible-type]
setSelfUpdatePropsCallback(cellKey, setSeparatorProps);
return () => {
setSelfUpdatePropsCallback(cellKey, null);
setSelfHighlightCallback(cellKey, null);
};
}, [
cellKey,
setSelfHighlightCallback,
setSeparatorProps,
setSelfUpdatePropsCallback,
]);
const separators = {
highlight: () => {
setLeadingSeparatorHighlighted(true);
setSeparatorHighlighted(true);
if (prevCellKey != null) {
updateHighlightFor(prevCellKey, true);
}
},
unhighlight: () => {
setLeadingSeparatorHighlighted(false);
setSeparatorHighlighted(false);
if (prevCellKey != null) {
updateHighlightFor(prevCellKey, false);
}
},
updateProps: (
select: 'leading' | 'trailing',
newProps: Partial<ItemWithSeparatorCommonProps<ItemT>>,
) => {
if (select === 'leading') {
if (LeadingSeparatorComponent != null) {
setLeadingSeparatorProps({...leadingSeparatorProps, ...newProps});
} else if (prevCellKey != null) {
// update the previous item's separator
updatePropsFor(prevCellKey, {...leadingSeparatorProps, ...newProps});
}
} else if (select === 'trailing' && SeparatorComponent != null) {
setSeparatorProps({...separatorProps, ...newProps});
}
},
};
const element = props.renderItem({
item,
index,
section,
separators,
});
const leadingSeparator =
LeadingSeparatorComponent != null &&
((React.isValidElement(LeadingSeparatorComponent) ? (
LeadingSeparatorComponent
) : (
// $FlowFixMe[not-a-component]
// $FlowFixMe[incompatible-type]
<LeadingSeparatorComponent
highlighted={leadingSeparatorHiglighted}
{...leadingSeparatorProps}
/>
)): any);
const separator =
SeparatorComponent != null &&
((React.isValidElement(SeparatorComponent) ? (
SeparatorComponent
) : (
// $FlowFixMe[not-a-component]
// $FlowFixMe[incompatible-type]
<SeparatorComponent
highlighted={separatorHighlighted}
{...separatorProps}
/>
)): any);
const RenderSeparator = leadingSeparator || separator;
const firstSeparator = inverted === false ? leadingSeparator : separator;
const secondSeparator = inverted === false ? separator : leadingSeparator;
return (
<>
{RenderSeparator ? firstSeparator : null}
{element}
{RenderSeparator ? secondSeparator : null}
</>
);
}
const VirtualizedSectionListComponent = VirtualizedSectionList as component<
ItemT,
SectionT: SectionBase<
ItemT,
DefaultVirtualizedSectionT,
> = DefaultVirtualizedSectionT,
>(
ref?: React.RefSetter<
interface {
getListRef(): ?VirtualizedList,
scrollToLocation(params: ScrollToLocationParamsType): void,
},
>,
...VirtualizedSectionListProps<ItemT, SectionT>
);
export default VirtualizedSectionListComponent;
export type AnyVirtualizedSectionList = typeof VirtualizedSectionListComponent<
any,
any,
>;

21
node_modules/@react-native/virtualized-lists/README.md generated vendored Normal file
View File

@@ -0,0 +1,21 @@
# @react-native/virtualized-lists
[![Version][version-badge]][package]
## Installation
```
yarn add @react-native/virtualized-lists
```
*Note: We're using `yarn` to install deps. Feel free to change commands to use `npm` 3+ and `npx` if you like*
[version-badge]: https://img.shields.io/npm/v/@react-native/virtualized-lists?style=flat-square
[package]: https://www.npmjs.com/package/@react-native/virtualized-lists
## Testing
To run the tests in this package, run the following commands from the React Native root folder:
1. `yarn` to install the dependencies. You just need to run this once
2. `yarn jest packages/virtualized-lists`.

View File

@@ -0,0 +1,23 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/
'use strict';
function clamp(min: number, value: number, max: number): number {
if (value < min) {
return min;
}
if (value > max) {
return max;
}
return value;
}
export default clamp;

View File

@@ -0,0 +1,20 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/
'use strict';
/**
* Intentional info-level logging for clear separation from ad-hoc console debug logging.
*/
function infoLog(...args: Array<unknown>): void {
return console.log(...args);
}
export default infoLog;

View File

@@ -0,0 +1,10 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
export * from './Lists/VirtualizedList';

61
node_modules/@react-native/virtualized-lists/index.js generated vendored Normal file
View File

@@ -0,0 +1,61 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import typeof FillRateHelperT from './Lists/FillRateHelper';
import typeof ViewabilityHelperT from './Lists/ViewabilityHelper';
import typeof VirtualizedListT from './Lists/VirtualizedList';
import type {AnyVirtualizedSectionList as AnyVirtualizedSectionListT} from './Lists/VirtualizedSectionList';
import {typeof VirtualizedListContextResetter as VirtualizedListContextResetterT} from './Lists/VirtualizedListContext';
import {keyExtractor} from './Lists/VirtualizeUtils';
export type {
ViewToken as ListViewToken,
ViewabilityConfig,
ViewabilityConfigCallbackPair,
ViewabilityConfigCallbackPairs,
} from './Lists/ViewabilityHelper';
export type {
CellRendererProps,
ListRenderItemInfo,
ListRenderItem,
Separators,
VirtualizedListProps,
} from './Lists/VirtualizedListProps';
export type {
VirtualizedSectionListProps,
ScrollToLocationParamsType,
SectionBase,
SectionData,
} from './Lists/VirtualizedSectionList';
export type {FillRateInfo} from './Lists/FillRateHelper';
export default {
keyExtractor,
get VirtualizedList(): VirtualizedListT {
return require('./Lists/VirtualizedList').default;
},
get VirtualizedSectionList(): AnyVirtualizedSectionListT {
return require('./Lists/VirtualizedSectionList').default;
},
get VirtualizedListContextResetter(): VirtualizedListContextResetterT {
const VirtualizedListContext = require('./Lists/VirtualizedListContext');
return VirtualizedListContext.VirtualizedListContextResetter;
},
get ViewabilityHelper(): ViewabilityHelperT {
return require('./Lists/ViewabilityHelper').default;
},
get FillRateHelper(): FillRateHelperT {
return require('./Lists/FillRateHelper').default;
},
};

View File

@@ -0,0 +1,63 @@
{
"name": "@react-native/virtualized-lists",
"version": "0.85.2",
"description": "Virtualized lists for React Native.",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/facebook/react-native.git",
"directory": "packages/virtualized-lists"
},
"homepage": "https://github.com/facebook/react-native/tree/HEAD/packages/virtualized-lists#readme",
"keywords": [
"lists",
"virtualized-lists",
"section-lists",
"react-native"
],
"bugs": "https://github.com/facebook/react-native/issues",
"engines": {
"node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
},
"exports": {
".": {
"react-native-strict-api": "./types_generated/index.d.ts",
"types": "./index.d.ts",
"default": "./index.js"
},
"./*": {
"types": null,
"default": "./*.js"
},
"./package.json": "./package.json"
},
"files": [
"index.js",
"index.d.ts",
"Lists",
"README.md",
"types_generated",
"Utilities",
"!**/__docs__/**",
"!**/__fixtures__/**",
"!**/__mocks__/**",
"!**/__tests__/**"
],
"dependencies": {
"invariant": "^2.2.4",
"nullthrows": "^1.1.1"
},
"devDependencies": {
"react-test-renderer": "19.2.3"
},
"peerDependencies": {
"@types/react": "^19.2.0",
"react": "*",
"react-native": "0.85.2"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<f9c45ce2bdf2e3ee2b9734aa8032542c>>
*
* This file was translated from Flow by scripts/js-api/build-types/index.js.
* Original file: packages/virtualized-lists/Lists/CellRenderMask.js
*/
export type CellRegion = {
first: number;
last: number;
isSpacer: boolean;
};
export declare class CellRenderMask {
constructor(numCells: number);
enumerateRegions(): ReadonlyArray<CellRegion>;
addCells(cells: {
first: number;
last: number;
}): void;
numCells(): number;
equals(other: CellRenderMask): boolean;
}

View File

@@ -0,0 +1,60 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<1ccfec0a0722ba9e0e9fb01d1ed7221c>>
*
* This file was translated from Flow by scripts/js-api/build-types/index.js.
* Original file: packages/virtualized-lists/Lists/FillRateHelper.js
*/
import type { CellMetricProps } from "./ListMetricsAggregator";
import ListMetricsAggregator from "./ListMetricsAggregator";
export type FillRateInfo = Info;
declare class Info {
any_blank_count: number;
any_blank_ms: number;
any_blank_speed_sum: number;
mostly_blank_count: number;
mostly_blank_ms: number;
pixels_blank: number;
pixels_sampled: number;
pixels_scrolled: number;
total_time_spent: number;
sample_count: number;
}
/**
* A helper class for detecting when the maximem fill rate of `VirtualizedList` is exceeded.
* By default the sampling rate is set to zero and this will do nothing. If you want to collect
* samples (e.g. to log them), make sure to call `FillRateHelper.setSampleRate(0.0-1.0)`.
*
* Listeners and sample rate are global for all `VirtualizedList`s - typical usage will combine with
* `SceneTracker.getActiveScene` to determine the context of the events.
*/
declare class FillRateHelper {
static addListener(callback: ($$PARAM_0$$: FillRateInfo) => void): {
remove: () => void;
};
static setSampleRate(sampleRate: number): void;
static setMinSampleCount(minSampleCount: number): void;
constructor(listMetrics: ListMetricsAggregator);
activate(): void;
deactivateAndFlush(): void;
computeBlankness(props: Omit<CellMetricProps, keyof {
initialNumToRender?: number | undefined;
}> & {
initialNumToRender?: number | undefined;
}, cellsAroundViewport: {
first: number;
last: number;
}, scrollMetrics: {
dOffset: number;
offset: number;
velocity: number;
visibleLength: number;
}): number;
enabled(): boolean;
}
export default FillRateHelper;

View File

@@ -0,0 +1,119 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<f623b47d93e1e0fe9b2f92b3e51741b1>>
*
* This file was translated from Flow by scripts/js-api/build-types/index.js.
* Original file: packages/virtualized-lists/Lists/ListMetricsAggregator.js
*/
import type { VirtualizedListProps } from "./VirtualizedListProps";
import type { LayoutRectangle } from "react-native";
export type CellMetrics = {
/**
* Index of the item in the list
*/
index: number;
/**
* Length of the cell along the scrolling axis
*/
length: number;
/**
* Distance between this cell and the start of the list along the scrolling
* axis
*/
offset: number;
/**
* Whether the cell is last known to be mounted
*/
isMounted: boolean;
};
export type ListOrientation = {
horizontal: boolean;
rtl: boolean;
};
/**
* Subset of VirtualizedList props needed to calculate cell metrics
*/
export type CellMetricProps = {
data: VirtualizedListProps["data"];
getItemCount: VirtualizedListProps["getItemCount"];
getItem: VirtualizedListProps["getItem"];
getItemLayout?: VirtualizedListProps["getItemLayout"];
keyExtractor?: VirtualizedListProps["keyExtractor"];
};
/**
* Provides an interface to query information about the metrics of a list and its cells.
*/
declare class ListMetricsAggregator {
/**
* Notify the ListMetricsAggregator that a cell has been laid out.
*
* @returns whether the cell layout has changed since last notification
*/
notifyCellLayout($$PARAM_0$$: {
cellIndex: number;
cellKey: string;
orientation: ListOrientation;
layout: LayoutRectangle;
}): boolean;
/**
* Notify ListMetricsAggregator that a cell has been unmounted.
*/
notifyCellUnmounted(cellKey: string): void;
/**
* Notify ListMetricsAggregator that the lists content container has been laid out.
*/
notifyListContentLayout($$PARAM_0$$: {
orientation: ListOrientation;
layout: Readonly<{
width: number;
height: number;
}>;
}): void;
/**
* Return the average length of the cells which have been measured
*/
getAverageCellLength(): number;
/**
* Return the highest measured cell index (or 0 if nothing has been measured
* yet)
*/
getHighestMeasuredCellIndex(): number;
/**
* Returns the exact metrics of a cell if it has already been laid out,
* otherwise an estimate based on the average length of previously measured
* cells
*/
getCellMetricsApprox(index: number, props: CellMetricProps): CellMetrics;
/**
* Returns the exact metrics of a cell if it has already been laid out
*/
getCellMetrics(index: number, props: CellMetricProps): null | undefined | CellMetrics;
/**
* Gets an approximate offset to an item at a given index. Supports
* fractional indices.
*/
getCellOffsetApprox(index: number, props: CellMetricProps): number;
/**
* Returns the length of all ScrollView content along the scrolling axis.
*/
getContentLength(): number;
/**
* Whether a content length has been observed
*/
hasContentLength(): boolean;
/**
* Finds the flow-relative offset (e.g. starting from the left in LTR, but
* right in RTL) from a layout box.
*/
flowRelativeOffset(layout: LayoutRectangle, referenceContentLength?: null | undefined | number): number;
/**
* Converts a flow-relative offset to a cartesian offset
*/
cartesianOffset(flowRelativeOffset: number): number;
}
export default ListMetricsAggregator;

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<e0d2ed06b7508b8127df565e426e1352>>
*
* This file was translated from Flow by scripts/js-api/build-types/index.js.
* Original file: packages/virtualized-lists/Lists/StateSafePureComponent.js
*/
import * as React from "react";
/**
* `setState` is called asynchronously, and should not rely on the value of
* `this.props` or `this.state`:
* https://react.dev/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
*
* SafePureComponent adds runtime enforcement, to catch cases where these
* variables are read in a state updater function, instead of the ones passed
* in.
*/
declare class StateSafePureComponent<Props, State extends {}> extends React.PureComponent<Props, State> {
constructor(props: Props);
setState<K extends keyof State>(partialState: null | undefined | (Pick<State, K> | (($$PARAM_0$$: State, $$PARAM_1$$: Props) => null | undefined | Pick<State, K>)), callback?: () => unknown): void;
}
export default StateSafePureComponent;

View File

@@ -0,0 +1,100 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<67bb8ff601be8116579df670a6e3ea35>>
*
* This file was translated from Flow by scripts/js-api/build-types/index.js.
* Original file: packages/virtualized-lists/Lists/ViewabilityHelper.js
*/
import type { CellMetricProps } from "./ListMetricsAggregator";
import ListMetricsAggregator from "./ListMetricsAggregator";
export type ViewToken = {
item: any;
key: string;
index: number | undefined;
isViewable: boolean;
section?: any;
};
export type ViewabilityConfigCallbackPair = {
viewabilityConfig: ViewabilityConfig;
onViewableItemsChanged: (info: {
viewableItems: Array<ViewToken>;
changed: Array<ViewToken>;
}) => void;
};
export type ViewabilityConfigCallbackPairs = Array<ViewabilityConfigCallbackPair>;
export type ViewabilityConfig = Readonly<{
/**
* Minimum amount of time (in milliseconds) that an item must be physically viewable before the
* viewability callback will be fired. A high number means that scrolling through content without
* stopping will not mark the content as viewable.
*/
minimumViewTime?: number;
/**
* Percent of viewport that must be covered for a partially occluded item to count as
* "viewable", 0-100. Fully visible items are always considered viewable. A value of 0 means
* that a single pixel in the viewport makes the item viewable, and a value of 100 means that
* an item must be either entirely visible or cover the entire viewport to count as viewable.
*/
viewAreaCoveragePercentThreshold?: number;
/**
* Similar to `viewAreaPercentThreshold`, but considers the percent of the item that is visible,
* rather than the fraction of the viewable area it covers.
*/
itemVisiblePercentThreshold?: number;
/**
* Nothing is considered viewable until the user scrolls or `recordInteraction` is called after
* render.
*/
waitForInteraction?: boolean;
}>;
/**
* A Utility class for calculating viewable items based on current metrics like scroll position and
* layout.
*
* An item is said to be in a "viewable" state when any of the following
* is true for longer than `minimumViewTime` milliseconds (after an interaction if `waitForInteraction`
* is true):
*
* - Occupying >= `viewAreaCoveragePercentThreshold` of the view area XOR fraction of the item
* visible in the view area >= `itemVisiblePercentThreshold`.
* - Entirely visible on screen
*/
declare class ViewabilityHelper {
constructor(config?: ViewabilityConfig);
/**
* Cleanup, e.g. on unmount. Clears any pending timers.
*/
dispose(): void;
/**
* Determines which items are viewable based on the current metrics and config.
*/
computeViewableItems(props: CellMetricProps, scrollOffset: number, viewportHeight: number, listMetrics: ListMetricsAggregator, renderRange?: {
first: number;
last: number;
}): Array<number>;
/**
* Figures out which items are viewable and how that has changed from before and calls
* `onViewableItemsChanged` as appropriate.
*/
onUpdate(props: CellMetricProps, scrollOffset: number, viewportHeight: number, listMetrics: ListMetricsAggregator, createViewToken: (index: number, isViewable: boolean, props: CellMetricProps) => ViewToken, onViewableItemsChanged: ($$PARAM_0$$: {
viewableItems: Array<ViewToken>;
changed: Array<ViewToken>;
}) => void, renderRange?: {
first: number;
last: number;
}): void;
/**
* clean-up cached _viewableIndices to evaluate changed items on next update
*/
resetViewableIndices(): void;
/**
* Records that an interaction has happened even if there has been no scroll.
*/
recordInteraction(): void;
}
export default ViewabilityHelper;

View File

@@ -0,0 +1,53 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<5b235d979a9e973d258802a5a7793f31>>
*
* This file was translated from Flow by scripts/js-api/build-types/index.js.
* Original file: packages/virtualized-lists/Lists/VirtualizeUtils.js
*/
import type ListMetricsAggregator from "./ListMetricsAggregator";
import type { CellMetricProps } from "./ListMetricsAggregator";
/**
* Used to find the indices of the frames that overlap the given offsets. Useful for finding the
* items that bound different windows of content, such as the visible area or the buffered overscan
* area.
*/
export declare function elementsThatOverlapOffsets(offsets: Array<number>, props: CellMetricProps, listMetrics: ListMetricsAggregator, zoomScale?: number): Array<number>;
/**
* Computes the number of elements in the `next` range that are new compared to the `prev` range.
* Handy for calculating how many new items will be rendered when the render window changes so we
* can restrict the number of new items render at once so that content can appear on the screen
* faster.
*/
export declare function newRangeCount(prev: {
first: number;
last: number;
}, next: {
first: number;
last: number;
}): number;
/**
* Custom logic for determining which items should be rendered given the current frame and scroll
* metrics, as well as the previous render state. The algorithm may evolve over time, but generally
* prioritizes the visible area first, then expands that with overscan regions ahead and behind,
* biased in the direction of scroll.
*/
export declare function computeWindowedRenderLimits(props: CellMetricProps, maxToRenderPerBatch: number, windowSize: number, prev: {
first: number;
last: number;
}, listMetrics: ListMetricsAggregator, scrollMetrics: {
dt: number;
offset: number;
velocity: number;
visibleLength: number;
zoomScale: number;
}): {
first: number;
last: number;
};
export declare function keyExtractor(item: any, index: number): string;

View File

@@ -0,0 +1,116 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<a562ddf8afa2306919e86f1c782188e8>>
*
* This file was translated from Flow by scripts/js-api/build-types/index.js.
* Original file: packages/virtualized-lists/Lists/VirtualizedList.js
*/
import type { Item, ListRenderItem, ListRenderItemInfo, Separators, VirtualizedListProps } from "./VirtualizedListProps";
import type { ScrollEvent, ScrollResponderType } from "react-native";
import { CellRenderMask } from "./CellRenderMask";
import StateSafePureComponent from "./StateSafePureComponent";
import { VirtualizedListContext } from "./VirtualizedListContext.js";
import * as React from "react";
import { ScrollView } from "react-native";
export type { ListRenderItemInfo, ListRenderItem, Separators };
type State = {
renderMask: CellRenderMask;
cellsAroundViewport: {
first: number;
last: number;
};
firstVisibleItemKey: string | undefined;
pendingScrollUpdateCount: number;
};
/**
* Base implementation for the more convenient [`<FlatList>`](https://reactnative.dev/docs/flatlist)
* and [`<SectionList>`](https://reactnative.dev/docs/sectionlist) components, which are also better
* documented. In general, this should only really be used if you need more flexibility than
* `FlatList` provides, e.g. for use with immutable data instead of plain arrays.
*
* Virtualization massively improves memory consumption and performance of large lists by
* maintaining a finite render window of active items and replacing all items outside of the render
* window with appropriately sized blank space. The window adapts to scrolling behavior, and items
* are rendered incrementally with low-pri (after any running interactions) if they are far from the
* visible area, or with hi-pri otherwise to minimize the potential of seeing blank space.
*
* Some caveats:
*
* - Internal state is not preserved when content scrolls out of the render window. Make sure all
* your data is captured in the item data or external stores like Flux, Redux, or Relay.
* - This is a `PureComponent` which means that it will not re-render if `props` remain shallow-
* equal. Make sure that everything your `renderItem` function depends on is passed as a prop
* (e.g. `extraData`) that is not `===` after updates, otherwise your UI may not update on
* changes. This includes the `data` prop and parent component state.
* - In order to constrain memory and enable smooth scrolling, content is rendered asynchronously
* offscreen. This means it's possible to scroll faster than the fill rate ands momentarily see
* blank content. This is a tradeoff that can be adjusted to suit the needs of each application,
* and we are working on improving it behind the scenes.
* - By default, the list looks for a `key` or `id` prop on each item and uses that for the React key.
* Alternatively, you can provide a custom `keyExtractor` prop.
* - As an effort to remove defaultProps, use helper functions when referencing certain props
*
*/
declare class VirtualizedList extends StateSafePureComponent<VirtualizedListProps, State> {
static contextType: typeof VirtualizedListContext;
scrollToEnd(params?: null | undefined | {
animated?: boolean | undefined;
}): void;
scrollToIndex(params: {
animated?: boolean | undefined;
index: number;
viewOffset?: number;
viewPosition?: number;
}): any;
scrollToItem(params: {
animated?: boolean | undefined;
item: Item;
viewOffset?: number;
viewPosition?: number;
}): void;
/**
* Scroll to a specific content pixel offset in the list.
*
* Param `offset` expects the offset to scroll to.
* In case of `horizontal` is true, the offset is the x-value,
* in any other case the offset is the y-value.
*
* Param `animated` (`true` by default) defines whether the list
* should do an animation while scrolling.
*/
scrollToOffset(params: {
animated?: boolean | undefined;
offset: number;
}): void;
recordInteraction(): void;
flashScrollIndicators(): void;
/**
* Provides a handle to the underlying scroll responder.
* Note that `this._scrollRef` might not be a `ScrollView`, so we
* need to check that it responds to `getScrollResponder` before calling it.
*/
getScrollResponder(): null | undefined | ScrollResponderType;
getScrollableNode(): null | undefined | number;
getScrollRef(): null | undefined | React.ComponentRef<typeof ScrollView>;
setNativeProps(props: Object): void;
hasMore(): boolean;
state: State;
constructor(props: VirtualizedListProps);
componentDidMount(): void;
componentWillUnmount(): void;
static getDerivedStateFromProps(newProps: VirtualizedListProps, prevState: State): State;
render(): React.ReactNode;
componentDidUpdate(prevProps: VirtualizedListProps): void;
measureLayoutRelativeToContainingList(): void;
unstable_onScroll(e: Object): void;
unstable_onScrollBeginDrag(e: ScrollEvent): void;
unstable_onScrollEndDrag(e: ScrollEvent): void;
unstable_onMomentumScrollBegin(e: ScrollEvent): void;
unstable_onMomentumScrollEnd(e: ScrollEvent): void;
}
export default VirtualizedList;

View File

@@ -0,0 +1,58 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<6e397911ca6023929d0359985d20c504>>
*
* This file was translated from Flow by scripts/js-api/build-types/index.js.
* Original file: packages/virtualized-lists/Lists/VirtualizedListContext.js
*/
import type VirtualizedList from "./VirtualizedList";
import * as React from "react";
type Context = Readonly<{
cellKey: string | undefined;
getScrollMetrics: () => {
contentLength: number;
dOffset: number;
dt: number;
offset: number;
timestamp: number;
velocity: number;
visibleLength: number;
zoomScale: number;
};
horizontal: boolean | undefined;
getOutermostParentListRef: () => VirtualizedList;
registerAsNestedChild: ($$PARAM_0$$: {
cellKey: string;
ref: VirtualizedList;
}) => void;
unregisterAsNestedChild: ($$PARAM_0$$: {
ref: VirtualizedList;
}) => void;
}>;
export declare const VirtualizedListContext: React.Context<null | undefined | Context>;
export declare type VirtualizedListContext = typeof VirtualizedListContext;
/**
* Resets the context. Intended for use by portal-like components (e.g. Modal).
*/
export declare function VirtualizedListContextResetter($$PARAM_0$$: {
children: React.ReactNode;
}): React.ReactNode;
/**
* Sets the context with memoization. Intended to be used by `VirtualizedList`.
*/
export declare function VirtualizedListContextProvider($$PARAM_0$$: {
children: React.ReactNode;
value: Context;
}): React.ReactNode;
/**
* Sets the `cellKey`. Intended to be used by `VirtualizedList` for each cell.
*/
export declare function VirtualizedListCellContextProvider($$PARAM_0$$: {
cellKey: string;
children: React.ReactNode;
}): React.ReactNode;

View File

@@ -0,0 +1,275 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<a23b061213c8c0234da27ab93021b537>>
*
* This file was translated from Flow by scripts/js-api/build-types/index.js.
* Original file: packages/virtualized-lists/Lists/VirtualizedListProps.js
*/
import type { ViewabilityConfig, ViewabilityConfigCallbackPair, ViewToken } from "./ViewabilityHelper";
import type { FocusEvent, LayoutChangeEvent, ScrollViewProps, StyleProp, ViewStyle } from "react-native";
import * as React from "react";
export type Item = any;
export type Separators = {
highlight: () => void;
unhighlight: () => void;
updateProps: (select: "leading" | "trailing", newProps: Object) => void;
};
export type ListRenderItemInfo<ItemT> = {
item: ItemT;
index: number;
separators: Separators;
};
export type CellRendererProps<ItemT> = Readonly<{
cellKey: string;
children: React.ReactNode;
index: number;
item: ItemT;
onFocusCapture?: (event: FocusEvent) => void;
onLayout?: (event: LayoutChangeEvent) => void;
style: StyleProp<ViewStyle>;
}>;
export type ListRenderItem<ItemT> = (info: ListRenderItemInfo<ItemT>) => React.ReactNode;
type RequiredVirtualizedListProps = {
/**
* The default accessor functions assume this is an Array<{key: string} | {id: string}> but you can override
* getItem, getItemCount, and keyExtractor to handle any type of index-based data.
*/
data?: any;
/**
* A generic accessor for extracting an item from any sort of data blob.
*/
getItem: (data: any, index: number) => Item | undefined;
/**
* Determines how many items are in the data blob.
*/
getItemCount: (data: any) => number;
};
type OptionalVirtualizedListProps = {
renderItem?: ListRenderItem<Item> | undefined;
/**
* `debug` will turn on extra logging and visual overlays to aid with debugging both usage and
* implementation, but with a significant perf hit.
*/
debug?: boolean | undefined;
/**
* DEPRECATED: Virtualization provides significant performance and memory optimizations, but fully
* unmounts react instances that are outside of the render window. You should only need to disable
* this for debugging purposes. Defaults to false.
*/
disableVirtualization?: boolean | undefined;
/**
* A marker property for telling the list to re-render (since it implements `PureComponent`). If
* any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the
* `data` prop, stick it here and treat it immutably.
*/
extraData?: any;
getItemLayout?: (data: any, index: number) => {
length: number;
offset: number;
index: number;
};
horizontal?: boolean | undefined;
/**
* How many items to render in the initial batch. This should be enough to fill the screen but not
* much more. Note these items will never be unmounted as part of the windowed rendering in order
* to improve perceived performance of scroll-to-top actions.
*/
initialNumToRender?: number | undefined;
/**
* Instead of starting at the top with the first item, start at `initialScrollIndex`. This
* disables the "scroll to top" optimization that keeps the first `initialNumToRender` items
* always rendered and immediately renders the items starting at this initial index. Requires
* `getItemLayout` to be implemented.
*/
initialScrollIndex?: number | undefined;
/**
* Reverses the direction of scroll. Uses scale transforms of -1.
*/
inverted?: boolean | undefined;
keyExtractor?: ((item: Item, index: number) => string) | undefined;
/**
* CellRendererComponent allows customizing how cells rendered by
* `renderItem`/`ListItemComponent` are wrapped when placed into the
* underlying ScrollView. This component must accept event handlers which
* notify VirtualizedList of changes within the cell.
*/
CellRendererComponent?: React.ComponentType<CellRendererProps<Item>> | undefined;
/**
* Rendered in between each item, but not at the top or bottom. By default, `highlighted` and
* `leadingItem` props are provided. `renderItem` provides `separators.highlight`/`unhighlight`
* which will update the `highlighted` prop, but you can also add custom props with
* `separators.updateProps`.
*/
ItemSeparatorComponent?: (React.ComponentType<any> | React.JSX.Element) | undefined;
/**
* Takes an item from `data` and renders it into the list. Example usage:
*
* <FlatList
* ItemSeparatorComponent={Platform.OS !== 'android' && ({highlighted}) => (
* <View style={[style.separator, highlighted && {marginLeft: 0}]} />
* )}
* data={[{title: 'Title Text', key: 'item1'}]}
* ListItemComponent={({item, separators}) => (
* <TouchableHighlight
* onPress={() => this._onPress(item)}
* onShowUnderlay={separators.highlight}
* onHideUnderlay={separators.unhighlight}>
* <View style={{backgroundColor: 'white'}}>
* <Text>{item.title}</Text>
* </View>
* </TouchableHighlight>
* )}
* />
*
* Provides additional metadata like `index` if you need it, as well as a more generic
* `separators.updateProps` function which let's you set whatever props you want to change the
* rendering of either the leading separator or trailing separator in case the more common
* `highlight` and `unhighlight` (which set the `highlighted: boolean` prop) are insufficient for
* your use-case.
*/
ListItemComponent?: (React.ComponentType<any> | React.JSX.Element) | undefined;
/**
* Rendered when the list is empty. Can be a React Component Class, a render function, or
* a rendered element.
*/
ListEmptyComponent?: (React.ComponentType<any> | React.JSX.Element) | undefined;
/**
* Rendered at the bottom of all the items. Can be a React Component Class, a render function, or
* a rendered element.
*/
ListFooterComponent?: (React.ComponentType<any> | React.JSX.Element) | undefined;
/**
* Styling for internal View for ListFooterComponent
*/
ListFooterComponentStyle?: StyleProp<ViewStyle>;
/**
* Rendered at the top of all the items. Can be a React Component Class, a render function, or
* a rendered element.
*/
ListHeaderComponent?: (React.ComponentType<any> | React.JSX.Element) | undefined;
/**
* Styling for internal View for ListHeaderComponent
*/
ListHeaderComponentStyle?: StyleProp<ViewStyle>;
/**
* The maximum number of items to render in each incremental render batch. The more rendered at
* once, the better the fill rate, but responsiveness may suffer because rendering content may
* interfere with responding to button taps or other interactions.
*/
maxToRenderPerBatch?: number | undefined;
/**
* Called once when the scroll position gets within within `onEndReachedThreshold`
* from the logical end of the list.
*/
onEndReached?: ((info: {
distanceFromEnd: number;
}) => void) | undefined;
/**
* How far from the end (in units of visible length of the list) the trailing edge of the
* list must be from the end of the content to trigger the `onEndReached` callback.
* Thus, a value of 0.5 will trigger `onEndReached` when the end of the content is
* within half the visible length of the list.
*/
onEndReachedThreshold?: number | undefined;
/**
* If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make
* sure to also set the `refreshing` prop correctly.
*/
onRefresh?: (() => void) | undefined;
/**
* Used to handle failures when scrolling to an index that has not been measured yet. Recommended
* action is to either compute your own offset and `scrollTo` it, or scroll as far as possible and
* then try again after more items have been rendered.
*/
onScrollToIndexFailed?: ((info: {
index: number;
highestMeasuredFrameIndex: number;
averageItemLength: number;
}) => void) | undefined;
/**
* Called once when the scroll position gets within within `onStartReachedThreshold`
* from the logical start of the list.
*/
onStartReached?: ((info: {
distanceFromStart: number;
}) => void) | undefined;
/**
* How far from the start (in units of visible length of the list) the leading edge of the
* list must be from the start of the content to trigger the `onStartReached` callback.
* Thus, a value of 0.5 will trigger `onStartReached` when the start of the content is
* within half the visible length of the list.
*/
onStartReachedThreshold?: number | undefined;
/**
* Called when the viewability of rows changes, as defined by the
* `viewabilityConfig` prop.
*/
onViewableItemsChanged?: ((info: {
viewableItems: Array<ViewToken>;
changed: Array<ViewToken>;
}) => void) | undefined;
persistentScrollbar?: boolean | undefined;
/**
* Set this when offset is needed for the loading indicator to show correctly.
*/
progressViewOffset?: number;
/**
* A custom refresh control element. When set, it overrides the default
* <RefreshControl> component built internally. The onRefresh and refreshing
* props are also ignored. Only works for vertical VirtualizedList.
*/
refreshControl?: React.JSX.Element | undefined;
/**
* Set this true while waiting for new data from a refresh.
*/
refreshing?: boolean | undefined;
/**
* Note: may have bugs (missing content) in some circumstances - use at your own risk.
*
* This may improve scroll performance for large lists.
*/
removeClippedSubviews?: boolean;
/**
* Render a custom scroll component, e.g. with a differently styled `RefreshControl`.
*/
renderScrollComponent?: (props: ScrollViewProps) => React.JSX.Element;
/**
* Amount of time between low-pri item render batches, e.g. for rendering items quite a ways off
* screen. Similar fill rate/responsiveness tradeoff as `maxToRenderPerBatch`.
*/
updateCellsBatchingPeriod?: number | undefined;
/**
* See `ViewabilityHelper` for flow type and further documentation.
*/
viewabilityConfig?: ViewabilityConfig;
/**
* List of ViewabilityConfig/onViewableItemsChanged pairs. A specific onViewableItemsChanged
* will be called when its corresponding ViewabilityConfig's conditions are met.
*/
viewabilityConfigCallbackPairs?: Array<ViewabilityConfigCallbackPair>;
/**
* Determines the maximum number of items rendered outside of the visible area, in units of
* visible lengths. So if your list fills the screen, then `windowSize={21}` (the default) will
* render the visible screen area plus up to 10 screens above and 10 below the viewport. Reducing
* this number will reduce memory consumption and may improve performance, but will increase the
* chance that fast scrolling may reveal momentary blank areas of unrendered content.
*/
windowSize?: number | undefined;
};
export type VirtualizedListProps = Omit<ScrollViewProps, keyof RequiredVirtualizedListProps | keyof OptionalVirtualizedListProps | keyof {}> & Omit<RequiredVirtualizedListProps, keyof OptionalVirtualizedListProps | keyof {}> & Omit<OptionalVirtualizedListProps, keyof {}> & {};
/**
* Default Props Helper Functions
* Use the following helper functions for default values
*/
export declare function horizontalOrDefault(horizontal: null | undefined | boolean): boolean;
export declare function initialNumToRenderOrDefault(initialNumToRender: null | undefined | number): number;
export declare function maxToRenderPerBatchOrDefault(maxToRenderPerBatch: null | undefined | number): number;
export declare function onStartReachedThresholdOrDefault(onStartReachedThreshold: null | undefined | number): number;
export declare function onEndReachedThresholdOrDefault(onEndReachedThreshold: null | undefined | number): number;
export declare function windowSizeOrDefault(windowSize: null | undefined | number): number;

View File

@@ -0,0 +1,113 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<c3707aac6284c44a775eafdc36668f5f>>
*
* This file was translated from Flow by scripts/js-api/build-types/index.js.
* Original file: packages/virtualized-lists/Lists/VirtualizedSectionList.js
*/
import type { VirtualizedListProps } from "./VirtualizedListProps";
import VirtualizedList from "./VirtualizedList";
import * as React from "react";
type DefaultVirtualizedSectionT = {
data: any;
[key: string]: any;
};
export type SectionData<SectionItemT, SectionT = DefaultVirtualizedSectionT> = (Readonly<SectionBase<SectionItemT, SectionT>> & SectionT) | (SectionBase<SectionItemT, SectionT> & SectionT) | SectionT;
export type SectionBase<SectionItemT, SectionT = DefaultVirtualizedSectionT> = {
/**
* The data for rendering items in this section.
*/
data: ReadonlyArray<SectionItemT>;
/**
* Optional key to keep track of section re-ordering. If you don't plan on re-ordering sections,
* the array index will be used by default.
*/
key?: string;
renderItem?: ((info: {
item: SectionItemT;
index: number;
section: SectionData<SectionItemT, SectionT>;
separators: {
highlight: () => void;
unhighlight: () => void;
updateProps: (select: "leading" | "trailing", newProps: Object) => void;
};
}) => null | React.JSX.Element) | undefined;
ItemSeparatorComponent?: (React.ComponentType<any> | React.JSX.Element) | undefined;
keyExtractor?: (item: SectionItemT | undefined, index?: number | undefined) => string;
};
type RequiredVirtualizedSectionListProps<ItemT, SectionT = DefaultVirtualizedSectionT> = {
sections: ReadonlyArray<SectionData<ItemT, SectionT>>;
};
type OptionalVirtualizedSectionListProps<ItemT, SectionT = DefaultVirtualizedSectionT> = {
/**
* Default renderer for every item in every section.
*/
renderItem?: (info: {
item: ItemT;
index: number;
section: SectionT;
separators: {
highlight: () => void;
unhighlight: () => void;
updateProps: (select: "leading" | "trailing", newProps: Object) => void;
};
}) => null | React.ReactNode;
/**
* Rendered at the top of each section. These stick to the top of the `ScrollView` by default on
* iOS. See `stickySectionHeadersEnabled`.
*/
renderSectionHeader?: ((info: {
section: SectionT;
}) => null | React.ReactNode) | undefined;
/**
* Rendered at the bottom of each section.
*/
renderSectionFooter?: ((info: {
section: SectionT;
}) => null | React.ReactNode) | undefined;
/**
* Rendered at the top and bottom of each section (note this is different from
* `ItemSeparatorComponent` which is only rendered between items). These are intended to separate
* sections from the headers above and below and typically have the same highlight response as
* `ItemSeparatorComponent`. Also receives `highlighted`, `[leading/trailing][Item/Separator]`,
* and any custom props from `separators.updateProps`.
*/
SectionSeparatorComponent?: React.ComponentType<any> | undefined;
/**
* Makes section headers stick to the top of the screen until the next one pushes it off. Only
* enabled by default on iOS because that is the platform standard there.
*/
stickySectionHeadersEnabled?: boolean;
onEndReached?: (($$PARAM_0$$: {
distanceFromEnd: number;
}) => void) | undefined;
};
export type VirtualizedSectionListProps<ItemT, SectionT = DefaultVirtualizedSectionT> = Omit<RequiredVirtualizedSectionListProps<ItemT, SectionT>, keyof OptionalVirtualizedSectionListProps<ItemT, SectionT> | keyof Omit<VirtualizedListProps, "data" | "renderItem"> | keyof {}> & Omit<OptionalVirtualizedSectionListProps<ItemT, SectionT>, keyof Omit<VirtualizedListProps, "data" | "renderItem"> | keyof {}> & Omit<Omit<VirtualizedListProps, "data" | "renderItem">, keyof {}> & {};
export type ScrollToLocationParamsType = {
animated?: boolean | undefined;
itemIndex: number;
sectionIndex: number;
viewOffset?: number;
viewPosition?: number;
};
declare const VirtualizedSectionListComponent: <ItemT, SectionT extends SectionBase<ItemT, DefaultVirtualizedSectionT> = DefaultVirtualizedSectionT>(props: Omit<VirtualizedSectionListProps<ItemT, SectionT>, keyof {
ref?: React.Ref<{
getListRef(): VirtualizedList | undefined;
scrollToLocation(params: ScrollToLocationParamsType): void;
}>;
}> & {
ref?: React.Ref<{
getListRef(): VirtualizedList | undefined;
scrollToLocation(params: ScrollToLocationParamsType): void;
}>;
}) => React.ReactNode;
declare const $$VirtualizedSectionList: typeof VirtualizedSectionListComponent;
declare type $$VirtualizedSectionList = typeof $$VirtualizedSectionList;
export default $$VirtualizedSectionList;
export type AnyVirtualizedSectionList = typeof VirtualizedSectionListComponent;

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<1eaf7f479c139c4bcc1ad5117c813ecf>>
*
* This file was translated from Flow by scripts/js-api/build-types/index.js.
* Original file: packages/virtualized-lists/index.js
*/
import type $$IMPORT_TYPEOF_1$$ from "./Lists/FillRateHelper";
type FillRateHelperT = typeof $$IMPORT_TYPEOF_1$$;
import type $$IMPORT_TYPEOF_2$$ from "./Lists/ViewabilityHelper";
type ViewabilityHelperT = typeof $$IMPORT_TYPEOF_2$$;
import type $$IMPORT_TYPEOF_3$$ from "./Lists/VirtualizedList";
type VirtualizedListT = typeof $$IMPORT_TYPEOF_3$$;
import type { AnyVirtualizedSectionList as AnyVirtualizedSectionListT } from "./Lists/VirtualizedSectionList";
import { type VirtualizedListContextResetter as $$IMPORT_TYPEOF_4$$ } from "./Lists/VirtualizedListContext";
type VirtualizedListContextResetterT = typeof $$IMPORT_TYPEOF_4$$;
import { keyExtractor } from "./Lists/VirtualizeUtils";
export type { ViewToken as ListViewToken, ViewabilityConfig, ViewabilityConfigCallbackPair, ViewabilityConfigCallbackPairs } from "./Lists/ViewabilityHelper";
export type { CellRendererProps, ListRenderItemInfo, ListRenderItem, Separators, VirtualizedListProps } from "./Lists/VirtualizedListProps";
export type { VirtualizedSectionListProps, ScrollToLocationParamsType, SectionBase, SectionData } from "./Lists/VirtualizedSectionList";
export type { FillRateInfo } from "./Lists/FillRateHelper";
declare const $$index: {
keyExtractor: typeof keyExtractor;
get VirtualizedList(): VirtualizedListT;
get VirtualizedSectionList(): AnyVirtualizedSectionListT;
get VirtualizedListContextResetter(): VirtualizedListContextResetterT;
get ViewabilityHelper(): ViewabilityHelperT;
get FillRateHelper(): FillRateHelperT;
};
declare type $$index = typeof $$index;
export default $$index;