Skip to content

Commit

Permalink
- Externalise and handle any sort of data blob (#20787)
Browse files Browse the repository at this point in the history
Summary:
Fixes #20770
Pull Request resolved: #20787

Reviewed By: sahrens

Differential Revision: D9485598

Pulled By: cpojer

fbshipit-source-id: edddebf6b5e1ca396ab1a519baf019c1e5188d44
  • Loading branch information
magrinj authored and facebook-github-bot committed Apr 18, 2019
1 parent f81d77c commit 1946aee
Show file tree
Hide file tree
Showing 5 changed files with 1,508 additions and 85 deletions.
49 changes: 16 additions & 33 deletions Libraries/Lists/SectionList.js
Original file line number Diff line number Diff line change
Expand Up @@ -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>> = {
/**
Expand Down Expand Up @@ -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>;
Expand Down
113 changes: 61 additions & 52 deletions Libraries/Lists/VirtualizedSectionList.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,38 +20,38 @@ 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,
updateProps: (select: 'leading' | 'trailing', newProps: Object) => void,
},
}) => ?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.
*/
Expand Down Expand Up @@ -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: [],
Expand All @@ -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,
Expand All @@ -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: {
Expand All @@ -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
Expand Down Expand Up @@ -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],
};
}
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 1946aee

Please sign in to comment.