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

Separate ReportScreenContext to prevent cyclic re-renders #25948

Closed
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/components/Reactions/ReportActionItemEmojiReactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import EmojiReactionsPropTypes from './EmojiReactionsPropTypes';
import Tooltip from '../Tooltip';
import ReactionTooltipContent from './ReactionTooltipContent';
import * as EmojiUtils from '../../libs/EmojiUtils';
import ReportScreenContext from '../../pages/home/ReportScreenContext';
import {ReactionListContext} from '../../pages/home/ReportScreenContext';

const propTypes = {
emojiReactions: EmojiReactionsPropTypes,
Expand Down Expand Up @@ -41,7 +41,7 @@ const defaultProps = {
};

function ReportActionItemEmojiReactions(props) {
const {reactionListRef} = useContext(ReportScreenContext);
const reactionListRef = useContext(ReactionListContext);
const popoverReactionListAnchor = useRef(null);
let totalReactionCount = 0;

Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useReportScrollManager/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {useContext, useCallback} from 'react';
import ReportScreenContext from '../../pages/home/ReportScreenContext';
import {ActionListContext} from '../../pages/home/ReportScreenContext';

function useReportScrollManager() {
const {flatListRef} = useContext(ReportScreenContext);
const flatListRef = useContext(ActionListContext);

/**
* Scroll to the provided index. On non-native implementations we do not want to scroll when we are scrolling because
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useReportScrollManager/index.native.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {useContext, useCallback} from 'react';
import ReportScreenContext from '../../pages/home/ReportScreenContext';
import {ActionListContext} from '../../pages/home/ReportScreenContext';

function useReportScrollManager() {
const {flatListRef} = useContext(ReportScreenContext);
const flatListRef = useContext(ActionListContext);

/**
* Scroll to the provided index.
Expand Down
188 changes: 92 additions & 96 deletions src/pages/home/ReportScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import getIsReportFullyVisible from '../../libs/getIsReportFullyVisible';
import MoneyRequestHeader from '../../components/MoneyRequestHeader';
import MoneyReportHeader from '../../components/MoneyReportHeader';
import * as ComposerActions from '../../libs/actions/Composer';
import ReportScreenContext from './ReportScreenContext';
import {ActionListContext, ReactionListContext} from './ReportScreenContext';
import TaskHeaderActionButton from '../../components/TaskHeaderActionButton';
import DragAndDropProvider from '../../components/DragAndDrop/Provider';

Expand Down Expand Up @@ -276,111 +276,107 @@ class ReportScreen extends React.Component {
}

return (
<ReportScreenContext.Provider
value={{
flatListRef: this.flatListRef,
reactionListRef: this.reactionListRef,
}}
>
<ScreenWrapper
style={screenWrapperStyle}
shouldEnableKeyboardAvoidingView={isTopMostReportId}
>
<FullPageNotFoundView
shouldShow={(!this.props.report.reportID && !this.props.report.isLoadingReportActions && !isLoading) || shouldHideReport}
subtitleKey="notFound.noAccess"
shouldShowCloseButton={false}
shouldShowBackButton={this.props.isSmallScreenWidth}
onBackButtonPress={Navigation.goBack}
shouldShowLink={false}
<ActionListContext.Provider value={this.flatListRef}>
<ReactionListContext.Provider value={this.reactionListRef}>
<ScreenWrapper
style={screenWrapperStyle}
shouldEnableKeyboardAvoidingView={isTopMostReportId}
>
<OfflineWithFeedback
pendingAction={addWorkspaceRoomOrChatPendingAction}
errors={addWorkspaceRoomOrChatErrors}
shouldShowErrorMessages={false}
needsOffscreenAlphaCompositing
<FullPageNotFoundView
shouldShow={(!this.props.report.reportID && !this.props.report.isLoadingReportActions && !isLoading) || shouldHideReport}
subtitleKey="notFound.noAccess"
shouldShowCloseButton={false}
shouldShowBackButton={this.props.isSmallScreenWidth}
onBackButtonPress={Navigation.goBack}
shouldShowLink={false}
>
{headerView}
{ReportUtils.isTaskReport(this.props.report) && this.props.isSmallScreenWidth && ReportUtils.isOpenTaskReport(this.props.report) && (
<View style={[styles.borderBottom]}>
<View style={[styles.appBG, styles.pl0]}>
<View style={[styles.ph5, styles.pb3]}>
<TaskHeaderActionButton report={this.props.report} />
<OfflineWithFeedback
pendingAction={addWorkspaceRoomOrChatPendingAction}
errors={addWorkspaceRoomOrChatErrors}
shouldShowErrorMessages={false}
needsOffscreenAlphaCompositing
>
{headerView}
{ReportUtils.isTaskReport(this.props.report) && this.props.isSmallScreenWidth && ReportUtils.isOpenTaskReport(this.props.report) && (
<View style={[styles.borderBottom]}>
<View style={[styles.appBG, styles.pl0]}>
<View style={[styles.ph5, styles.pb3]}>
<TaskHeaderActionButton report={this.props.report} />
</View>
</View>
</View>
</View>
)}
</OfflineWithFeedback>
{Boolean(this.props.accountManagerReportID) && ReportUtils.isConciergeChatReport(this.props.report) && this.state.isBannerVisible && (
<Banner
containerStyles={[styles.mh4, styles.mt4, styles.p4, styles.bgDark]}
textStyles={[styles.colorReversed]}
text={this.props.translate('reportActionsView.chatWithAccountManager')}
onClose={this.dismissBanner}
onPress={this.chatWithAccountManager}
shouldShowCloseButton
/>
)}
<DragAndDropProvider isDisabled={!this.isReportReadyForDisplay()}>
<View
style={[styles.flex1, styles.justifyContentEnd, styles.overflowHidden]}
onLayout={(event) => {
// Rounding this value for comparison because they can look like this: 411.9999694824219
const skeletonViewContainerHeight = Math.round(event.nativeEvent.layout.height);

// Only set state when the height changes to avoid unnecessary renders
if (reportActionsListViewHeight === skeletonViewContainerHeight) return;

// The height can be 0 if the component unmounts - we are not interested in this value and want to know how much space it
// takes up so we can set the skeleton view container height.
if (skeletonViewContainerHeight === 0) {
return;
}
reportActionsListViewHeight = skeletonViewContainerHeight;
this.setState({skeletonViewContainerHeight});
}}
>
{this.isReportReadyForDisplay() && !isLoadingInitialReportActions && !isLoading && (
<ReportActionsView
reportActions={this.props.reportActions}
report={this.props.report}
isComposerFullSize={this.props.isComposerFullSize}
parentViewHeight={this.state.skeletonViewContainerHeight}
policy={policy}
/>
)}

{/* Note: The report should be allowed to mount even if the initial report actions are not loaded. If we prevent rendering the report while they are loading then
we'll unnecessarily unmount the ReportActionsView which will clear the new marker lines initial state. */}
{(!this.isReportReadyForDisplay() || isLoadingInitialReportActions || isLoading) && (
<ReportActionsSkeletonView containerHeight={this.state.skeletonViewContainerHeight} />
)}

{this.isReportReadyForDisplay() && (
<>
<ReportFooter
pendingAction={addWorkspaceRoomOrChatPendingAction}
isOffline={this.props.network.isOffline}
</OfflineWithFeedback>
{Boolean(this.props.accountManagerReportID) && ReportUtils.isConciergeChatReport(this.props.report) && this.state.isBannerVisible && (
<Banner
containerStyles={[styles.mh4, styles.mt4, styles.p4, styles.bgDark]}
textStyles={[styles.colorReversed]}
text={this.props.translate('reportActionsView.chatWithAccountManager')}
onClose={this.dismissBanner}
onPress={this.chatWithAccountManager}
shouldShowCloseButton
/>
)}
<DragAndDropProvider isDisabled={!this.isReportReadyForDisplay()}>
<View
style={[styles.flex1, styles.justifyContentEnd, styles.overflowHidden]}
onLayout={(event) => {
// Rounding this value for comparison because they can look like this: 411.9999694824219
Copy link
Contributor

Choose a reason for hiding this comment

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

Please move this logic to a method outside of the return()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

All these are part of linting (the component it is now wrapped into a new context provider), I have not changed or touched the internal logic. I can do it if you want, but it falls out of the scope of the performance improvements

const skeletonViewContainerHeight = Math.round(event.nativeEvent.layout.height);

// Only set state when the height changes to avoid unnecessary renders
if (reportActionsListViewHeight === skeletonViewContainerHeight) return;

// The height can be 0 if the component unmounts - we are not interested in this value and want to know how much space it
// takes up so we can set the skeleton view container height.
if (skeletonViewContainerHeight === 0) {
return;
}
reportActionsListViewHeight = skeletonViewContainerHeight;
this.setState({skeletonViewContainerHeight});
}}
>
{this.isReportReadyForDisplay() && !isLoadingInitialReportActions && !isLoading && (
<ReportActionsView
reportActions={this.props.reportActions}
report={this.props.report}
isComposerFullSize={this.props.isComposerFullSize}
onSubmitComment={this.onSubmitComment}
policies={this.props.policies}
policy={policy}
/>
</>
)}
)}

{!this.isReportReadyForDisplay() && (
<ReportFooter
shouldDisableCompose
isOffline={this.props.network.isOffline}
/>
)}
</View>
</DragAndDropProvider>
</FullPageNotFoundView>
</ScreenWrapper>
</ReportScreenContext.Provider>
{/* Note: The report should be allowed to mount even if the initial report actions are not loaded. If we prevent rendering the report while they are loading then
Copy link
Contributor

Choose a reason for hiding this comment

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

The report should be allowed to mount

Can you please clarify in this comment what "the report" is? Is it:

  • ReportActionsSkeletonView
  • ReportFooter
  • ReportFooter
  • All three?
  • Something else?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

All these are part of linting (the component it is now wrapped into a new context provider), I have not changed or touched the internal logic. I can do it if you want, but it falls out of the scope of the performance improvements

Copy link
Contributor

Choose a reason for hiding this comment

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

Aren't you lucky that you stumbled into this code? 😀

I guess it's not a blocker for this particular PR, but I am always happy to see it when people leave the code better than how they found it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok, I will take care of it then :)

we'll unnecessarily unmount the ReportActionsView which will clear the new marker lines initial state. */}
{(!this.isReportReadyForDisplay() || isLoadingInitialReportActions || isLoading) && (
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 making this a ternary with the block above it will improve readability. It makes it easier to understand that the intention is that either one or the other should be displayed, never both.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

All these are part of linting (the component it is now wrapped into a new context provider), I have not changed or touched the internal logic. I can do it if you want, but it falls out of the scope of the performance improvements

<ReportActionsSkeletonView containerHeight={this.state.skeletonViewContainerHeight} />
)}

{this.isReportReadyForDisplay() && (
<>
<ReportFooter
pendingAction={addWorkspaceRoomOrChatPendingAction}
isOffline={this.props.network.isOffline}
reportActions={this.props.reportActions}
report={this.props.report}
isComposerFullSize={this.props.isComposerFullSize}
onSubmitComment={this.onSubmitComment}
policies={this.props.policies}
/>
</>
)}

{!this.isReportReadyForDisplay() && (
<ReportFooter
shouldDisableCompose
isOffline={this.props.network.isOffline}
/>
)}
Copy link
Contributor

Choose a reason for hiding this comment

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

These should be a ternary

Copy link
Contributor Author

Choose a reason for hiding this comment

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

All these are part of linting (the component it is now wrapped into a new context provider), I have not changed or touched the internal logic. I can do it if you want, but it falls out of the scope of the performance improvements

</View>
</DragAndDropProvider>
</FullPageNotFoundView>
</ScreenWrapper>
</ReactionListContext.Provider>
</ActionListContext.Provider>
);
}
}
Expand Down
6 changes: 4 additions & 2 deletions src/pages/home/ReportScreenContext.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {createContext} from 'react';

const ReportScreenContext = createContext();
export default ReportScreenContext;
const ActionListContext = createContext();
const ReactionListContext = createContext();

export {ActionListContext, ReactionListContext};
4 changes: 2 additions & 2 deletions src/pages/home/report/ReportActionItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ import * as PersonalDetailsUtils from '../../../libs/PersonalDetailsUtils';
import ReportActionItemBasicMessage from './ReportActionItemBasicMessage';
import * as store from '../../../libs/actions/ReimbursementAccount/store';
import * as BankAccounts from '../../../libs/actions/BankAccounts';
import ReportScreenContext from '../ReportScreenContext';
import {ReactionListContext} from '../ReportScreenContext';
import Permissions from '../../../libs/Permissions';

const propTypes = {
Expand Down Expand Up @@ -127,7 +127,7 @@ function ReportActionItem(props) {
const [isContextMenuActive, setIsContextMenuActive] = useState(ReportActionContextMenu.isActiveReportAction(props.action.reportActionID));
const [isHidden, setIsHidden] = useState(false);
const [moderationDecision, setModerationDecision] = useState(CONST.MODERATION.MODERATOR_DECISION_APPROVED);
const {reactionListRef} = useContext(ReportScreenContext);
const reactionListRef = useContext(ReactionListContext);
const textInputRef = useRef();
const popoverAnchorRef = useRef();
const downloadedPreviews = useRef([]);
Expand Down
23 changes: 11 additions & 12 deletions src/pages/home/report/ReportActionsList.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@ function ReportActionsList({
const readActionSkipped = useRef(false);
const reportActionSize = useRef(sortedReportActions.length);

// Considering that renderItem is enclosed within a useCallback, marking it as "read" twice will retain the value as "true," preventing the useCallback from re-executing.
// However, if we create and listen to an object, it will lead to a new useCallback execution.
const [messageManuallyMarked, setMessageManuallyMarked] = useState({read: false});
// This state is used to force a re-render when the user manually marks a message as unread
// by using a timestamp you can force re-renders without having to worry about if another message was marked as unread before
const [messageManuallyMarkedUnread, setMessageManuallyMarkedUnread] = useState(0);
const [isFloatingMessageCounterVisible, setIsFloatingMessageCounterVisible] = useState(false);
const animatedStyles = useAnimatedStyle(() => ({
opacity: opacity.value,
Expand Down Expand Up @@ -163,15 +163,14 @@ function ReportActionsList({

useEffect(() => {
const didManuallyMarkReportAsUnread = report.lastReadTime < DateUtils.getDBTime() && ReportUtils.isUnread(report);
if (!didManuallyMarkReportAsUnread) {
setMessageManuallyMarked({read: false});
return;
if (didManuallyMarkReportAsUnread) {
// Clearing the current unread marker so that it can be recalculated
currentUnreadMarker.current = null;
setMessageManuallyMarkedUnread(new Date().getTime());
} else {
setMessageManuallyMarkedUnread(0);
}

// Clearing the current unread marker so that it can be recalculated
currentUnreadMarker.current = null;
setMessageManuallyMarked({read: true});

// We only care when a new lastReadTime is set in the report
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [report.lastReadTime]);
Expand Down Expand Up @@ -230,7 +229,7 @@ function ReportActionsList({
const isCurrentMessageUnread = isMessageUnread(reportAction, report.lastReadTime);
shouldDisplayNewMarker = isCurrentMessageUnread && !isMessageUnread(nextMessage, report.lastReadTime);

if (!messageManuallyMarked.read) {
if (!messageManuallyMarkedUnread) {
shouldDisplayNewMarker = shouldDisplayNewMarker && reportAction.actorAccountID !== Report.getCurrentUserAccountID();
}
const canDisplayMarker = scrollingVerticalOffset.current < MSG_VISIBLE_THRESHOLD ? reportAction.created < userActiveSince.current : true;
Expand Down Expand Up @@ -272,7 +271,7 @@ function ReportActionsList({
/>
);
},
[report, hasOutstandingIOU, sortedReportActions, mostRecentIOUReportActionID, messageManuallyMarked],
[report, hasOutstandingIOU, sortedReportActions, mostRecentIOUReportActionID, messageManuallyMarkedUnread],
);

// Native mobile does not render updates flatlist the changes even though component did update called.
Expand Down
Loading
Loading