[No QA] perf(contextmenu): ContextMenu performance overhaul#83784
Draft
roryabraham wants to merge 22 commits intomainfrom
Draft
[No QA] perf(contextmenu): ContextMenu performance overhaul#83784roryabraham wants to merge 22 commits intomainfrom
roryabraham wants to merge 22 commits intomainfrom
Conversation
Replace 7 refs + 8 useState calls with a single PopoverContextMenuState object. Keep separate isPopoverVisible boolean for animation control: hideContextMenu sets isPopoverVisible=false (triggering animation), onModalHide clears menuState=null (clearing data after animation). Consolidate popoverAnchorPosition + contextMenuDimensions into PopoverPosition within the state object. Eliminate reportActionRef entirely (latent staleness bug -- only set in showDeleteModal, never in showContextMenu). Store only reportActionID in consolidated state. Derive full action from reportActions[reportActionID]. Move onEmojiPickerToggle from ref to state to avoid accessing refs during render. Remove all useCallback wrappers and inline the logic. Made-with: Cursor
…nContextMenu
Remove memo(deepEqual) wrapper and all useMemo calls (5 total).
React Compiler handles memoization automatically. Replace inline
{current: null} with stable nullRef to avoid new object creation
per render. Inline all computed values directly.
Made-with: Cursor
Extract delete confirmation flow from PopoverReportActionContextMenu into a standalone ConfirmDeleteReportActionModal component that mounts via the established global modal system (useModal/ModalProvider). The new component owns all ~16 delete-related Onyx subscriptions, which are only active when the delete modal is actually shown. This eliminates 5 duplicate subscriptions with BaseReportActionContextMenu and defers the remaining ~11 until actually needed. The promise-based API replaces 3 callback refs + 2 state vars from PopoverReportActionContextMenu. The shouldSetModalVisibility parameter is dropped as the global modal system manages visibility independently. Made-with: Cursor
Create MiniContextMenuProvider with split contexts (Actions/State) to avoid unnecessary re-renders in list items. The provider manages show/hide with 120ms delay, shouldKeepOpen/pendingHide for emoji picker flow, and stable action references via useState lazy init. Rewrite MiniReportActionContextMenu as an animated singleton rendered via createPortal to document.body for reliable position:fixed. Uses Reanimated shared values for animated row-to-row slides with overshoot easing, and CSS transitions for opacity fade. PureReportActionItem now measures its row via getBoundingClientRect on hover and calls showMiniContextMenu instead of rendering its own MiniReportActionContextMenu instance. This eliminates ~1100 Onyx subscriptions (24 per visible item). Made-with: Cursor
In mini mode, BaseReportActionContextMenu now uses the provider's keepOpen()/release() API for emoji picker and overflow menu flows. Non-mini (popover) mode retains local state. This ensures the singleton stays visible when submenus are open. Made-with: Cursor
Add a scroll event listener (capture phase) to dismiss the mini context menu when the list scrolls. The menu reappears at the correct position on the next hover via fresh getBoundingClientRect. Made-with: Cursor
Add ContextMenuPayloadProvider (shared context for all action components), ContextMenuLayout (visibility evaluation, mini-mode truncation, arrow key focus), and actionConfig (shouldShow registry with ordered action IDs). Foundation for converting the config array into individual dot-notation components. Made-with: Cursor
Replace the config array .filter().map() loop in BaseReportActionContextMenu with ContextMenuPayloadProvider, ContextMenuLayout, and individual dot-notation action components. Convert disabledActions (ContextMenuAction[]) to disabledActionIds (Set<string>) throughout the chain: PopoverReportActionContextMenu, MiniContextMenuProvider, ReportActionContextMenu, PureReportActionItem. Use Reanimated .get()/.set() API for shared values. Fix import alias in actionConfig. Made-with: Cursor
Organize component internals: hooks, derived values, callbacks, effects, render. Fix deprecated getReportNameDeprecated usage (add eslint-disable), fix no-default-id-values errors in Debug, Delete, and Explain. Use Reanimated .get()/.set() API. Fix import aliases for @pages prefix. Clean up unused imports. Reduce lint warning budget from 383 to 353 (30 warnings eliminated). Made-with: Cursor
|
npm has a |
This comment has been minimized.
This comment has been minimized.
Wire hideDeleteModal to call modalContext.closeModal() so the delete confirmation modal is dismissed when a report action item unmounts while the modal is open (e.g., navigating away). Made-with: Cursor
This comment has been minimized.
This comment has been minimized.
When the delete target is a money request, the effectiveReportID can be the IOU report ID, but the action lives in the chat report's REPORT_ACTIONS collection. Pass actionSourceReportID to the modal and fall back to it when the action isn't found in the primary collection. Made-with: Cursor
The positioning useEffect had no dependency array, causing it to run after every render. Scope it to state changes so animations only trigger when row measurements or visibility actually change. Made-with: Cursor
This comment has been minimized.
This comment has been minimized.
In mini-menu flows the popover menuState is unset, so deriving the source report from menuState?.reportID yields undefined. Pass the source report ID explicitly from Delete.tsx through the showDeleteModal interface so the modal can always resolve the action. Also guard hideDeleteModal with a ref so it only closes the modal when showDeleteModal actually opened one, preventing unrelated modals from being dismissed during navigation/unmount cleanup. Made-with: Cursor
This comment has been minimized.
This comment has been minimized.
The scroll handler repeatedly called hideMiniContextMenu() which uses
a 120ms debounced timer, causing the menu to stay visible while
scrolling. Add an {immediate: true} option to bypass the timer for
scroll-triggered hides.
Made-with: Cursor
This comment has been minimized.
This comment has been minimized.
The old code passed filteredContextMenuActions as disabledOptions when opening the overflow popover, hiding actions already visible in the mini row. Restore this behavior by passing the current visibleActionIds from ContextMenuLayout as disabledActionIds to the overflow menu. Made-with: Cursor
This comment has been minimized.
This comment has been minimized.
…ctions The old code centrally wrapped all action onPress handlers with interceptAnonymousUser in BaseReportActionContextMenu. The composition refactor moved onPress to individual components but some non-anonymous actions (EmojiReaction, MarkAsUnread, Explain, MarkAsRead, Edit, Pin, Unpin) lost this guard, allowing anonymous users to execute restricted handlers instead of being redirected to sign in. Made-with: Cursor
This comment has been minimized.
This comment has been minimized.
…enu props Made-with: Cursor
Move visibility evaluation and focus management from ContextMenuLayout into BaseReportActionContextMenu. The parent now conditionally renders each action and passes isFocused/onFocus/onBlur as explicit props. ContextMenuLayout is simplified to a pure wrapper (FocusTrap + View). Made-with: Cursor
…ContextMenu ContextMenuLayout was a thin wrapper after the previous refactor. Inline its FocusTrap, View, and styles directly into BaseReportActionContextMenu and delete the file. Made-with: Cursor
Move the disabled action IDs computation from PureReportActionItem into BaseReportActionContextMenu, which already subscribes to the report via Onyx. This eliminates an unnecessary prop threaded through 6 intermediate components (MiniContextMenuProvider, MiniReportActionContextMenu, PopoverReportActionContextMenu, ReportActionContextMenu, OverflowMenu). Also renames disabledActionIds to disabledActionIDs per code style. Made-with: Cursor
Move isChronosReport, isArchivedRoom, isPinnedChat, isUnreadChat, and isThreadReportParentAction computations from callers into BaseReportActionContextMenu, which already subscribes to the report via Onyx. This eliminates unnecessary prop threading through MiniContextMenuProvider, PopoverReportActionContextMenu, PureReportActionItem, ShowContextMenuContext, and their callers. Made-with: Cursor
Convert all 22 action components to headless hooks returning ActionDescriptor data, letting MiniReportActionContextMenu and PopoverReportActionContextMenu each render their own UI wrappers. - Extract shared Onyx subscriptions into useContextMenuData hook - Create useContextMenuActions aggregator for all action hooks - Add hideAndRun to ContextMenuPayloadContext for mode-agnostic close - Remove isMini from ContextMenuPayloadContextValue, actionConfig ShouldShowArgs, and ContextMenuItem - Delete BaseReportActionContextMenu (logic distributed to consumers) - Delete stale types.ts and ReportActionContextMenuContent.tsx Made-with: Cursor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Performance overhaul and composition refactor of the ContextMenu system. This PR:
memo,useMemo,useCallback) and fixes ref-during-render violations inPopoverReportActionContextMenuandBaseReportActionContextMenuuseRef+ 8useStatecalls with a singlePopoverContextMenuStateobject, eliminating the stalereportActionRefbugConfirmDeleteReportActionModalusing the global modal system, so its Onyx subscriptions are only active when shownreact-native-reanimatedfor smooth position animations and CSS transitions for opacityContextMenuActions.tsxwith individual dot-notation components (<ContextMenuAction.EmojiReaction>, etc.), each self-contained in its own file--max-warningsfrom 383 to 353Key architectural changes:
ContextMenuPayloadProviderprovides shared context for all action componentsContextMenuLayouthandles visibility evaluation, mini-mode truncation, and focus managementMiniContextMenuProvidermanages singleton mini-menu state with split action/state contextsactionConfig.tscentralizesshouldShowpredicates for all 23 actionsdisabledActions: ContextMenuAction[]converted todisabledActionIds: Set<string>throughoutFixed Issues
Performance optimization - no specific issue
Tests
Mini context menu (web only - singleton behavior):
Context menu actions (web - right click, mobile - long press):
Delete action (deferred modal):
Emoji reactions (mini menu):
Edit action:
Hold/Unhold (requires money request):
Overflow menu (mini mode):
Arrow key navigation (full context menu):
QA Steps
Same as tests
Offline tests
N/A - Context menus are UI-only and do not initiate network requests directly
PR Author Checklist
Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectionAvatar, I tested other components which use that component)