Skip to content

Upload instrument selection page re-renders in a loop while its query has no data #1638

Description

@joshunrau

The upload instrument selection page (/upload) re-renders in a tight loop for as long as its instrument query has no data. data ?? [] creates a new empty array on every render. useSearch runs its effect whenever that array's identity changes and calls setFilteredData with yet another new array, which schedules the next render. While the query is loading this burns CPU and logs React's "Maximum update depth exceeded" warning. If the query fails, data never arrives and the loop never stops.

Where

  • apps/web/src/routes/_app/upload/index.tsx:49-55 (no loader; a plain useQuery, so data is undefined while loading or on error):
    const { data } = useInstrumentInfoQuery({ params: { kind: 'FORM' } });
    const { filteredData, searchTerm, setSearchTerm } = useSearch(data ?? [], (instrument) => instrument.details.title);
  • apps/web/src/hooks/useSearch.ts:27-34, an effect keyed on data that always sets a new array.

Reproduce

Open /upload with the /v1/instruments/info request throttled or failing, and watch the console and CPU.

Actual: continuous re-rendering and a Maximum update depth exceeded warning. An isolated reproduction of the same hook pattern with data held undefined rendered about 10,000 times in 300 ms. On a failed request the loop never ends.
Expected: one or two renders while loading, then a render when the data arrives.

Tests

No test pins this. In apps/web/src/__tests__/upload-index.test.tsx, should list the instruments once the query data arrives, so a page opened while loading fills in drives the loading-to-loaded transition with an explicit rerender, so it passes either way. The fix should add a test that holds the query in the loading state and asserts a bounded render count (for example, that useInstrumentInfoQuery is called only a few times).

Suggested fix

Pass a stable empty array (a module-level const NO_INSTRUMENTS: UnilingualInstrumentInfo[] = []), or derive filteredData with useMemo in useSearch instead of copying it into state from an effect. The latter also protects every future caller.

Found while raising unit test coverage in #1632.

Activity

  1. added
    BugType: existing behavior is wrong
    Difficulty: LowIsolated change, about 2 hours or less
    Area: Webapps/web (clinician-facing React SPA)
    PerformanceSpeed, memory or scalability
    Good First IssueWell-scoped, low-risk and self-contained; a good entry point for a new contributor
    on Oct 5, 2026
  2. AMAN240310 commented on Oct 6, 2026

    @AMAN240310

    Hi! I'd like to work on this. My plan is to derive filteredData with useMemo in useSearch instead of syncing it into state from an effect, which fixes the loop for all callers. I'll also pass a module-level empty array from upload/index.tsx instead of data ?? [], and add a test that holds useInstrumentInfoQuery in the loading state and asserts a bounded render count. Happy to adjust if you'd prefer a different approach.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area: Webapps/web (clinician-facing React SPA)BugType: existing behavior is wrongDifficulty: LowIsolated change, about 2 hours or lessGood First IssueWell-scoped, low-risk and self-contained; a good entry point for a new contributorPerformanceSpeed, memory or scalabilityPriority: MediumReal value; schedule normally

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions