react-spectrum: Passing nested row into table causes getCollectionNode error

🐛 Bug Report

I have a table where I want to render a number of <Skeleton /> components that would contain a Row with five Cells both imported from the react-stately package. The Skeleton animation component is dispalyed while the table data waits to be retrieved from the API. Obviously I would like to keep this in a separate file from the actual table file.

🤔 Expected Behavior

The rows should render normally while data from the API loads.

😯 Current Behavior

This error is returned.

Call Stack
 $f8429209754fda4b9142d514065f4$export$CollectionBuilder.getFullNode$
  app.2b21bd4ce9b6247290f9.bundle.js:31712:853
 l
  app.2b21bd4ce9b6247290f9.bundle.js:33546:5798
 Generator._invoke
  app.2b21bd4ce9b6247290f9.bundle.js:33546:5582
 Generator.next
  app.2b21bd4ce9b6247290f9.bundle.js:33546:6224
 Object.n
  app.2b21bd4ce9b6247290f9.bundle.js:20551:4865
 _callee13$
  app.2b21bd4ce9b6247290f9.bundle.js:31717:269
 l
  app.2b21bd4ce9b6247290f9.bundle.js:33546:5798
 Generator._invoke
  app.2b21bd4ce9b6247290f9.bundle.js:33546:5582
 Generator.next
  app.2b21bd4ce9b6247290f9.bundle.js:33546:6224
 Object.n
  app.2b21bd4ce9b6247290f9.bundle.js:20551:4865

💁 Possible Solution

🔦 Context

If I render the Skeleton Row directly into the table component without passing it from the Skeleton file it works just fine. What I can tell is that the map function from the table component doesn’t detect the nested components and throws this error.

💻 Code Sample

Table component

<Table scrollRef={ref} >
                        <Table.Head>
                            <Table.Heading align="center" className="border-r border-gray-300">
                               Head 1
                            </Table.Heading>
                            <Table.Heading align="center" className="border-r border-gray-300">
                                Head 2
                            </Table.Heading>
                            <Table.Heading align="center" className="border-r border-gray-300">
                                Head 3
                            </Table.Heading>
                            <Table.Heading align="center" className="border-r border-gray-300">
                                Head 4
                            </Table.Heading>
                            <Table.Heading align="center">
                                Head 5
                            </Table.Heading>
                        </Table.Head>
                    <Table.Body>
                            networkStatus
                                && (new Array(50)
                                    .fill({})).map((item, index) => (
                                    /* eslint-disable-next-line react/no-array-index-key */
                                        <Skeleton key={index} />
                                ))
              </Table.Body>
</Table>

Skeleton component

import React from "react";

import {
    PageSkeleton, Table,
} from "components";

function Skeleton() {
    return (
        <Table.Row>
            {(new Array(5).fill({})).map((cell, i) => (
              <Table.Cell key={i} className="border-r border-gray-300">
                  <PageSkeleton.Text />
              </Table.Cell>
              ))}
        </Table.Row>
    );
}

Working verison

<Table scrollRef={ref} >
                        <Table.Head>
                            <Table.Heading align="center" className="border-r border-gray-300">
                               Head 1
                            </Table.Heading>
                            <Table.Heading align="center" className="border-r border-gray-300">
                                Head 2
                            </Table.Heading>
                            <Table.Heading align="center" className="border-r border-gray-300">
                                Head 3
                            </Table.Heading>
                            <Table.Heading align="center" className="border-r border-gray-300">
                                Head 4
                            </Table.Heading>
                            <Table.Heading align="center">
                                Head 5
                            </Table.Heading>
                        </Table.Head>
                    <Table.Body>
                            networkStatus
                                && (new Array(50)
                                    .fill({})).map((item, index) => (
                                    /* eslint-disable-next-line react/no-array-index-key */
                                        <Table.Row>
                                          {(new Array(5).fill({})).map((cell, i) => (
                                            <Table.Cell key={i} className="border-r border-gray-300">
                                                <PageSkeleton.Text />
                                            </Table.Cell>
                                          ))}
                                        </Table.Row>
                                 ))
                       </Table.Body>
</Table>

About this issue

  • Original URL
  • State: open
  • Created 2 years ago
  • Comments: 22 (11 by maintainers)

Most upvoted comments

As mentioned above, collections don’t currently support wrapping Item.

However, we’ve been experimenting with a way to allow this, you can try it out in our React Aria Components. See https://react-spectrum.adobe.com/react-aria/Select.html#reusable-wrappers Eventually, we’ll have this in all the docs and components, but for now it’s just React Aria Components.

I ran into the same problem with useSelect,<Item /> and the getCollectionNode error when I was modifying this tailwind example.

Instead of including Item in my custom DropdownItem component, I ended up getting it to work like this:

import type { DropdownProps } from './types';
import { Item, Select } from './Select';
import { DropdownItem } from './DropdownItem';

export const Dropdown = ({ items }: DropdownProps) => {
    return (
        <Select
            label='Favorite Animal'
            defaultSelectedKey={items[0].id}
            onSelectionChange={(key) => {
                console.log({ key });
            }}
        >
            {items.map((item) => (
                <Item key={item.id} textValue={item.title}>
                    <DropdownItem item={item} />
                </Item>
            ))}
        </Select>
    );
};

Hmmm I’m not sure why that is causing a problem specifically. You could check if the function, getCollectionNode, defined on Item gets carried over with the assign. It should be, but maybe some other piece is wrong due to the assign.

It’s not that it’s a problematic approach, so much as a path we haven’t tested. It’s fine to use React Aria hooks, they don’t reference the new collection work yet is all. That work is in the React Aria Components, and it’s currently an alpha while we make sure it’s in a stable state with an API we like. Eventually, the hooks docs will reference the new work.

Thanks for the info @snowystinger.

In my case it’s not a wrapper, but simply a re-export of Item from react-stately for convenience:

import { Item } from 'react-stately';

import Select from './Select'; // my custom Select

export default Object.assign(Select, { Item });

This way users can just import the custom Select and use Select.Item instead of having to import Item separately.

Are you suggesting that this is a problematic approach? I’m only using React Aria hooks, haven’t converted to Components yet.

Just an update, but @devongovett is currently experimenting with changes to collections as part of https://github.com/adobe/react-spectrum/discussions/2368 that will allow for custom wrappers around things like Item.