Coming from here
Background
When the app dismisses an RHP modal to show a new fullscreen destination, the modal closes first, then the destination mounts - creating a visible gap (narrow layout) or flash of the previous page (wide layout).
For the Submit Expense flow, I solved this with two strategies: pre-inserting the destination behind the RHP on narrow layout, and swapping the fullscreen before dismissing on wide layout (see before/after videos). Both are shipped and work well, but they're manually orchestrated in SubmitExpenseOrchestrator.tsx with ~40 lines of flag management, cleanup paths, and layout-specific function calls that other flows can't easily reuse.
Problem
When a user dismisses a modal and the app navigates to a different destination than what's currently behind it, there is a visible gap (narrow layout) or flash of the previous page (wide layout) between the modal closing and the destination appearing - because the destination component tree hasn't mounted yet at the time of dismissal. This affects any modal-to-destination flow where the destination differs from what was previously behind the modal (e.g., workspace creation, which currently has this gap/flash).
Solution
A usePreMountDestination(route) hook that encapsulates the entire pre-mount lifecycle behind a one-line setup:
const {reveal} = usePreMountDestination(ROUTES.SEARCH_CENTRAL_PANE);
const handleSubmit = () => {
reveal(() => {
// expensive work runs after the user already sees the destination
createExpense(params);
});
};
The hook has two internal strategies based on layout:
Narrow layout (pre-insert on mount):
- On mount, schedules preInsertFullscreenUnderRHP(route) after a 300ms delay (configurable via {delay: ms}). This gives the confirmation screen time to render before the background insert starts.
- Emits SET_RHP_SLIDE_LEFT so the RHP dismiss animation slides forward (reveals the destination underneath) instead of the default slide-right.
- On reveal(callback): clears the pre-insert flag and calls dismissModal({afterTransition: callback}).
- On unmount without submit (user backed out): calls removePreInsertedFullscreenIfNeeded() and emits RESTORE_RHP_ANIMATION to reset the dismiss direction.
Wide layout (reveal at call time):
- On mount: no-op (no pre-insertion needed - web rendering is fast enough).
- On reveal(callback): calls revealRouteBeforeDismissingModal(route, {afterTransition: callback}) which swaps the fullscreen in frame 1 and dismisses the modal in frame 2.
- On unmount without reveal being called: no-op (nothing was pre-inserted).
What this changes from today
- Encapsulation - The pre-insert lifecycle (flag management, cleanup, animation events) moves from consumer code into the hook. Consumers just call usePreMountDestination(route) and reveal().
- Automatic back-out cleanup - Currently manual; the hook handles it via useEffect cleanup + an internal ref tracking whether reveal() was called.
Parameters
- route (type: Route) - required; The destination to pre-mount
- delay (type: number) - optional, default: CONST.PRE_INSERT_FULLSCREEN_DELAY (300ms); How long to wait before pre-inserting on narrow layout. Shorter for screens where the user spends less time, longer for heavier destinations.
Migration plan
- Ship the hook.
- Migrate Submit Expense flow (SubmitExpenseOrchestrator.tsx) to use the hook instead of manual orchestration - this validates it works end-to-end.
- Create a rule (ESLint or AI) that will enforce only using this hook in the narrow usecase it is designed for
- First new adoption: workspace creation -> workspace overview (follow-up issue).
When NOT to use this hook
- Destination isn't known in advance - route must be fixed at mount time.
- No dwell time before dismiss (narrow layout only) - the user must spend at least 300ms on the screen before the RHP is dismissed, so the pre-insert timer can fire.
- No modal to dismiss - this is for RHP/modal-to-destination flows only.
- Transition is already fast enough - profile first, don't add complexity if there's no perceived gap.
Alternative solutions explored
- Pre-inserting on both layouts - unnecessary on wide (web renders fast enough); would risk browser-resize edge case for zero benefit.
- Platform-split (native/web) instead of layout-split (narrow/wide) - Tim's feedback: keep narrow/wide as the boundary, simpler and cross-platform.
- No hook, keep manual orchestration - leads to duplicated ~40-line patterns and leaked flags. The hook eliminates this class of bugs by design.
Flows that meet all requirements (RHP dismissed to reveal a fullscreen destination, destination known at mount, dwell time before dismiss):
- Workspace Creation → Workspace Overview - already partially uses the pattern manually (pushNewlyCreatedWorkspaceUnderActiveModal); first non-IOU adoption.
- Approval Workflow Create/Edit - dismisses modal + createApprovalWorkflow/updateApprovalWorkflow in afterTransition.
- Copy Policy Settings - confirmation screen + copyPolicySettings across multiple policies.
- Company Card Assignment - multi-step flow, confirmation step, assigns card via API write.
- Search Advanced Filters → Search Results - filter form dismissed, triggers full search list re-render.
- Workspace Invite → Members Page - multi-step invite, navigates to members list on confirm.
Full technical reference: CANVAS
Note to original participants
The afterTransition error isolation fix (wrapping the synchronous callback path in try/catch in TransitionTracker.ts) will be shipped as a separate small PR in parallel - it's a one-line defensive fix that doesn't need its own proposal. The remaining items from the original thread (deferred write scheduling, progressive rendering hook renames) will be posted as separate proposals to allow independent discussion and progress on each.
Issue Owner
Current Issue Owner: @JakubKorytko
Coming from here
Background
When the app dismisses an RHP modal to show a new fullscreen destination, the modal closes first, then the destination mounts - creating a visible gap (narrow layout) or flash of the previous page (wide layout).
For the Submit Expense flow, I solved this with two strategies: pre-inserting the destination behind the RHP on narrow layout, and swapping the fullscreen before dismissing on wide layout (see before/after videos). Both are shipped and work well, but they're manually orchestrated in SubmitExpenseOrchestrator.tsx with ~40 lines of flag management, cleanup paths, and layout-specific function calls that other flows can't easily reuse.
Problem
When a user dismisses a modal and the app navigates to a different destination than what's currently behind it, there is a visible gap (narrow layout) or flash of the previous page (wide layout) between the modal closing and the destination appearing - because the destination component tree hasn't mounted yet at the time of dismissal. This affects any modal-to-destination flow where the destination differs from what was previously behind the modal (e.g., workspace creation, which currently has this gap/flash).
Solution
A usePreMountDestination(route) hook that encapsulates the entire pre-mount lifecycle behind a one-line setup:
The hook has two internal strategies based on layout:
Narrow layout (pre-insert on mount):
Wide layout (reveal at call time):
What this changes from today
Parameters
Migration plan
When NOT to use this hook
Alternative solutions explored
Flows that meet all requirements (RHP dismissed to reveal a fullscreen destination, destination known at mount, dwell time before dismiss):
Full technical reference: CANVAS
Note to original participants
The afterTransition error isolation fix (wrapping the synchronous callback path in try/catch in TransitionTracker.ts) will be shipped as a separate small PR in parallel - it's a one-line defensive fix that doesn't need its own proposal. The remaining items from the original thread (deferred write scheduling, progressive rendering hook renames) will be posted as separate proposals to allow independent discussion and progress on each.
Issue Owner
Current Issue Owner: @JakubKorytko