- Externalise and handle any sort of data blob (#20787)

Summary:
Fixes #20770
Pull Request resolved: https://github.com/facebook/react-native/pull/20787

Reviewed By: sahrens

Differential Revision: D9485598

Pulled By: cpojer

fbshipit-source-id: edddebf6b5e1ca396ab1a519baf019c1e5188d44
This commit is contained in:
Jérémy M
2019-04-18 11:52:26 -07:00
committed by Facebook Github Bot
parent f81d77c102
commit 1946aee3d9
5 changed files with 1508 additions and 85 deletions

View File

@@ -15,38 +15,14 @@ const ScrollView = require('ScrollView');
const VirtualizedSectionList = require('VirtualizedSectionList');
import type {ViewToken} from 'ViewabilityHelper';
import type {Props as VirtualizedSectionListProps} from 'VirtualizedSectionList';
import type {
SectionBase as _SectionBase,
Props as VirtualizedSectionListProps,
} from 'VirtualizedSectionList';
type Item = any;
export type SectionBase<SectionItemT> = {
/**
* 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: SectionBase<SectionItemT>,
separators: {
highlight: () => void,
unhighlight: () => void,
updateProps: (select: 'leading' | 'trailing', newProps: Object) => void,
},
}) => ?React.Element<any>,
ItemSeparatorComponent?: ?React.ComponentType<any>,
keyExtractor?: (item: SectionItemT) => string,
// TODO: support more optional/override props
// onViewableItemsChanged?: ...
};
export type SectionBase<SectionItemT> = _SectionBase<SectionItemT>;
type RequiredProps<SectionT: SectionBase<any>> = {
/**
@@ -326,10 +302,17 @@ class SectionList<SectionT: SectionBase<any>> extends React.PureComponent<
}
render() {
/* $FlowFixMe(>=0.66.0 site=react_native_fb) This comment suppresses an
* error found when Flow v0.66 was deployed. To see the error delete this
* comment and run Flow. */
return <VirtualizedSectionList {...this.props} ref={this._captureRef} />;
return (
/* $FlowFixMe(>=0.66.0 site=react_native_fb) This comment suppresses an
* error found when Flow v0.66 was deployed. To see the error delete this
* comment and run Flow. */
<VirtualizedSectionList
{...this.props}
ref={this._captureRef}
getItemCount={items => items.length}
getItem={(items, index) => items[index]}
/>
);
}
_wrapperListRef: ?React.ElementRef<typeof VirtualizedSectionList>;

View File

@@ -20,18 +20,23 @@ import type {ViewToken} from 'ViewabilityHelper';
import type {Props as VirtualizedListProps} from 'VirtualizedList';
type Item = any;
type SectionItem = any;
type SectionBase = {
// Must be provided directly on each section.
data: $ReadOnlyArray<SectionItem>,
export type SectionBase<SectionItemT> = {
/**
* 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?: ?({
item: SectionItem,
renderItem?: ?(info: {
item: SectionItemT,
index: number,
section: SectionBase,
section: SectionBase<SectionItemT>,
separators: {
highlight: () => void,
unhighlight: () => void,
@@ -39,19 +44,14 @@ type SectionBase = {
},
}) => ?React.Element<any>,
ItemSeparatorComponent?: ?React.ComponentType<any>,
keyExtractor?: (item: SectionItem, index: ?number) => string,
// TODO: support more optional/override props
// FooterComponent?: ?ReactClass<any>,
// HeaderComponent?: ?ReactClass<any>,
// onViewableItemsChanged?: ({viewableItems: Array<ViewToken>, changed: Array<ViewToken>}) => void,
keyExtractor?: (item: SectionItemT, index?: ?number) => string,
};
type RequiredProps<SectionT: SectionBase> = {
type RequiredProps<SectionT: SectionBase<any>> = {
sections: $ReadOnlyArray<SectionT>,
};
type OptionalProps<SectionT: SectionBase> = {
type OptionalProps<SectionT: SectionBase<any>> = {
/**
* Rendered after the last item in the last section.
*/
@@ -131,10 +131,9 @@ type State = {childProps: VirtualizedListProps};
* 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<SectionT: SectionBase> extends React.PureComponent<
Props<SectionT>,
State,
> {
class VirtualizedSectionList<
SectionT: SectionBase<any>,
> extends React.PureComponent<Props<SectionT>, State> {
static defaultProps: DefaultProps = {
...VirtualizedList.defaultProps,
data: [],
@@ -147,8 +146,8 @@ class VirtualizedSectionList<SectionT: SectionBase> extends React.PureComponent<
viewPosition?: number,
}) {
let index = Platform.OS === 'ios' ? params.itemIndex : params.itemIndex + 1;
for (let ii = 0; ii < params.sectionIndex; ii++) {
index += this.props.sections[ii].data.length + 2;
for (let i = 0; i < params.sectionIndex; i++) {
index += this.props.getItemCount(this.props.sections[i].data) + 2;
}
const toIndexParams = {
...params,
@@ -173,10 +172,12 @@ class VirtualizedSectionList<SectionT: SectionBase> extends React.PureComponent<
_computeState(props: Props<SectionT>): State {
const offset = props.ListHeaderComponent ? 1 : 0;
const stickyHeaderIndices = [];
const itemCount = props.sections.reduce((v, section) => {
stickyHeaderIndices.push(v + offset);
return v + section.data.length + 2; // Add two for the section header and footer.
}, 0);
const itemCount = props.sections
? props.sections.reduce((v, section) => {
stickyHeaderIndices.push(v + offset);
return v + props.getItemCount(section.data) + 2; // Add two for the section header and footer.
}, 0)
: 0;
return {
childProps: {
@@ -185,7 +186,8 @@ class VirtualizedSectionList<SectionT: SectionBase> extends React.PureComponent<
ItemSeparatorComponent: undefined, // Rendered with renderItem
data: props.sections,
getItemCount: () => itemCount,
getItem,
// $FlowFixMe
getItem: (sections, index) => getItem(props, sections, index),
keyExtractor: this._keyExtractor,
onViewableItemsChanged: props.onViewableItemsChanged
? this._onViewableItemsChanged
@@ -221,42 +223,41 @@ class VirtualizedSectionList<SectionT: SectionBase> extends React.PureComponent<
trailingSection?: ?SectionT,
} {
let itemIndex = index;
const {sections} = this.props;
for (let ii = 0; ii < sections.length; ii++) {
const section = sections[ii];
const key = section.key || String(ii);
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 >= section.data.length + 1) {
itemIndex -= section.data.length + 1; // The section adds an item for the footer.
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[ii + 1],
trailingSection: sections[i + 1],
};
} else if (itemIndex === section.data.length) {
} else if (itemIndex === getItemCount(sectionData)) {
return {
section,
key: key + ':footer',
index: null,
header: false,
trailingSection: sections[ii + 1],
trailingSection: sections[i + 1],
};
} else {
const keyExtractor = section.keyExtractor || this.props.keyExtractor;
const extractor = section.keyExtractor || keyExtractor;
return {
section,
key: key + ':' + keyExtractor(section.data[itemIndex], itemIndex),
key:
key + ':' + extractor(getItem(sectionData, itemIndex), itemIndex),
index: itemIndex,
leadingItem: section.data[itemIndex - 1],
leadingSection: sections[ii - 1],
trailingItem:
section.data.length > itemIndex + 1
? section.data[itemIndex + 1]
: undefined,
trailingSection: sections[ii + 1],
leadingItem: getItem(sectionData, itemIndex - 1),
leadingSection: sections[i - 1],
trailingItem: getItem(sectionData, itemIndex + 1),
trailingSection: sections[i + 1],
};
}
}
@@ -358,7 +359,8 @@ class VirtualizedSectionList<SectionT: SectionBase> extends React.PureComponent<
info.section.ItemSeparatorComponent || this.props.ItemSeparatorComponent;
const {SectionSeparatorComponent} = this.props;
const isLastItemInList = index === this.state.childProps.getItemCount() - 1;
const isLastItemInSection = info.index === info.section.data.length - 1;
const isLastItemInSection =
info.index === this.props.getItemCount(info.section.data) - 1;
if (SectionSeparatorComponent && isLastItemInSection) {
return SectionSeparatorComponent;
}
@@ -523,22 +525,29 @@ class ItemWithSeparator extends React.Component<
}
}
function getItem(sections: ?$ReadOnlyArray<Item>, index: number): ?Item {
function getItem(
props: Props<SectionBase<any>>,
sections: ?$ReadOnlyArray<Item>,
index: number,
): ?Item {
if (!sections) {
return null;
}
let itemIdx = index - 1;
for (let ii = 0; ii < sections.length; ii++) {
if (itemIdx === -1 || itemIdx === sections[ii].data.length) {
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.
return sections[ii];
} else if (itemIdx < sections[ii].data.length) {
return section;
} else if (itemIdx < itemCount) {
// If we are in the bounds of the list's data then return the item.
return sections[ii].data[itemIdx];
return props.getItem(sectionData, itemIdx);
} else {
itemIdx -= sections[ii].data.length + 2; // Add two for the header and footer
itemIdx -= itemCount + 2; // Add two for the header and footer
}
}
return null;

View File

@@ -0,0 +1,164 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*
* @format
* @emails oncall+react_native
*/
'use strict';
const React = require('React');
const ReactTestRenderer = require('react-test-renderer');
const VirtualizedSectionList = require('VirtualizedSectionList');
describe('VirtualizedSectionList', () => {
it('renders simple list', () => {
const component = ReactTestRenderer.create(
<VirtualizedSectionList
sections={[
{title: 's1', data: [{key: 'i1'}, {key: 'i2'}, {key: 'i3'}]},
]}
renderItem={({item}) => <item value={item.key} />}
getItem={(data, key) => data[key]}
getItemCount={data => data.length}
/>,
);
expect(component).toMatchSnapshot();
});
it('renders empty list', () => {
const component = ReactTestRenderer.create(
<VirtualizedSectionList
sections={[]}
renderItem={({item}) => <item value={item.key} />}
getItem={(data, key) => data[key]}
getItemCount={data => data.length}
/>,
);
expect(component).toMatchSnapshot();
});
it('renders null list', () => {
const component = ReactTestRenderer.create(
<VirtualizedSectionList
sections={undefined}
renderItem={({item}) => <item value={item.key} />}
getItem={(data, key) => data[key]}
getItemCount={data => 0}
/>,
);
expect(component).toMatchSnapshot();
});
it('renders empty list with empty component', () => {
const component = ReactTestRenderer.create(
<VirtualizedSectionList
sections={[]}
ListEmptyComponent={() => <empty />}
ListFooterComponent={() => <footer />}
ListHeaderComponent={() => <header />}
getItem={(data, key) => data[key]}
getItemCount={data => data.length}
renderItem={({item}) => <item value={item.key} />}
/>,
);
expect(component).toMatchSnapshot();
});
it('renders list with empty component', () => {
const component = ReactTestRenderer.create(
<VirtualizedSectionList
sections={[{title: 's1', data: [{key: 'hello'}]}]}
ListEmptyComponent={() => <empty />}
getItem={(data, key) => data[key]}
getItemCount={data => data.length}
renderItem={({item}) => <item value={item.key} />}
/>,
);
expect(component).toMatchSnapshot();
});
it('renders all the bells and whistles', () => {
const component = ReactTestRenderer.create(
<VirtualizedSectionList
ItemSeparatorComponent={() => <separator />}
ListEmptyComponent={() => <empty />}
ListFooterComponent={() => <footer />}
ListHeaderComponent={() => <header />}
sections={[
{
title: 's1',
data: new Array(5).fill().map((_, ii) => ({id: String(ii)})),
},
]}
getItem={(data, key) => data[key]}
getItemCount={data => data.length}
getItemLayout={({index}) => ({length: 50, offset: index * 50})}
inverted={true}
keyExtractor={(item, index) => item.id}
onRefresh={jest.fn()}
refreshing={false}
renderItem={({item}) => <item value={item.id} />}
/>,
);
expect(component).toMatchSnapshot();
});
it('handles separators correctly', () => {
const infos = [];
const component = ReactTestRenderer.create(
<VirtualizedSectionList
ItemSeparatorComponent={props => <separator {...props} />}
sections={[
{title: 's0', data: [{key: 'i0'}, {key: 'i1'}, {key: 'i2'}]},
]}
renderItem={info => {
infos.push(info);
return <item title={info.item.key} />;
}}
getItem={(data, key) => data[key]}
getItemCount={data => data.length}
/>,
);
expect(component).toMatchSnapshot();
infos[1].separators.highlight();
expect(component).toMatchSnapshot();
infos[2].separators.updateProps('leading', {press: true});
expect(component).toMatchSnapshot();
infos[1].separators.unhighlight();
});
it('handles nested lists', () => {
const component = ReactTestRenderer.create(
<VirtualizedSectionList
sections={[{title: 'outer', data: [{key: 'outer0'}, {key: 'outer1'}]}]}
renderItem={outerInfo => (
<VirtualizedSectionList
sections={[
{
title: 'inner',
data: [
{key: outerInfo.item.key + ':inner0'},
{key: outerInfo.item.key + ':inner1'},
],
},
]}
horizontal={outerInfo.item.key === 'outer1'}
renderItem={innerInfo => {
return <item title={innerInfo.item.key} />;
}}
getItem={(data, key) => data[key]}
getItemCount={data => data.length}
/>
)}
getItem={(data, key) => data[key]}
getItemCount={data => data.length}
/>,
);
expect(component).toMatchSnapshot();
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -176,6 +176,9 @@ module.exports = {
get VirtualizedList() {
return require('VirtualizedList');
},
get VirtualizedSectionList() {
return require('VirtualizedSectionList');
},
// APIs
get ActionSheetIOS() {