Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert SearchPage to functional component #23076

Merged
merged 11 commits into from
Sep 14, 2023
170 changes: 76 additions & 94 deletions src/pages/SearchPage.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import _ from 'underscore';
import React, {Component} from 'react';
import React, {useCallback, useEffect, useState} from 'react';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
Expand Down Expand Up @@ -45,69 +45,68 @@ const defaultProps = {
reports: {},
};

class SearchPage extends Component {
constructor(props) {
super(props);
function SearchPage(props) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use prop destructuring now in order to be aligned with the style guidelines -> https://github.com/Expensify/App/blob/main/contributingGuides/STYLE.md#destructuring

Copy link
Contributor

@fabioh8010 fabioh8010 Jul 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, you have to remove usage of defaultProps and assign default values during the prop destructuring.

You don't need to do this one now, we should still use defaultProps in JS files.

const {recentReports: initialRecentReports, personalDetails: initialPersonalDetails, userToInvite: initialUserToInvite} = OptionsListUtils.getSearchOptions(props.reports, props.personalDetails, '', props.betas);
Piotrfj marked this conversation as resolved.
Show resolved Hide resolved

Timing.start(CONST.TIMING.SEARCH_RENDER);
Performance.markStart(CONST.TIMING.SEARCH_RENDER);
const [searchValue, setSearchValue] = useState('')
const [recentReports, setRecentReports] = useState(initialRecentReports)
const [personalDetails, setPersonalDetails] = useState(initialPersonalDetails)
const [userToInvite, setUserToInvite] = useState(initialUserToInvite)

this.searchRendered = this.searchRendered.bind(this);
this.selectReport = this.selectReport.bind(this);
this.onChangeText = this.onChangeText.bind(this);
this.debouncedUpdateOptions = _.debounce(this.updateOptions.bind(this), 75);
const updateOptions = useCallback(() => {
const {recentReports: localRecentReports, personalDetails: localPersonalDetails, userToInvite: localUserToInvite} = OptionsListUtils.getSearchOptions(
props.reports,
props.personalDetails,
searchValue.trim(),
props.betas,
);

const {recentReports, personalDetails, userToInvite} = OptionsListUtils.getSearchOptions(props.reports, props.personalDetails, '', props.betas);
setUserToInvite(localUserToInvite)
setRecentReports(localRecentReports)
setPersonalDetails(localPersonalDetails)
}, [props.reports, props.personalDetails, searchValue, props.betas])

this.state = {
searchValue: '',
recentReports,
personalDetails,
userToInvite,
};
}
const debouncedUpdateOptions =_.debounce(updateOptions, 75);
Copy link
Contributor

@gedu gedu Jul 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_.debounce returns always a new function, probably would be good to wrap it into an useCallback for the useEffect, so you can add it into the dependency array safely

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the app should have the same reference across the re-renders. useCallback is necessary here.


componentDidUpdate(prevProps) {
if (_.isEqual(prevProps.reports, this.props.reports) && _.isEqual(prevProps.personalDetails, this.props.personalDetails)) {
return;
}
this.updateOptions();
}
useEffect(() => {
Timing.start(CONST.TIMING.SEARCH_RENDER);
Performance.markStart(CONST.TIMING.SEARCH_RENDER);
},[])

onChangeText(searchValue = '') {
this.setState({searchValue}, this.debouncedUpdateOptions);
}
useEffect(() => {
updateOptions();
Copy link
Contributor

@gedu gedu Jul 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if updateOptions isn't part of the dependency array, please add a comment explaining why

Copy link
Contributor

@rezkiy37 rezkiy37 Jul 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized that updateOptions useCallback, also is recreated when the searchValue change, I think could be some useEffects and function duplications, would be good to update the logic, and merge where is possible

},[props.reports, props.personalDetails])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this useEffect should have only this function as a dependency. It is logically. Finally, it uses the same deps.

Suggested change
},[props.reports, props.personalDetails])
},[updateOptions])


/**
* Returns the sections needed for the OptionsSelector
*
* @returns {Array}
*/
getSections() {
const getSections = () => {
const sections = [];
let indexOffset = 0;

if (this.state.recentReports.length > 0) {
if (recentReports.length > 0) {
sections.push({
data: this.state.recentReports,
data: recentReports,
shouldShow: true,
indexOffset,
});
indexOffset += this.state.recentReports.length;
indexOffset += recentReports.length;
}

if (this.state.personalDetails.length > 0) {
if (personalDetails.length > 0) {
sections.push({
data: this.state.personalDetails,
data: personalDetails,
shouldShow: true,
indexOffset,
});
indexOffset += this.state.recentReports.length;
indexOffset += recentReports.length;
}

if (this.state.userToInvite) {
if (userToInvite) {
sections.push({
data: [this.state.userToInvite],
data: [userToInvite],
shouldShow: true,
indexOffset,
});
Expand All @@ -116,88 +115,71 @@ class SearchPage extends Component {
return sections;
}

searchRendered() {
const searchRendered = () => {
Timing.end(CONST.TIMING.SEARCH_RENDER);
Performance.markEnd(CONST.TIMING.SEARCH_RENDER);
}

updateOptions() {
const {recentReports, personalDetails, userToInvite} = OptionsListUtils.getSearchOptions(
this.props.reports,
this.props.personalDetails,
this.state.searchValue.trim(),
this.props.betas,
);
this.setState({
userToInvite,
recentReports,
personalDetails,
});
useEffect(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please group all useEffects in one place instead of having them in random places across the component.

debouncedUpdateOptions()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are you updating the options after every new word? You are listening to reports and personalDetails to update the options

}, [searchValue])

const onChangeText = (value = '') => {
setSearchValue(value)
}

/**
* Reset the search value and redirect to the selected report
*
* @param {Object} option
*/
selectReport(option) {
const selectReport = (option) => {
if (!option) {
return;
}

if (option.reportID) {
this.setState(
{
searchValue: '',
},
() => {
Navigation.dismissModal(option.reportID);
},
);
setSearchValue('')
Navigation.dismissModal(option.reportID);
} else {
Report.navigateToAndOpenReport([option.login]);
}
}

render() {
const sections = this.getSections();
const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(this.props.personalDetails);
const headerMessage = OptionsListUtils.getHeaderMessage(
this.state.recentReports.length + this.state.personalDetails.length !== 0,
Boolean(this.state.userToInvite),
this.state.searchValue,
);

return (
<ScreenWrapper includeSafeAreaPaddingBottom={false}>
{({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => (
<>
<HeaderWithBackButton title={this.props.translate('common.search')} />
<View style={[styles.flex1, styles.w100, styles.pRelative]}>
<OptionsSelector
sections={sections}
value={this.state.searchValue}
onSelectRow={this.selectReport}
onChangeText={this.onChangeText}
headerMessage={headerMessage}
hideSectionHeaders
showTitleTooltip
shouldShowOptions={didScreenTransitionEnd && isOptionsDataReady}
textInputLabel={this.props.translate('optionsSelector.nameEmailOrPhoneNumber')}
onLayout={this.searchRendered}
safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle}
/>
</View>
</>
)}
</ScreenWrapper>
);
}
const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(props.personalDetails);
const headerMessage = OptionsListUtils.getHeaderMessage(
recentReports.length + personalDetails.length !== 0,
Boolean(userToInvite),
searchValue,
);
return (
<ScreenWrapper includeSafeAreaPaddingBottom={false}>
{({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => (
<>
<HeaderWithBackButton title={props.translate('common.search')} />
<View style={[styles.flex1, styles.w100, styles.pRelative]}>
<OptionsSelector
sections={getSections()}
value={searchValue}
onSelectRow={selectReport}
onChangeText={onChangeText}
headerMessage={headerMessage}
hideSectionHeaders
showTitleTooltip
shouldShowOptions={didScreenTransitionEnd && isOptionsDataReady}
textInputLabel={props.translate('optionsSelector.nameEmailOrPhoneNumber')}
onLayout={searchRendered}
safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle}
/>
</View>
</>
)}
</ScreenWrapper>
)
}

SearchPage.propTypes = propTypes;
SearchPage.defaultProps = defaultProps;

SearchPage.displayName = 'SearchPage';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use hooks useLocalize and useWindowDimensions

export default compose(
withLocalize,
withWindowDimensions,
Expand Down