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.
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.useSearchruns its effect whenever that array's identity changes and callssetFilteredDatawith 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,datanever arrives and the loop never stops.Where
apps/web/src/routes/_app/upload/index.tsx:49-55(no loader; a plainuseQuery, sodataisundefinedwhile loading or on error):apps/web/src/hooks/useSearch.ts:27-34, an effect keyed ondatathat always sets a new array.Reproduce
Open
/uploadwith the/v1/instruments/inforequest throttled or failing, and watch the console and CPU.Actual: continuous re-rendering and a
Maximum update depth exceededwarning. An isolated reproduction of the same hook pattern withdataheldundefinedrendered 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 indrives the loading-to-loaded transition with an explicitrerender, 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, thatuseInstrumentInfoQueryis called only a few times).Suggested fix
Pass a stable empty array (a module-level
const NO_INSTRUMENTS: UnilingualInstrumentInfo[] = []), or derivefilteredDatawithuseMemoinuseSearchinstead of copying it into state from an effect. The latter also protects every future caller.Found while raising unit test coverage in #1632.