Skip to content

Commit

Permalink
Merge pull request #25401 from software-mansion-labs/migrate-baseopti…
Browse files Browse the repository at this point in the history
…onslist-to-functional-component

Migrate BaseOptionsList.js to functional component
  • Loading branch information
marcaaron authored Aug 25, 2023
2 parents f2a952d + 1998c14 commit 22c1687
Showing 1 changed file with 151 additions and 141 deletions.
292 changes: 151 additions & 141 deletions src/components/OptionsList/BaseOptionsList.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import _ from 'underscore';
import React, {forwardRef, Component} from 'react';
import React, {useRef, useEffect, forwardRef, memo} from 'react';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import styles from '../../styles/styles';
Expand All @@ -9,6 +9,7 @@ import SectionList from '../SectionList';
import Text from '../Text';
import {propTypes as optionsListPropTypes, defaultProps as optionsListDefaultProps} from './optionsListPropTypes';
import OptionsListSkeletonView from '../OptionsListSkeletonView';
import usePrevious from '../../hooks/usePrevious';

const propTypes = {
/** Determines whether the keyboard gets dismissed in response to a drag */
Expand All @@ -30,47 +31,89 @@ const defaultProps = {
...optionsListDefaultProps,
};

class BaseOptionsList extends Component {
constructor(props) {
super(props);
function BaseOptionsList({
keyboardDismissMode,
onScrollBeginDrag,
onScroll,
focusedIndex,
selectedOptions,
headerMessage,
isLoading,
sections,
onLayout,
hideSectionHeaders,
shouldHaveOptionSeparator,
showTitleTooltip,
optionHoveredStyle,
contentContainerStyles,
showScrollIndicator,
listContainerStyles,
shouldDisableRowInnerPadding,
disableFocusOptions,
canSelectMultipleOptions,
onSelectRow,
boldStyle,
isDisabled,
innerRef,
}) {
const flattenedData = useRef();
const previousSections = usePrevious(sections);
const didLayout = useRef(false);

this.renderItem = this.renderItem.bind(this);
this.renderSectionHeader = this.renderSectionHeader.bind(this);
this.getItemLayout = this.getItemLayout.bind(this);
this.buildFlatSectionArray = this.buildFlatSectionArray.bind(this);
this.extractKey = this.extractKey.bind(this);
this.onViewableItemsChanged = this.onViewableItemsChanged.bind(this);
this.didLayout = false;
/**
* This helper function is used to memoize the computation needed for getItemLayout. It is run whenever section data changes.
*
* @returns {Array<Object>}
*/
const buildFlatSectionArray = () => {
let offset = 0;

this.flattenedData = this.buildFlatSectionArray();
}
// Start with just an empty list header
const flatArray = [{length: 0, offset}];

shouldComponentUpdate(nextProps) {
return (
nextProps.focusedIndex !== this.props.focusedIndex ||
nextProps.selectedOptions.length !== this.props.selectedOptions.length ||
nextProps.headerMessage !== this.props.headerMessage ||
nextProps.isLoading !== this.props.isLoading ||
!_.isEqual(nextProps.sections, this.props.sections)
);
}
// Build the flat array
for (let sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
const section = sections[sectionIndex];

componentDidUpdate(prevProps) {
if (_.isEqual(this.props.sections, prevProps.sections)) {
return;
// Add the section header
const sectionHeaderHeight = section.title && !hideSectionHeaders ? variables.optionsListSectionHeaderHeight : 0;
flatArray.push({length: sectionHeaderHeight, offset});
offset += sectionHeaderHeight;

// Add section items
for (let i = 0; i < section.data.length; i++) {
let fullOptionHeight = variables.optionRowHeight;
if (i > 0 && shouldHaveOptionSeparator) {
fullOptionHeight += variables.borderTopWidth;
}
flatArray.push({length: fullOptionHeight, offset});
offset += fullOptionHeight;
}

// Add the section footer
flatArray.push({length: 0, offset});
}

this.flattenedData = this.buildFlatSectionArray();
}
// Then add the list footer
flatArray.push({length: 0, offset});
return flatArray;
};

useEffect(() => {
if (_.isEqual(sections, previousSections)) {
return;
}
flattenedData.current = buildFlatSectionArray();
});

onViewableItemsChanged() {
if (this.didLayout || !this.props.onLayout) {
const onViewableItemsChanged = () => {
if (didLayout.current || !onLayout) {
return;
}

this.didLayout = true;
this.props.onLayout();
}
didLayout.current = true;
onLayout();
};

/**
* This function is used to compute the layout of any given item in our list.
Expand All @@ -88,66 +131,25 @@ class BaseOptionsList extends Component {
*
* @returns {Object}
*/
getItemLayout(data, flatDataArrayIndex) {
if (!_.has(this.flattenedData, flatDataArrayIndex)) {
this.flattenedData = this.buildFlatSectionArray();
const getItemLayout = (data, flatDataArrayIndex) => {
if (!_.has(flattenedData.current, flatDataArrayIndex)) {
flattenedData.current = buildFlatSectionArray();
}

const targetItem = this.flattenedData[flatDataArrayIndex];
const targetItem = flattenedData.current[flatDataArrayIndex];
return {
length: targetItem.length,
offset: targetItem.offset,
index: flatDataArrayIndex,
};
}

/**
* This helper function is used to memoize the computation needed for getItemLayout. It is run whenever section data changes.
*
* @returns {Array<Object>}
*/
buildFlatSectionArray() {
let offset = 0;

// Start with just an empty list header
const flatArray = [{length: 0, offset}];

// Build the flat array
for (let sectionIndex = 0; sectionIndex < this.props.sections.length; sectionIndex++) {
const section = this.props.sections[sectionIndex];

// Add the section header
const sectionHeaderHeight = section.title && !this.props.hideSectionHeaders ? variables.optionsListSectionHeaderHeight : 0;
flatArray.push({length: sectionHeaderHeight, offset});
offset += sectionHeaderHeight;

// Add section items
for (let i = 0; i < section.data.length; i++) {
let fullOptionHeight = variables.optionRowHeight;
if (i > 0 && this.props.shouldHaveOptionSeparator) {
fullOptionHeight += variables.borderTopWidth;
}
flatArray.push({length: fullOptionHeight, offset});
offset += fullOptionHeight;
}

// Add the section footer
flatArray.push({length: 0, offset});
}

// Then add the list footer
flatArray.push({length: 0, offset});
return flatArray;
}
};

/**
* Returns the key used by the list
* @param {Object} option
* @return {String}
*/
extractKey(option) {
return option.keyForList;
}
const extractKey = (option) => option.keyForList;

/**
* Function which renders a row in the list
Expand All @@ -159,24 +161,24 @@ class BaseOptionsList extends Component {
*
* @return {Component}
*/
renderItem({item, index, section}) {
const isDisabled = this.props.isDisabled || section.isDisabled || !!item.isDisabled;
const renderItem = ({item, index, section}) => {
const isItemDisabled = isDisabled || section.isDisabled || !!item.isDisabled;
return (
<OptionRow
option={item}
showTitleTooltip={this.props.showTitleTooltip}
hoverStyle={this.props.optionHoveredStyle}
optionIsFocused={!this.props.disableFocusOptions && !isDisabled && this.props.focusedIndex === index + section.indexOffset}
onSelectRow={this.props.onSelectRow}
isSelected={Boolean(_.find(this.props.selectedOptions, (option) => option.accountID === item.accountID))}
showSelectedState={this.props.canSelectMultipleOptions}
boldStyle={this.props.boldStyle}
isDisabled={isDisabled}
shouldHaveOptionSeparator={index > 0 && this.props.shouldHaveOptionSeparator}
shouldDisableRowInnerPadding={this.props.shouldDisableRowInnerPadding}
showTitleTooltip={showTitleTooltip}
hoverStyle={optionHoveredStyle}
optionIsFocused={!disableFocusOptions && !isItemDisabled && focusedIndex === index + section.indexOffset}
onSelectRow={onSelectRow}
isSelected={Boolean(_.find(selectedOptions, (option) => option.accountID === item.accountID))}
showSelectedState={canSelectMultipleOptions}
boldStyle={boldStyle}
isDisabled={isItemDisabled}
shouldHaveOptionSeparator={index > 0 && shouldHaveOptionSeparator}
shouldDisableRowInnerPadding={shouldDisableRowInnerPadding}
/>
);
}
};

/**
* Function which renders a section header component
Expand All @@ -188,8 +190,8 @@ class BaseOptionsList extends Component {
*
* @return {Component}
*/
renderSectionHeader({section: {title, shouldShow}}) {
if (title && shouldShow && !this.props.hideSectionHeaders) {
const renderSectionHeader = ({section: {title, shouldShow}}) => {
if (title && shouldShow && !hideSectionHeaders) {
return (
// Note: The `optionsListSectionHeader` style provides an explicit height to section headers.
// We do this so that we can reference the height in `getItemLayout` –
Expand All @@ -202,56 +204,64 @@ class BaseOptionsList extends Component {
}

return <View />;
}
};

render() {
return (
<View style={this.props.listContainerStyles}>
{this.props.isLoading ? (
<OptionsListSkeletonView />
) : (
<>
{this.props.headerMessage ? (
<View style={[styles.ph5, styles.pb5]}>
<Text style={[styles.textLabel, styles.colorMuted]}>{this.props.headerMessage}</Text>
</View>
) : null}
<SectionList
ref={this.props.innerRef}
indicatorStyle="white"
keyboardShouldPersistTaps="always"
keyboardDismissMode={this.props.keyboardDismissMode}
onScrollBeginDrag={this.props.onScrollBeginDrag}
onScroll={this.props.onScroll}
contentContainerStyle={this.props.contentContainerStyles}
showsVerticalScrollIndicator={this.props.showScrollIndicator}
sections={this.props.sections}
keyExtractor={this.extractKey}
stickySectionHeadersEnabled={false}
renderItem={this.renderItem}
getItemLayout={this.getItemLayout}
renderSectionHeader={this.renderSectionHeader}
extraData={this.props.focusedIndex}
initialNumToRender={12}
maxToRenderPerBatch={5}
windowSize={5}
viewabilityConfig={{viewAreaCoveragePercentThreshold: 95}}
onViewableItemsChanged={this.onViewableItemsChanged}
/>
</>
)}
</View>
);
}
return (
<View style={listContainerStyles}>
{isLoading ? (
<OptionsListSkeletonView />
) : (
<>
{headerMessage ? (
<View style={[styles.ph5, styles.pb5]}>
<Text style={[styles.textLabel, styles.colorMuted]}>{headerMessage}</Text>
</View>
) : null}
<SectionList
ref={innerRef}
indicatorStyle="white"
keyboardShouldPersistTaps="always"
keyboardDismissMode={keyboardDismissMode}
onScrollBeginDrag={onScrollBeginDrag}
onScroll={onScroll}
contentContainerStyle={contentContainerStyles}
showsVerticalScrollIndicator={showScrollIndicator}
sections={sections}
keyExtractor={extractKey}
stickySectionHeadersEnabled={false}
renderItem={renderItem}
getItemLayout={getItemLayout}
renderSectionHeader={renderSectionHeader}
extraData={focusedIndex}
initialNumToRender={12}
maxToRenderPerBatch={5}
windowSize={5}
viewabilityConfig={{viewAreaCoveragePercentThreshold: 95}}
onViewableItemsChanged={onViewableItemsChanged}
/>
</>
)}
</View>
);
}

BaseOptionsList.propTypes = propTypes;
BaseOptionsList.defaultProps = defaultProps;
BaseOptionsList.displayName = 'BaseOptionsList';

export default forwardRef((props, ref) => (
<BaseOptionsList
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
innerRef={ref}
/>
));
// using memo to avoid unnecessary rerenders when parents component rerenders (thus causing this component to rerender because shallow comparison is used for some props).
export default memo(
forwardRef((props, ref) => (
<BaseOptionsList
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
innerRef={ref}
/>
)),
(prevProps, nextProps) =>
nextProps.focusedIndex === prevProps.focusedIndex &&
nextProps.selectedOptions.length === prevProps.selectedOptions.length &&
nextProps.headerMessage === prevProps.headerMessage &&
nextProps.isLoading === prevProps.isLoading &&
_.isEqual(nextProps.sections, prevProps.sections),
);

0 comments on commit 22c1687

Please sign in to comment.