Skip to content

DRY MoneyRequestHeader/MoneyReportHeader and enable React Compiler#80276

Closed
roryabraham wants to merge 31 commits intomainfrom
Rory-DRYMoneyRequestView
Closed

DRY MoneyRequestHeader/MoneyReportHeader and enable React Compiler#80276
roryabraham wants to merge 31 commits intomainfrom
Rory-DRYMoneyRequestView

Conversation

@roryabraham
Copy link
Contributor

@roryabraham roryabraham commented Jan 22, 2026

Explanation of Change

This PR:

  • Makes MoneyRequestHeader, MoneyReportHeader, and SearchContext React Compiler compliant.
    • Removed all manual useMemo/useCallback from above components
    • Fixed infinite loop in SearchContext by splitting clearSelectedTransactions into two functions with appropriate dependencies
    • Fixed useEffect chain of computation anti-pattern (state → Effect chain) for export modals
  • Eliminates code duplication between MoneyRequestHeader and MoneyReportHeader
  • Leverages the newer global promise-based modal architecture to simplify and DRY modal code between these two components

Fixed Issues

(partial) #68765

Tests

Hold/Reject Functionality:

  1. Submit an expense report
  2. As an approver, click the "Hold" button on an expense
  3. Verify educational modal appears (if first time)
  4. Confirm the modal
  5. Verify expense is successfully held
  6. Click "Reject" on another expense
  7. Verify educational modal appears (if first time)
  8. Confirm and verify expense is rejected

Delete Expense:

  1. Open a money request
  2. Click "More" → "Delete"
  3. Verify confirm modal appears with proper styling
  4. Click "Delete"
  5. Verify expense is deleted

Duplicate Expense:

  1. Open a money request
  2. Click "More" → "Duplicate"
  3. Verify expense is duplicated to correct policy
  4. Verify duplicate uses correct categories and currencies

Status Bar:

  1. Put an expense on hold
  2. Verify status bar displays "Expense on hold" with stopwatch icon
  3. Mark expense as duplicate
  4. Verify status bar shows "Duplicate" with flag icon
  5. Verify status bar auto-hides when no status applies

Export Modal:

  1. In MoneyReportHeader, click export to integration on an already-exported report
  2. Verify "Export Again" confirmation modal appears
  3. Confirm and verify export succeeds
  • Verify that no errors appear in the JS console

Offline tests

Same as Tests

QA Steps

Same as Tests

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I verified there are no new alerts related to the canBeMissing param for useOnyx
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text shown in the product is localized by adding it to src/languages/* files and using the translation method
      • If any non-english text was added/modified, I used JaimeGPT to get English > Spanish translation. I then posted it in #expensify-open-source and it was approved by an internal Expensify engineer. Link to Slack message:
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
  • I verified that if a function's arguments changed that all usages have also been updated correctly
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.
  • I verified that similar component doesn't exist in the codebase
  • I verified that all props are defined accurately and each prop has a /** comment above it */
  • I verified that each file is named correctly
  • I verified that each component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
  • I verified that the only data being stored in component state is data necessary for rendering and nothing else
  • In component if we are not using the full Onyx data that we loaded, I've added the proper selector in order to ensure the component only re-renders when the data it is using changes
  • For Class Components, any internal methods passed to components event handlers are bound to this properly so there are no scoping issues (i.e. for onClick={this.submit} the method this.submit should be bound to this in the constructor)
  • I verified that component internal methods bound to this are necessary to be bound (i.e. avoid this.submit = this.submit.bind(this); if this.submit is never passed to a component event handler like onClick)
  • I verified that all JSX used for rendering exists in the render method
  • I verified that each component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

Transform MoneyRequestHeaderStatusBar from a presentational component
to a smart component that handles its own Onyx subscriptions and status
computation logic. This extracts the duplicated status bar logic from
MoneyRequestHeader and MoneyReportHeader.

Changes:
- Accept identifying props (transactionID, reportID, policyID, etc.)
- Add internal Onyx subscriptions for transaction, report, policy
- Move status computation logic from parent components
- Return null when no status bar should be shown
- Internalize getStatusIcon helper function
Add promise-based API for DecisionModal following the same pattern as
useConfirmModal. This preserves DecisionModal's distinct styling and
behavior while enabling imperative usage.

Changes:
- Create DecisionModalWrapper component to bridge DecisionModal with
  global modal system
- Create useDecisionModal hook with showDecisionModal method
- Add onModalHide prop to DecisionModal to support wrapper pattern
- Export DecisionModalActions for clarity on return values
Add promise-based wrappers for HoldSubmitterEducationalModal and
HoldOrRejectEducationalModal to integrate them with the global modal
system. These wrappers intercept callbacks to signal completion.

Changes:
- Create HoldSubmitterEducationalModalWrapper
- Create HoldOrRejectEducationalModalWrapper
- Both wrappers intercept onClose and onConfirm to resolve with
  'CONFIRMED' action
- Handles FeatureTrainingModal's internal visibility management
Add promise-based hook for educational modals with internal NVP
management. This hook encapsulates the logic for showing and
dismissing educational modals based on user state.

Changes:
- Create useHoldEducationalModal hook
- Internally subscribes to NVPs for hold/reject dismissal state
- Provides showSubmitterEducationalModal for submitters
- Provides showApproverEducationalModal for approvers
- Provides unified showEducationalModalIfNeeded method
- Automatically updates NVPs after confirmation
- Returns dismissal state for advanced use cases
Extract duplicate transaction logic into reusable hook. This hook
encapsulates the throttled button state and duplicateTransaction
callback with all necessary Onyx subscriptions.

Changes:
- Create useDuplicateExpenseAction hook
- Encapsulate useThrottledButtonState for isDuplicateActive
- Provide duplicateTransaction callback
- Handle all related Onyx subscriptions (allPolicyCategories,
  policyRecentlyUsedCurrencies, etc.)
- Maintains same behavior as inline implementation
Major refactoring to use promise-based modals and extracted hooks,
eliminating significant code duplication.

Changes:
- Replace manual status bar computation with smart
  MoneyRequestHeaderStatusBar component
- Use useDuplicateExpenseAction hook for duplicate logic
- Use useHoldEducationalModal hook for educational modals with
  promise-based pattern
- Use useConfirmModal for delete confirmation
- Remove local modal state (isDeleteModalVisible,
  isHoldEducationalModalVisible, rejectModalAction)
- Remove modal JSX from component tree
- Update HOLD and REJECT actions to use async/await with
  showEducationalModalIfNeeded
- Update DELETE action to use promise-based showConfirmModal
- Simplify imports by removing unused dependencies
Add imports for new promise-based modal hooks and duplicate expense
hook. This prepares MoneyReportHeader for similar refactoring as
MoneyRequestHeader.

Changes:
- Add useDecisionModal import
- Add useHoldEducationalModal import
- Add useDuplicateExpenseAction import
- Remove useThrottledButtonState import (will use from hook)
- Remove duplicateExpenseTransaction import (will use from hook)

Note: Full refactoring of MoneyReportHeader secondary actions and
modal state management remains to be done following the same pattern
as MoneyRequestHeader.
Refactor the modal system to support custom action types per modal
wrapper implementation, eliminating the need for type assertions.

Changes in ModalContext:
- Remove global ModalActions constant (now defined per wrapper)
- Make ModalStateChangePayload generic with string constraint
- Update ModalProps to be generic with action type parameter
- Remove ModalActions export

Changes in ConfirmModalWrapper:
- Define ConfirmModalActions locally
- Define ConfirmModalAction type
- Use typed ModalProps<ConfirmModalAction>
- Export ConfirmModalActions

Changes in DecisionModalWrapper:
- Define DecisionModalActions locally
- Define DecisionModalAction type
- Use typed ModalProps<DecisionModalAction>
- Export DecisionModalActions
- Remove isSmallScreenWidth prop (now computed internally in DecisionModal)

Changes in DecisionModal:
- Add useResponsiveLayout to compute isSmallScreenWidth internally
- Remove isSmallScreenWidth from props

Changes in hooks:
- useConfirmModal: Import and re-export ConfirmModalActions, return typed promise
- useDecisionModal: Import and re-export DecisionModalActions, return typed promise

Changes in MoneyRequestHeader:
- Import ConfirmModalActions from useConfirmModal
- Add eslint suppression for ConfirmModalActions.CONFIRM access
- Remove unused imports (useState, Transaction, useDecisionModal)

Changes in MoneyReportHeader:
- Import ConfirmModalActions from useConfirmModal
- Remove DecisionModal isSmallScreenWidth prop usages
- Add eslint suppressions for ConfirmModalActions.CONFIRM access
- Remove unused new hook imports (will be used in future refactoring)

Changes in MoneyRequestHeaderStatusBar:
- Remove useMemo wrapper from status computation
- Use lazy-loaded Expensify icons instead of namespace import
- Simplify to use separate statusIcon and statusDescription variables
Remove old callback-based educational modal pattern and use the new
useHoldEducationalModal hook for cleaner, async/await flow.

Changes:
- Remove HoldSubmitterEducationalModal and HoldOrRejectEducationalModal
  imports
- Remove ModalActions import
- Add useHoldEducationalModal hook
- Remove state variables (isHoldEducationalModalVisible, rejectModalAction)
- Remove NVP subscriptions (dismissedRejectUseExplanation,
  dismissedHoldUseExplanation)
- Update HOLD action to use showEducationalModalIfNeeded with async/await
- Update REJECT action to use showEducationalModalIfNeeded with async/await
- Remove dismissModalAndUpdateUseHold callback
- Remove dismissRejectModalBasedOnAction callback
- Remove educational modal JSX from component tree
- Remove isSmallScreenWidth prop from DecisionModal instances
Migrate MoneyReportHeader and SearchPage to use promise-based
educational modals, eliminating all remaining lint suppressions and
type errors.

Changes in MoneyReportHeader:
- Add useHoldEducationalModal hook
- Remove educational modal imports
- Remove state variables (isHoldEducationalModalVisible, rejectModalAction)
- Remove NVP subscriptions (dismissedRejectUseExplanation, dismissedHoldUseExplanation)
- Update HOLD action to use async showEducationalModalIfNeeded
- Update REJECT and REJECT_BULK actions to use async showEducationalModalIfNeeded
- Remove dismissModalAndUpdateUseHold and dismissRejectModalBasedOnAction callbacks
- Remove educational modal JSX from render
- Update MoneyRequestHeaderStatusBar usage to smart component pattern
- Remove getStatusBarProps and getStatusIcon functions (now in smart component)
- Remove unused imports (IconAsset, BrokenConnectionDescription, variables,
  dismissRejectUseExplanation, setNameValuePair, isProcessingReport,
  getArchiveReason, archiveReason, isArchivedReport)
- Remove isSmallScreenWidth props from DecisionModal instances

Changes in SearchPage:
- Add useHoldEducationalModal hook
- Remove educational modal imports and ModalActions import
- Remove state variables (isHoldEducationalModalVisible, rejectModalAction)
- Remove NVP subscriptions
- Update HOLD and REJECT bulk actions to use async showEducationalModalIfNeeded
- Remove dismiss callbacks
- Remove educational modal JSX
- Remove isSmallScreenWidth props from DecisionModal instances

Changes in educational modal wrappers:
- Add TODO comments explaining their purpose as bridge components
- Document future migration path to remove wrappers

Changes in type system:
- Remove all unsafe member access lint suppressions
- Generic types properly infer through the call chain
Changes:
- Import ConfirmModalActions from useConfirmModal
- Replace ModalActions.CONFIRM with ConfirmModalActions.CONFIRM
- Remove unused imports (setNameValuePair, dismissRejectUseExplanation)
- Remove isSmallScreenWidth (now computed inside DecisionModal)
- Remove outdated comment about isSmallScreenWidth
- Update useMemo dependency array to use showEducationalModalIfNeeded
  instead of dismissedHoldUseExplanation and dismissedRejectUseExplanation
Update educational modals to accept closeModal directly, eliminating
the wrapper layer. This completes the promise-based modal migration.

Changes:
- Update HoldSubmitterEducationalModal to accept closeModal (ModalProps)
- Update HoldOrRejectEducationalModal to accept closeModal (ModalProps)
- Both modals now handle closeModal with 'CONFIRMED' action internally
- Update useHoldEducationalModal to reference modals directly
- Delete HoldSubmitterEducationalModalWrapper.tsx (no longer needed)
- Delete HoldOrRejectEducationalModalWrapper.tsx (no longer needed)
- Fix import path in useHoldEducationalModal to use relative import

This follows the same pattern as ConfirmModal, where wrappers serve as
temporary bridges during migration. Since all usages now go through
useHoldEducationalModal, we can eliminate the wrapper layer entirely.
Simplify the hook by avoiding method destructuring and removing unused
return values.

Changes:
- Use context.showModal instead of destructuring to avoid lint error
  about unbound methods
- Remove unused isDismissedHoldExplanation and isDismissedRejectExplanation
  from return (NVP checks are handled internally)
Remove duplicate imports and use relative paths for hook imports per
lint rules.

Changes:
- Use relative imports for all hooks (useDefaultExpensePolicy, useOnyx,
  usePermissions, useThrottledButtonState)
- Remove duplicate imports that were accidentally added
Define specific action types for educational modals using ValueOf
pattern, matching the approach used in ConfirmModalWrapper and
DecisionModalWrapper.

Changes:
- Add HoldSubmitterEducationalModalActions constant with CONFIRMED action
- Define HoldSubmitterEducationalModalAction type using ValueOf
- Type HoldSubmitterEducationalModalProps with ModalProps<HoldSubmitterEducationalModalAction>
- Use constant instead of string literal in handleClose
- Export actions and action type for consistency

- Add HoldOrRejectEducationalModalActions constant with CONFIRMED action
- Define HoldOrRejectEducationalModalAction type using ValueOf
- Type HoldOrRejectEducationalModalProps with ModalProps<HoldOrRejectEducationalModalAction>
- Use constant instead of string literal in handleClose
- Export actions and action type for consistency

This ensures type safety and makes the action values explicit rather
than using generic string literals.
Since MoneyRequestHeader is compiled by React Compiler, manual
memoization is unnecessary and can be removed.

Changes:
- Remove useCallback and useMemo from imports
- Remove useCallback wrapper from markAsCash function
- Remove useMemo wrapper from primaryAction computation
- Remove useMemo wrapper from secondaryActions computation
- Convert to inline ternary expressions for cleaner code

React Compiler automatically optimizes these computations.
Replace simple acknowledgement DecisionModals with ConfirmModal using
shouldShowCancelButton: false, which is more semantically appropriate.

Changes:
- Remove DecisionModal import (not needed)
- Remove useDecisionModal import
- Remove 3 DecisionModal JSX instances from render
- Convert onExportFailed and onExportOffline callbacks to use
  showConfirmModal with shouldShowCancelButton: false
- Inline the modal calls directly where needed (download error and
  offline prompts)
- Update useMemo dependency to include showConfirmModal

These were simple acknowledgement modals with only a confirm button,
so ConfirmModal is the correct semantic choice.
Replace the single clearSelectedTransactions function with two
specialized functions to eliminate unnecessary dependencies and enable
React Compiler compatibility.

Root cause:
The old clearSelectedTransactions had searchContextData.selectedTransactions
in its dependencies, even when called with boolean parameter (which
doesn't read that value). This caused the function reference to change
whenever selections changed, creating infinite loops in useEffect.

Solution (Option 1):
Split into two functions with appropriate dependencies:
- clearAllSelectedTransactions() - stable, only depends on
  setSelectedTransactions
- clearSelectedTransactionsByHash(hash, shouldTurnOff) - has
  searchContextData dependencies for hash-based logic

Changes in SearchContext:
- Create clearAllSelectedTransactions with stable dependencies
- Create clearSelectedTransactionsByHash with searchContextData deps
- Remove old clearSelectedTransactions function
- Update context value and useMemo dependencies

Changes in types.ts:
- Replace clearSelectedTransactions overloaded type with two separate
  function signatures
- Add documentation for each function's purpose

Changes across 15 files (call site migration):
- clearSelectedTransactions(true) → clearAllSelectedTransactions()
- clearSelectedTransactions() → clearSelectedTransactionsByHash()
- clearSelectedTransactions(hash) → clearSelectedTransactionsByHash(hash)
- clearSelectedTransactions(hash, flag) → clearSelectedTransactionsByHash(hash, flag)
- Updated all destructuring from useSearchContext()
- Updated all useEffect dependency arrays

Critical fix in MoneyReportHeader:
- Removed eslint-disable comment from useEffect
- Now safely depends on stable clearAllSelectedTransactions
- No more infinite loop risk

This enables MoneyReportHeader to be compiled by React Compiler in the
future.
Remove all useMemo and useCallback wrappers from MoneyReportHeader to
make it fully React Compiler compatible.

Changes:
- Remove useMemo and useCallback from React imports
- Remove 30 useMemo/useCallback wrappers total
- Convert to inline expressions, ternaries, or direct assignments
- No IIFE patterns used - all logic is inline
- Keep useMemoizedLazyExpensifyIcons (lazy loading pattern, not manual memoization)

Conversions made:
- exportTemplates: Remove useMemo wrapper
- requestParentReportAction: Convert to ternary expression
- transactions: Inline Object.values()
- isBlockSubmitDueToStrictPolicyRules: Inline function call
- isExported: Inline function call
- integrationNameFromExportMessage: Convert to ternary
- hasOnlyPendingTransactions: Inline boolean expression
- transactionIDs: Inline map
- messagePDF: Convert to if-else statements (no IIFE)
- hasAllPendingRTERViolations: Inline function call
- getCanIOUBePaid: Remove useCallback
- showExportProgressModal: Remove useCallback
- beginExportWithTemplate: Remove useCallback
- canIOUBePaid: Inline getCanIOUBePaid() call
- onlyShowPayElsewhere: Inline conditional call
- shouldShowApproveButton: Inline boolean logic
- confirmPayment: Remove useCallback
- markAsCash: Remove useCallback
- duplicateExpenseTransaction: Remove useCallback
- primaryAction: Inline getReportPrimaryAction() call
- confirmExport: Remove useCallback
- addExpenseDropdownOptions: Inline getAddExpenseDropdownOptions() call
- exportSubmenuOptions: Remove useMemo, use direct object (no IIFE)
- secondaryActions: Remove useMemo, use conditional assignment
- secondaryExportActions: Remove useMemo, use conditional assignment
- unapproveWarningText: Remove useMemo, inline JSX
- showDeleteModal: Remove useCallback
- showExportAgainModal: Remove useCallback
- selectedTransactionsOptions: Remove useMemo, inline map call

MoneyReportHeader is now fully React Compiler compatible with zero
manual memoization.
Remove all useMemo and useCallback wrappers from MoneyReportHeader to
make it fully React Compiler compatible.

Changes:
- Remove useMemo and useCallback from React imports
- Remove 30 useMemo/useCallback wrappers total
- Convert to inline expressions, ternaries, or direct assignments
- No IIFE patterns used - all logic is inline
- Keep useMemoizedLazyExpensifyIcons (lazy loading pattern, not manual memoization)

Conversions made:
- exportTemplates: Remove useMemo wrapper
- requestParentReportAction: Convert to ternary expression
- transactions: Inline Object.values()
- isBlockSubmitDueToStrictPolicyRules: Inline function call
- isExported: Inline function call
- integrationNameFromExportMessage: Convert to ternary
- hasOnlyPendingTransactions: Inline boolean expression
- transactionIDs: Inline map
- messagePDF: Convert to if-else statements (no IIFE)
- hasAllPendingRTERViolations: Inline function call
- getCanIOUBePaid: Remove useCallback
- showExportProgressModal: Remove useCallback
- beginExportWithTemplate: Remove useCallback
- canIOUBePaid: Inline getCanIOUBePaid() call
- onlyShowPayElsewhere: Inline conditional call
- shouldShowApproveButton: Inline boolean logic
- confirmPayment: Remove useCallback
- markAsCash: Remove useCallback
- duplicateExpenseTransaction: Remove useCallback
- primaryAction: Inline getReportPrimaryAction() call
- confirmExport: Remove useCallback, add exportType parameter
- addExpenseDropdownOptions: Inline getAddExpenseDropdownOptions() call
- exportSubmenuOptions: Remove useMemo, use direct object (no IIFE)
- secondaryActions: Remove useMemo, use conditional assignment
- secondaryExportActions: Remove useMemo, use conditional assignment
- unapproveWarningText: Remove useMemo, inline JSX
- showDeleteModal: Remove useCallback, convert to async/await
- showExportAgainModal: Remove useCallback, accept exportType param,
  convert to async/await
- selectedTransactionsOptions: Remove useMemo, inline map call

Fix useEffect anti-pattern (per React docs):
- Remove state → Effect chain for export modals
- Move showExportAgainModal definition before first usage
- Remove exportModalStatus state (unused after refactoring)
- Remove useEffect that watched exportModalStatus
- Event handlers now call showExportAgainModal(exportType) directly
- showExportAgainModal passes exportType to confirmExport
- Eliminates unnecessary re-renders from state changes

MoneyReportHeader is now fully React Compiler compatible with zero
manual memoization and proper event-driven architecture.
Reduce --max-warnings from 383 to 378 (5 fewer warnings) as a result
of enabling React Compiler for MoneyRequestHeader, MoneyReportHeader,
and SearchContext.

The refactoring eliminated manual memoization that was causing React
Compiler warnings, resulting in cleaner code and fewer lint warnings
overall.
@melvin-bot
Copy link

melvin-bot bot commented Jan 22, 2026

npm has a package.json file and a package-lock.json file. It seems you updated one without the other, which is usually a sign of a mistake. If you are updating a package make sure that you update the version in package.json then run npm install

Update files that were importing ModalActions from the old location to
use ConfirmModalActions from useConfirmModal instead.

Changes:
- DomainAdminDetailsPage: Import ConfirmModalActions from useConfirmModal
- PureReportActionItem: Import ConfirmModalActions from useConfirmModal
- IOURequestStepConfirmation: Import ConfirmModalActions from useConfirmModal
- IOURequestStepWaypoint: Import ConfirmModalActions from useConfirmModal
- All files: Update ModalActions.CONFIRM → ConfirmModalActions.CONFIRM
- Combine duplicate imports into single import statements

This fixes lint errors introduced when we made modal actions generic
and moved ModalActions definitions from ModalContext to individual
modal wrappers.
- Remove CONFLICTING_RULES.md and non-compiling-files.json from tracking
- Reset Mobile-Expensify submodule to match main
@roryabraham roryabraham force-pushed the Rory-DRYMoneyRequestView branch from 6833a5a to 146e3f2 Compare January 22, 2026 23:05
@roryabraham
Copy link
Contributor Author

This PR has lots of good stuff but is too big and must be split up

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant