Skip to content

Fix console error when opening a newly created expense on the Spend page#95670

Draft
MelvinBot wants to merge 2 commits into
mainfrom
claude-fixSearchRenderPhaseSetStateWarning
Draft

Fix console error when opening a newly created expense on the Spend page#95670
MelvinBot wants to merge 2 commits into
mainfrom
claude-fixSearchRenderPhaseSetStateWarning

Conversation

@MelvinBot

@MelvinBot MelvinBot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

Fixes the dev-only console error Cannot update a component (SearchPage) while rendering a different component (Search) that appears when opening a newly created expense on the Spend page.

Search bails out to an error/empty state via conditional returns during render (searchResults === undefined, hasErrors, and the empty-state branch). Each of those calls cancelNavigationSpans(), which — before this change — also called the parent-owned onContentReady?.() (SearchPage's setIsSearchReady(true) from useSearchOverlay). Updating the parent's state during Search's render is exactly what React's warning flags.

When a just-created expense is opened before its transaction lands in the search snapshot, Search transiently takes the empty-state bail-out, so the render-phase parent update fires and the warning is logged.

The fix moves the onContentReady?.() call out of cancelNavigationSpans and into the existing no-dependency post-commit useEffect (guarded by the didBailToFallbackState ref that the bail-out paths already set). This defers the parent update to the commit phase instead of during render, matching the flush pattern already used in the same file. setIsSearchReady(true) is idempotent, so firing it on every bail-out render is safe and does not loop. Behavior is otherwise unchanged.

Fixed Issues

$ #94318
PROPOSAL: #94318 (comment)

Tests

  1. Log in to an account
  2. Open expense reports in the workspace chat
  3. On iOS, verify that the Cannot update a component (SearchPage) while rendering a different component (Search) console error does not appear
  4. On other platforms, verify that everything works as expected
  5. Open Spend > Expense and Spend > Report, then open any expense/report and verify the same as steps 2 and 3
  • Verify that no errors appear in the JS console

Offline tests

Same as Tests.

QA Steps

// TODO: The human co-author must fill out the QA tests you ran before marking this PR as "ready for review".
// Please describe what QA needs to do to validate your changes and what areas do they need to test for regressions.

  • Verify that no errors appear in the JS console

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 followed proper code patterns (see Reviewing the code)
    • 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 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)
  • 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)
  • 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.
  • 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.

Screenshots/Videos

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

Co-authored-by: DylanDylann <DylanDylann@users.noreply.github.com>
@MelvinBot MelvinBot requested a review from a team July 9, 2026 09:25
@MelvinBot

Copy link
Copy Markdown
Contributor Author

I created this PR from an upstream branch since I don't have push access to your fork.

To take ownership of this branch and be able to push updates, run:

git remote add upstream https://github.com/Expensify/App.git
git fetch upstream claude-fixSearchRenderPhaseSetStateWarning
git checkout -b claude-fixSearchRenderPhaseSetStateWarning upstream/claude-fixSearchRenderPhaseSetStateWarning
git push -u origin claude-fixSearchRenderPhaseSetStateWarning

Then you can close this PR and open a new one from your fork.

@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ Changes either increased or maintained existing code coverage, great job!

Files with missing lines Coverage Δ
src/components/Search/index.tsx 46.07% <75.00%> (+0.01%) ⬆️
... and 11 files with indirect coverage changes

@MelvinBot

Copy link
Copy Markdown
Contributor Author

DylanDylann — no code fix is needed. The only genuinely failing check is PR Author Checklist / checklist, and it's failing for a non-code reason: the PR body's author-checklist boxes are all unchecked and the Tests / QA Steps sections still contain the // TODO placeholders left for the human co-author.

Per MelvinBot policy I can't tick the author checklist or fill out the human Tests/QA sections on your behalf — so this check will stay red until you complete it. That's the "fix": edit the PR description to fill in the Tests/QA steps and check the author-checklist items.

Every code-relevant check is green. I verified the two most likely to catch this change locally against the changed file (src/components/Search/index.tsx):

  • ESLint: PASS (0 errors). The useEffect that now calls onContentReady?.() intentionally has no dependency array, so react-hooks/exhaustive-deps doesn't apply, and the cancelNavigationSpans useCallback correctly dropped onContentReady from its deps since it no longer references it.
  • typecheck: PASS (0 errors project-wide). onContentReady is an existing optional prop already invoked elsewhere in the file, so the moved call is type-safe.

All other CI jobs are passing. When I finished, ESLint check and build (Debug, true) were still queued/running (not failed) — both are expected to pass, as this is a one-line render-phase refactor with no native or lint impact.

The failing check log
You completed 0 out of 38 checklist items with 38 unfinished items
##[error]PR Author Checklist is not completely filled out. Please check every box to verify you've thought about the item.

@DylanDylann

Copy link
Copy Markdown
Contributor

@MelvinBot Ready to review

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Review

Verdict: The code change is correct, minimal, and the root-cause reasoning holds up — the render-phase → commit-phase move is the right fix for this warning. No functional regression. Two non-code items still block merge (see below); I can't complete them myself.

Why the fix is correct

The warning fires because Search updates its parent (SearchPage) during its own render. The bail-out paths — searchResults === undefined, hasErrors, and the empty-state branch — all call cancelNavigationSpans() inside conditional returns (render phase). Moving onContentReady?.() out of that callback and into the post-commit dep-free effect defers the parent update to the commit phase, which is exactly what React wants.

I verified the supporting claims:

  • Idempotency: onContentReady resolves to onSearchContentReadysetIsSearchReady(true). Setting the same true value bails out of re-render, so firing it on every bail-out render is safe and won't loop. ✅
  • Behavior equivalence: before this PR, onContentReady was already called unconditionally on every bail-out (just synchronously, in render). The dep-free effect runs after every commit and the didBailToFallbackState ref is set during the same bail-out render, so it still fires once per bail-out — just one commit later. ✅
  • Dependency change: dropping onContentReady from the cancelNavigationSpans useCallback deps is correct — it no longer references it. react-hooks/exhaustive-deps doesn't flag the effect since it intentionally has no dep array (pre-existing pattern in this file). ✅
  • Skeleton/deferred bail-outs (isDeferringHeavyWork, shouldShowRowSkeleton) don't call cancelNavigationSpans, so they don't trigger onContentReady — unchanged from before, and correct since those paths flush via their own onLayout. ✅

Blockers before this can merge

  1. PR Author Checklist is failing because the checklist boxes are all unchecked and the Tests / QA Steps sections still contain the // TODO placeholders. Per policy I can't tick the author checklist or fill the human Tests/QA sections — a human co-author must do this. Since the repro is a dev-only console error, the natural QA step is the issue's own flow: create a workspace expense → immediately open it on the Spend page → confirm no Cannot update a component (SearchPage) while rendering a different component (Search) error in the console.

  2. No regression test. This is genuinely hard to unit-test (it's a dev-mode console.error), so I'd treat it as optional rather than a hard blocker — but if feasible, a test that renders Search in the transient empty-state and asserts console.error was not called with the render-phase warning would guard against re-introduction. Worth a note in the checklist either way.

Every code-relevant check is green (ESLint + typecheck verified locally earlier). Once the checklist/Tests/QA are filled in, this looks good to go from a code standpoint.

@DylanDylann

Copy link
Copy Markdown
Contributor

@MelvinBot Mark the PR ready

@MelvinBot MelvinBot marked this pull request as ready for review July 9, 2026 09:49
@MelvinBot MelvinBot requested a review from a team as a code owner July 9, 2026 09:49
@melvin-bot melvin-bot Bot requested review from DylanDylann and heyjennahay and removed request for a team July 9, 2026 09:49
@melvin-bot

melvin-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

@DylanDylann Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot melvin-bot Bot removed the request for review from a team July 9, 2026 09:50
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Done — PR #95670 is now marked Ready for review. ✅

One heads-up: the PR Author Checklist / checklist check will stay red until the author-checklist boxes are checked and the Tests / QA Steps sections (still // TODO placeholders) are filled in. Per policy I can't tick the checklist or complete the human Tests/QA sections on your behalf, so that last step is on the human co-author before this can merge.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

The failing ESLint check is not related to this PR — it's a pre-existing eslint-seatbelt baseline drift on main, so I have not pushed a change to this branch.

Why it's unrelated: This PR only touches src/components/Search/index.tsx, which lints clean. All 31 reported errors are in two files this PR never touches:

  • src/libs/actions/Report/index.tsno-restricted-syntax (namespace imports): 10 errors, baseline allows 9
  • tests/actions/IOUTest/SplitTest.ts@typescript-eslint/no-unsafe-type-assertion: 21 errors, baseline allows 20

Both files have the exact same violation counts on main (origin/main = 7f29834) as on this branch, and the committed baseline in config/eslint/eslint.seatbelt.tsv is byte-identical between the two. In other words, main itself is currently over its lint baseline — a PR merged an extra namespace import to Report/index.ts and an extra unsafe assertion to SplitTest.ts without regenerating the seatbelt baseline, and CI enforces it repo-wide, so every open PR is failing this check, not just this one.

Merging latest main won't help — main's tip is still drifted.

Recommended fix (belongs on main, not this PR): either resolve the two actual violations, or regenerate the seatbelt baseline (npm run lint locally updates config/eslint/eslint.seatbelt.tsv to the real counts) and merge that. I deliberately did not bundle that unrelated baseline edit into this focused Search bugfix.

Evidence
# Files changed by this PR (vs origin/main)
src/components/Search/index.tsx   ← only real code change
package.json / package-lock.json / *.plist / build.gradle / Mobile-Expensify  ← version bumps from main merge

# origin/main Report/index.ts namespace imports: 10  (baseline in seatbelt.tsv: 9)
# origin/main SplitTest.ts unsafe assertions:    21  (baseline in seatbelt.tsv: 20)
# seatbelt.tsv lines 930 & 1772 identical on branch and origin/main

The other failing check, PR Author Checklist / checklist, is the standard author-checklist gate (needs a human to complete the checklist) — nothing to fix in code.

@DylanDylann

Copy link
Copy Markdown
Contributor

@MelvinBot Got a failed Eslint check

@DylanDylann

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 0aa1d5923a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@MelvinBot

Copy link
Copy Markdown
Contributor Author

The failing ESLint check is not caused by this PR — it's a broken-main issue affecting every PR built right now. No action is needed on this branch.

What's failing: the seatbelt rule in src/libs/actions/Report/index.tsno-restricted-syntax reports 10 import * as … from '@libs/…' namespace imports, but the seatbelt baseline only allows 9. Since seatbelt runs frozen in CI, actual(10) > allowed(9) is a hard failure. This PR only touches src/components/Search/index.tsx and never goes near Report/index.ts.

Root cause: a recent revert on main reintroduced the 10th namespace import in Report/index.ts without bumping the seatbelt baseline. main is currently red on ESLint, so the failure is inherited by this branch.

Already being fixed: #95671"Fix ESLint seatbelt budget violations reintroduced by revert." Once that lands on main, merge main into this branch and the ESLint check will go green.

I'm not pushing anything here: the fix belongs on main (via #95671), not smuggled into this Search PR, and editing an unrelated file wouldn't unblock everyone else.

Evidence

@DylanDylann

Copy link
Copy Markdown
Contributor

@MelvinBot Merge latest main

…rPhaseSetStateWarning

Co-authored-by: DylanDylann <DylanDylann@users.noreply.github.com>
@MelvinBot MelvinBot requested a review from a team as a code owner July 10, 2026 07:12
@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 Merged latest main into this branch and pushed the merge commit (33568f8). No conflicts — the merge was clean.

@melvin-bot melvin-bot Bot requested review from NicolasBonet and removed request for a team July 10, 2026 07:12
@melvin-bot

melvin-bot Bot commented Jul 10, 2026

Copy link
Copy Markdown

@NicolasBonet Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@DylanDylann

Copy link
Copy Markdown
Contributor

@MelvinBot I can't edit the PR description. Please update the test steps below in the test steps section

  1. Open expense reports in the workspace chat
  2. On iOS, verify that the Cannot update a component (SearchPage) while rendering a different component (Search) console error does not appear
  3. On other platforms, verify that everything works as expected
  4. Open Spend > Expense and Spend > Report, then open any expense/report and verify the same as steps 2 and 3

@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 Updated the Tests section of the PR description with your steps:

  1. Open expense reports in the workspace chat
  2. On iOS, verify that the Cannot update a component (SearchPage) while rendering a different component (Search) console error does not appear
  3. On other platforms, verify that everything works as expected
  4. Open Spend > Expense and Spend > Report, then open any expense/report and verify the same as steps 2 and 3

I left the QA Steps section as-is since your request was scoped to the test steps — let me know if you'd like me to mirror these there too.

@DylanDylann

Copy link
Copy Markdown
Contributor

@MelvinBot I can't edit the PR description. Please update the test steps below in the test steps section

  1. Log in to an account
  2. Open expense reports in the workspace chat
  3. On iOS, verify that the Cannot update a component (SearchPage) while rendering a different component (Search) console error does not appear
  4. On other platforms, verify that everything works as expected
  5. Open Spend > Expense and Spend > Report, then open any expense/report and verify the same as steps 2 and 3

@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 Updated the ### Tests section in the PR description with your steps:

  1. Log in to an account
  2. Open expense reports in the workspace chat
  3. On iOS, verify that the Cannot update a component (SearchPage) while rendering a different component (Search) console error does not appear
  4. On other platforms, verify that everything works as expected
  5. Open Spend > Expense and Spend > Report, then open any expense/report and verify the same as steps 2 and 3

The rest of the description is unchanged.

@DylanDylann

Copy link
Copy Markdown
Contributor

@MelvinBot After testing, I noticed that while the problem no longer occurs on iOS, I can still reproduce it on the Android simulator. Could you check again and propose a new fix to address this issue on both IOS and Android?

Screen.Recording.2026-07-10.at.16.13.42.mov

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Thanks for testing, DylanDylann. I re-traced the code on the current branch tip (33568f8). Heads up: the attached video couldn't be loaded on my end, and I can't drive an Android emulator here — so this is a code-level analysis, not a reproduction.

TL;DR: After the applied fix, there is no remaining render-phase update to SearchPage state anywhere in Search's render — I verified every call site. And since these files have no native variants and no Platform.OS branches, iOS and Android run byte-identical JS. The most likely reason Android still warns while iOS is clean is that the Android build you tested didn't include this fix (stale Metro bundle / not rebuilt), not a second code path.

What I checked

Every onContentReady?.() call (which is what reaches back into SearchPage via onSearchContentReadysetIsSearchReady) is now in a commit-phase construct, never during render:

The render-phase bail-outs (:928, :934, :961) now only call cancelNavigationSpans(), which just sets a ref (:760-767) — no parent state update. The other callbacks Search receives (onDestinationVisible, handleSearch, onSortPressedCallback) either set no React state or fire only from effects/user events. The warning names "SearchPage" as the updated component and "Search" as the rendering one, so it must be a synchronous parent update during Search's render — and none remains.

Please confirm before I propose more code

Could you double-check the Android build actually includes 33568f8? Concretely:

  1. Rebuild / hard-reload so Metro re-bundles this branch (kill Metro, npm start -- --reset-cache, rebuild the Android app), then re-run the exact flow.
  2. If the warning is gone → this was a stale bundle and we're done.

If it still reproduces on a confirmed-fresh build, then static analysis is missing something and I need the runtime signal. Grab the full component/owner stack React prints under the warning on Android (dev mode includes it) and paste it here. That tells us exactly which call site and which bail-out branch (undefined :926 / hasErrors :932 / empty-state :954) fires during render on Android — I couldn't rule out an Android-specific frame-timing race, but I have no evidence for a concrete render-phase trigger without that stack.

Once I have either result, reply @MelvinBot <details> and I'll take it from there.

@DylanDylann

Copy link
Copy Markdown
Contributor

@MelvinBot Tried again with the latest build and the issue still happens. I suspect it might be because the app is laggy on the Android simulator more than on iOS, which could be why the issue still occurs.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Thanks for retesting, DylanDylann. I dug into the "lag on Android" theory, and I don't think it can be the mechanism for this particular warning — which changes where I'd look next. Heads up: I can't drive an Android emulator here, so this is a code-level analysis.

TL;DR: On the current tip (33568f8) there is no synchronous render-phase update to SearchPage left anywhere in Search — I re-verified every call site. These files have no Platform.OS branches, so iOS and Android run byte-identical JS, and iOS is clean. That combination points at a stale Android JS bundle, not a second code path. Lag can't create this warning (see below), so before proposing more code I want to rule the bundle out — and if it survives a provably-clean rebuild, I need the one artifact that pinpoints the trigger.

Why lag can't be the cause

React only prints Cannot update a component (SearchPage) while rendering a different component (Search) when a SearchPage state setter runs synchronously, on the call stack, during Search's render. Effects (useEffect), layout callbacks (onLayout), and event handlers all run in the commit phase, after render — React never attributes those to "while rendering Search," no matter how slow the device. A laggy device widens timing windows; it cannot convert a commit-phase update into a render-phase one. So if the only setIsSearchReady call sites are commit-phase (they are), Android lag alone can't produce this warning from that path.

What the code shows on 33568f8

The only SearchPage-owned setter reachable from Search is setIsSearchReady, via onContentReadyonSearchContentReady. Every call to it is now commit-phase:

The render-phase bail-outs (:928, :934, :961) now only call cancelNavigationSpans(), which just sets a ref (:760-767) — no parent setState. I found no other synchronous render-phase SearchPage update in Search's subtree, and the child views (EmptySearchView, SearchRowSkeleton, error views) don't receive onContentReady/setIsSearchReady. That's fully consistent with iOS being clean.

Leading suspect: the Android bundle, not the code

HybridApp Android builds bundle NewDot JS from the Mobile-Expensify submodule, and a stale Metro cache / not-fully-rebuilt bundle is a classic way for "iOS fixed, Android still warns" to persist even after a git pull. To rule it out:

  1. Kill Metro, then npm start -- --reset-cache
  2. Rebuild the Android app from scratch (clear the installed JS bundle / reinstall), and confirm the running JS is actually from 33568f8
  3. Re-run the exact flow

If it still reproduces on a provably-clean build

Then static analysis is missing a runtime-only path and I need the decisive signal. In dev mode React prints a component/owner stack right beneath the warning — please paste that full stack (and the whyDidYouRender output for SearchPage at that moment, if you have it). That names the exact call site and which bail-out branch fires during render on Android, which is the only thing that lets me propose a correct fix instead of a guess. I asked for this last round — it really is the blocker.

Optional belt-and-suspenders

If you'd rather just neutralize the warning regardless of trigger, I can defer the readiness signal so setIsSearchReady can never run during a render — e.g. wrap onSearchContentReady to schedule via queueMicrotask / InteractionManager instead of calling setIsSearchReady synchronously. It's low-risk (the update is already idempotent and not frame-critical). I'd prefer to confirm the real trigger first rather than mask a possible build issue, but say the word and I'll implement it.


Next Steps: Reply with the clean-rebuild result + the Android warning's owner stack, or @MelvinBot implement the defensive fix to have me apply the queueMicrotask deferral.

@NicolasBonet

Copy link
Copy Markdown
Contributor

@DylanDylann is this ready for review? If not, please remove assignments and move to draft.

@DylanDylann

Copy link
Copy Markdown
Contributor

@MelvinBot Please mark this PR as draft

@MelvinBot MelvinBot marked this pull request as draft July 10, 2026 17:09
@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 Done — this PR is now marked as a draft. Mark it "Ready for review" when it's ready.

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.

3 participants