Skip to content

Virtualizer's CollectionRendererContext leaks into overlay content, crashing nested collections #10500

Description

@coolassassin

Provide a general summary of the issue here

When a collection is virtualized with <Virtualizer> (e.g. a virtualized Table), the Virtualizer publishes a CollectionRendererContext ({ isVirtualized: true, CollectionRoot, layout, dropTargetDelegate }) that wraps its entire React subtree. Any other collection component (Menu, ListBox, Select, …) rendered inside an overlay (Popover / Dialog) that is a React descendant of the virtualized collection inherits that context — React context propagates through portals — and renders its own collection with the ancestor's CollectionRoot / layout.

If the ancestor layout is a TableLayout (virtualized Table), the nested overlay collection (a Menu/ListBox collection, which has no columns) is laid out with TableLayout, and TableLayout.buildCollection() throws:

TypeError: collection.columns is not iterable

In practice: a virtualized Table whose column headers contain menus/selects/filter popovers crashes the whole table (via the error boundary) the moment any of those popovers is opened.

🤔 Expected Behavior?

Overlay content (Popover / Dialog / Modal) should not inherit a virtualized ancestor's CollectionRendererContext. A Menu / ListBox / Select opened from inside a virtualized Table (or any virtualized collection) should render its own collection with the default renderer (or with its own Virtualizer if one is explicitly nested), not with the ancestor collection's layout. Opening the popover should just work.

😯 Current Behavior

Opening the overlay throws and unmounts the tree:

TypeError: collection.columns is not iterable
    at TableLayout.buildCollection (TableLayout.ts:169:35)
    at TableLayout.update (ListLayout.ts:359:27)   // super.update
    at TableLayout.update (TableLayout.ts:162:11)
    at Virtualizer.relayout (Virtualizer.ts:194:17)
    at Virtualizer.render (Virtualizer.ts:419:12)
    at useVirtualizerState (useVirtualizerState.ts:91:34)
    at CollectionRoot (Virtualizer.tsx:108:15)

Root cause chain:

  1. Virtualizer (Virtualizer.tsx) provides CollectionRendererContext with { isVirtualized: true, CollectionRoot, layout, dropTargetDelegate } around its children.
  2. Menu (Menu.tsx, let { isVirtualized, CollectionRoot } = useContext(CollectionRendererContext)) and ListBox (ListBox.tsx, reads isVirtualized / CollectionRoot / layoutDelegate from the same context) render their collection through the inherited CollectionRoot. Neither resets the context, and Popover/Dialog do not reset it either.
  3. Because the popover content is still a React descendant of the Virtualizer (portals preserve React context), the menu/listbox is laid out with the ancestor's TableLayout.
  4. TableLayout.buildCollection() does for (let column of collection.columns), but the overlay's collection is a Menu/ListBox collection (not a TableCollection), so collection.columns is undefined → not iterable.

Note: a real (even empty) TableCollection always exposes columns (columns: GridNode<T>[] = []), so this only happens because a non-table collection reaches the table layout.

💁 Possible Solution

The overlay boundary should reset the collection renderer so nested collections don't inherit an unrelated virtualized ancestor's renderer. Options:

  • Have Popover / Dialog / Modal (or the overlay Provider inside them) reset CollectionRendererContext to DefaultCollectionRenderer for their children, or
  • Have MenuTrigger / Select / ComboBox reset it around the Popover + ListBox/Menu they render.

Consumer-side workaround (works today, since both symbols are publicly exported from react-aria-components):

import { CollectionRendererContext, DefaultCollectionRenderer } from 'react-aria-components'

<Popover>
  <CollectionRendererContext.Provider value={DefaultCollectionRenderer}>
    {/* Menu / ListBox / Select content */}
  </CollectionRendererContext.Provider>
</Popover>

This is a no-op outside a virtualizer (the context is already DefaultCollectionRenderer), and a nested explicit <Virtualizer> inside the popover still overrides it, so no legitimate case is broken.

🔦 Context

We virtualize a data grid built on Table using Virtualizer + a custom TableLayout subclass. Column header cells contain menus (sort/reorder/resize actions), a column-select menu, and filter popovers. After migrating the grid to Virtualizer, opening any header popover crashes the entire grid through the error boundary — it makes header menus/selects/filters unusable in a virtualized table. This is a trading application where the grid is central, so the whole feature is blocked without the workaround above.

🖥️ Steps to Reproduce

Minimal reproduction (a virtualized Table whose column header contains a MenuTriggerPopoverMenu):

import {
  Virtualizer,
  TableLayout,
  Table,
  TableHeader,
  TableBody,
  Column,
  Row,
  Cell,
  MenuTrigger,
  Button,
  Popover,
  Menu,
  MenuItem,
} from 'react-aria-components'

function Example() {
  return (
    <div style={{ height: 200 }}>
      <Virtualizer layout={TableLayout} layoutOptions={{ rowHeight: 32 }}>
        <Table aria-label="Example">
          <TableHeader>
            <Column isRowHeader>
              Name
              <MenuTrigger>
                <Button aria-label="Menu"></Button>
                <Popover>
                  <Menu>
                    <MenuItem>Sort ascending</MenuItem>
                    <MenuItem>Sort descending</MenuItem>
                  </Menu>
                </Popover>
              </MenuTrigger>
            </Column>
          </TableHeader>
          <TableBody>
            <Row>
              <Cell>Alpha</Cell>
            </Row>
            <Row>
              <Cell>Bravo</Cell>
            </Row>
          </TableBody>
        </Table>
      </Virtualizer>
    </div>
  )
}

Steps:

  1. Render the virtualized table above — it renders fine.
  2. Click the menu button in the column header to open the Popover + Menu.
  3. The app throws TypeError: collection.columns is not iterable and the tree unmounts (error boundary).

The same crash occurs with Select/ComboBox/ListBox inside a Popover in the header, and generally with any Menu/ListBox/Select rendered in an overlay under a virtualized Table.

(A live StackBlitz/CodeSandbox based on the snippet above can be added.)

Version

1.20.0

What browsers are you seeing the problem on?

Chrome

If other, please specify.

No response

What operating system are you using?

MacOS

🧢 Your Company/Team

No response

🕷 Tracking Issue

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions