Skip to content
Open
Changes from all 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
70 changes: 63 additions & 7 deletions src/pages/inbox/ReportNotFoundGuard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,25 @@ type ReportNotFoundGuardProps = {
children: ReactNode;
};

/**
* Two-level gate: the outer guard subscribes to lightweight keys to determine
* whether the report clearly exists. When it does (and the report is not a
* transaction thread), children render directly without the cost of
* parentReportMetadata / useParentReportAction subscriptions.
*
* The inner gate mounts only when the "not found" path is plausible:
* the report is missing, the path is invalid, or the report is a transaction
* thread whose parent action may have been deleted.
*/
// eslint-disable-next-line rulesdir/no-negated-variables
function ReportNotFoundGuard({children}: ReportNotFoundGuardProps) {
const styles = useThemeStyles();
const route = useRoute();
const routeParams = route.params as {reportID?: string} | undefined;
const reportIDFromRoute = getNonEmptyStringOnyxID(routeParams?.reportID);

const {isOffline} = useNetwork();
const {shouldUseNarrowLayout} = useResponsiveLayout();

const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDFromRoute}`);
const [parentReportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${report?.parentReportID}`);
const [userLeavingStatus = false] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${reportIDFromRoute}`);
const [isLoadingInitialReportActions = true] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportIDFromRoute}`, {
selector: isLoadingInitialReportActionsSelector,
Expand All @@ -40,9 +47,60 @@ function ReportNotFoundGuard({children}: ReportNotFoundGuardProps) {
const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP);
const [deleteTransactionNavigateBackUrl] = useOnyx(ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL);

const reportID = report?.reportID;
const isOptimisticDelete = report?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED;
const isInvalidReportPath = !!routeParams?.reportID && !isValidReportIDFromPath(routeParams.reportID);
const isLoading = isLoadingApp !== false || isLoadingReportData || (!isOffline && !!isLoadingInitialReportActions);
const mayBeTransactionThread = isReportTransactionThread(report);

// Fast path: skip the expensive inner guard (parentReportMetadata, useParentReportAction)
// when we can determine shouldShowNotFoundPage is definitely false.
//
// shouldShowNotFoundPage is false when:
// - deleteTransactionNavigateBackUrl is set (always suppresses not-found), OR
// - path is valid AND report is not a transaction thread AND (still loading OR report exists)
const reportClearlyExists = !!reportID || isOptimisticDelete || userLeavingStatus;
const canSkipInnerGuard = !!deleteTransactionNavigateBackUrl || (!isInvalidReportPath && !mayBeTransactionThread && (isLoading || reportClearlyExists));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep transaction-thread detection reactive before skipping inner guard

canSkipInnerGuard relies on mayBeTransactionThread, but isReportTransactionThread(report) depends on parent report actions that may not be in memory on first render (e.g., deep-linking directly into a transaction thread). In that case this evaluates to false, the fast path returns children, and this component no longer subscribes to parent action/metadata updates that would later reveal the thread is deleted; the Not Found view can therefore be skipped for deleted/inaccessible transaction threads until another unrelated rerender happens.

Useful? React with 👍 / 👎.

if (canSkipInnerGuard) {
return children;
}

return <ReportNotFoundInnerGuard reportIDFromPath={routeParams?.reportID}>{children}</ReportNotFoundInnerGuard>;
}

type ReportNotFoundInnerGuardProps = {
reportIDFromPath: string | undefined;
children: ReactNode;
};

/**
* Inner guard that mounts only when the "not found" path is plausible.
* Re-derives all state from its own hooks (Onyx returns cached values
* synchronously, so the extra subscriptions are near-zero cost).
*/
// eslint-disable-next-line rulesdir/no-negated-variables
function ReportNotFoundInnerGuard({reportIDFromPath, children}: ReportNotFoundInnerGuardProps) {
const styles = useThemeStyles();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const {isOffline} = useNetwork();

const reportIDFromRoute = getNonEmptyStringOnyxID(reportIDFromPath);

const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDFromRoute}`);
const [userLeavingStatus = false] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${reportIDFromRoute}`);
const [isLoadingInitialReportActions = true] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportIDFromRoute}`, {
selector: isLoadingInitialReportActionsSelector,
});
const [isLoadingReportData = true] = useOnyx(ONYXKEYS.IS_LOADING_REPORT_DATA);
const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP);
const [deleteTransactionNavigateBackUrl] = useOnyx(ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL);
const [parentReportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${report?.parentReportID}`);
const parentReportAction = useParentReportAction(report);

const reportID = report?.reportID;
const isOptimisticDelete = report?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED;
const isInvalidReportPath = !!reportIDFromPath && !isValidReportIDFromPath(reportIDFromPath);
const isLoading = isLoadingApp !== false || isLoadingReportData || (!isOffline && !!isLoadingInitialReportActions);

const {isParentActionMissingAfterLoad, isParentActionDeleted} = getParentReportActionDeletionStatus({
parentReportID: report?.parentReportID,
Expand All @@ -53,8 +111,6 @@ function ReportNotFoundGuard({children}: ReportNotFoundGuardProps) {
});
const isDeletedTransactionThread = isReportTransactionThread(report) && (isParentActionDeleted || isParentActionMissingAfterLoad);

const isInvalidReportPath = !!routeParams?.reportID && !isValidReportIDFromPath(routeParams.reportID);
const isLoading = isLoadingApp !== false || isLoadingReportData || (!isOffline && !!isLoadingInitialReportActions);
const reportExists = !!reportID || (!isDeletedTransactionThread && isOptimisticDelete) || userLeavingStatus;

// eslint-disable-next-line rulesdir/no-negated-variables
Expand All @@ -73,7 +129,7 @@ function ReportNotFoundGuard({children}: ReportNotFoundGuardProps) {
reportID,
isOptimisticDelete,
userLeavingStatus,
reportIDFromPath: routeParams?.reportID,
reportIDFromPath,
deleteTransactionNavigateBackUrl,
isDeletedTransactionThread,
isParentActionDeleted,
Expand All @@ -88,7 +144,7 @@ function ReportNotFoundGuard({children}: ReportNotFoundGuardProps) {
reportID,
isOptimisticDelete,
userLeavingStatus,
routeParams?.reportID,
reportIDFromPath,
deleteTransactionNavigateBackUrl,
isDeletedTransactionThread,
isParentActionDeleted,
Expand Down
Loading