nextui icon indicating copy to clipboard operation
nextui copied to clipboard

[BUG] - When using Table, "type.getCollectionNode is not a function" error

Open Clovel opened this issue 3 years ago • 43 comments

Describe the bug

When using the Table components, I get an error in the console from inside NextUI's code :

image

I tried using the Table component two different ways :

  • First, by using the render function as indicated by the documentation :
<Table.Header columns={counterTableColumns}>
  {
    (pColumn: CounterTableColumn) => {
      return (
        <Table.Column
          key={pColumn.uid}
          hideHeader={pColumn.hideHeader}
          align={pColumn.align ?? 'start'}
        >
          {pColumn.name}
        </Table.Column>
      );
    }
  }
</Table.Header>
  • Then by using the Array.map method :
<Table.Header>
  {
    counterTableColumns.map(
      (pColumn: CounterTableColumn) => {
        return (
          <Table.Column
            key={pColumn.uid}
            hideHeader={pColumn.hideHeader}
            align={pColumn.align ?? 'start'}
          >
            {pColumn.name}
          </Table.Column>
        );
      },
    )
  }
</Table.Header>

In both cases, no ESLint or TypeScript errors are displayed in my IDE nor in my terminal.

These test were also made with the Table.Body section of the table.

I am using :

  • Windows 10
  • "@nextui-org/react": "^1.0.0-beta.10"
  • "react": "^18.2.0"
  • "react-scripts": "5.0.1"
  • "typescript": "4.4.2"

Your Example Website or App

No response

Steps to Reproduce the Bug or Issue

CountersList.tsx :

/* Type declarations ----------------------------------- */
interface CounterTableColumn {
  name: string;
  uid: string;
  hideHeader?: boolean;
  align?: 'center' | 'start' | 'end';
}

/* Internal variables & constants ---------------------- */
const counterTableColumns: CounterTableColumn[] = [
  { name: 'Nom', uid: 'name' },
  { name: 'Description', uid: 'role' },
  { name: 'Actions', uid: 'actions', hideHeader: true },
];

const userCounters: CounterLightFragment[] = [
  {
    __typename: 'Counter',
    id: '1',
    name: 'Test1',
    description: 'Une description',
    avatarHash: null,
    avatarContentType: null,
  },
];

/* CountersList component prop types ------------------- */
interface CountersListProps {}

/* CountersList component ------------------------------ */
const CountersList: React.FC<CountersListProps> = () => {
  const userID = useReactiveVar(userIDReactiveVar);

  // const {
  //   data: userData,
  //   loading: loadingUserData,
  //   error: errorUserData,
  // } = useGetUserQuery(
  //   {
  //     variables: {
  //       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  //       id: userID!,
  //     },
  //     skip: userID === null,
  //   },
  // );

  // const userCounters: CounterLightFragment[] = userData?.getUser?.counters?.map(
  //   (pVal: UserCounterOwnershipFragment) => {
  //     return pVal.counter;
  //   },
  // ) ?? [];

  console.log(`[DEBUG] <CountersList> userCounters :`, userCounters);

  return (
    <Table bordered>
      <Table.Header>
        {
          counterTableColumns.map(
            (pColumn: CounterTableColumn) => {
              return (
                <Table.Column
                  key={pColumn.uid}
                  hideHeader={pColumn.hideHeader}
                  align={pColumn.align ?? 'start'}
                >
                  {pColumn.name}
                </Table.Column>
              );
            },
          )
        }
      </Table.Header>
      {/* <Table.Header columns={counterTableColumns}>
        {
          (pColumn: CounterTableColumn) => {
            return (
              <Table.Column
                key={pColumn.uid}
                hideHeader={pColumn.hideHeader}
                align={pColumn.align ?? 'start'}
              >
                {pColumn.name}
              </Table.Column>
            );
          }
        }
      </Table.Header> */}
      <Table.Body>
        {
          userCounters.map(
            (pCounter: CounterLightFragment) => {
              return (
                <CountersListItem
                  key={`${pCounter.id}-${pCounter.name}`}
                  counter={pCounter}
                />
              );
            },
          )
        }
      </Table.Body>
      {/* <Table.Body items={userCounters}>
        {
          (pCounter: CounterLightFragment) => {
            return (
              <CountersListItem
                key={`${pCounter.id}-${pCounter.name}`}
                counter={pCounter}
              />
            );
          }
        }
      </Table.Body> */}
      <Table.Pagination
        shadow
        noMargin
        align="center"
        rowsPerPage={10}
        onPageChange={(page) => console.log({ page })}
      />
    </Table>
  );
};

/* Export CountersList component ----------------------- */
export default CountersList;

CountersListItem.tsx :

/* CountersListItem component prop types --------------- */
interface CountersListItemProps {
  counter: CounterLightFragment;
}

/* CountersListItem component -------------------------- */
const CountersListItem: React.FC<CountersListItemProps> = ({ counter }) => {
  return (
    <Table.Row key={counter.id}>
      <Table.Cell>
        <User
          src={counter.avatarHash ?? undefined}
          name={counter.name}
        >
          {counter.description}
        </User>
      </Table.Cell>
      <Table.Cell>
        {JSON.stringify(counter)}
      </Table.Cell>
      <Table.Cell>
        Actions
      </Table.Cell>
    </Table.Row>
  );
};

/* Export CountersListItem component ------------------- */
export default CountersListItem;

Expected behavior

Render the table correctly

Screenshots or Videos

image

Operating System Version

Windows 10

Browser

Chrome

Clovel avatar Sep 02 '22 14:09 Clovel

Hey @Clovel could you try again using react:17?, React 18 is still not fully supported by react-aria which is the library used by NextUI under the hood for creating tables, could you create a codesanbox/repository with the issue?

jrgarciadev avatar Sep 06 '22 02:09 jrgarciadev

Using React & React DOM 17 completely broke my application.

Most errors come from ReactNode types being incompatible with React.ReactNode types, etc. Most likely I have a bunch of incompatible dependencies now.

for example, I have MUI v5 in my project alongside NextUI. MUI v5.10 required a version of @types/react-transition-group that requires React 18.

Type 'React.ReactNode' is not assignable to type 'import("C:/Users/<myUser>/repository/<project>/<project>-frontend/node_modules/@types/react-transition-group/node_modules/@types/react/index").ReactNode'.

Also, it seems that React 18 is listed as a peer dependency for the react-aria project you linked :

  "peerDependencies": {
    "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0",
    "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
  },

I tried reproducing the error in a sandbox, but it's not the same error... Anyway : https://codesandbox.io/s/nextui-react-18-error-tes3wg

Clovel avatar Sep 06 '22 10:09 Clovel

EDIT : I had a rogue component in my table, the issue came back. I removed it and now the error is reproducible in the CodeSandbox.

Clovel avatar Sep 09 '22 11:09 Clovel

As mentioned in https://github.com/adobe/react-spectrum/issues/3502#issuecomment-1242241141, this error is due to using CountersListItem as children of Table.Body, when it's expecting Table.Row as children.

reidbarber avatar Sep 09 '22 19:09 reidbarber

As mentioned in adobe/react-spectrum#3502 (comment), this error is due to using CountersListItem as children of Table.Body, when it's expecting Table.Row as children.

Even if the CountersListItem has Table.Row as it's main component ?

Clovel avatar Sep 10 '22 11:09 Clovel

Even if the CountersListItem has Table.Row as it's main component ?

Correct. Refactoring around that should work

reidbarber avatar Sep 10 '22 17:09 reidbarber

Hey @Clovel as @devongovett (react-aria lead maintainer) mentioned here, The Table.Body component only supports Table.Row elements as children

jrgarciadev avatar Sep 19 '22 01:09 jrgarciadev

Any reason for this limitation? I don't see a reason why we couldn't just pass the TableRow a custom as prop or something like that. Main reason for this is you cannot integrate NextUI tables with any other library that has container elements. For example: react-beautiful-dnd basically works out of the box with every UI library I've tried before aside from NextUI

Also curious how the builder works, if it looks for a tr element within the table body and then find the cells in that, what does allowing container elements do that's so bad? I think all that would need to happen in that case is changing how the builder selects the elements, instead of looking for direct child elements, select all the tr elements only for example

TylerNRobertson avatar Sep 08 '23 15:09 TylerNRobertson

This behavior is non sense.

We can not do something like this, which would make sense if you need to do some processing per row, or event if you just want to encapsulate the row logic...

 const MyRow = ({item, ...tableRowProps} : {item: FooItemType}) => {
    const status = useFetchItemStatus(item)
    return (
      <TableRow {...tableRowProps} key={`example-${item.id}`}>
          <TableCell>{item.foo}</TableCell>
          <TableCell>{status || "loading satus"}</TableCell>
      </TableRow>
    )
 }

.... 
// Then in the parent component you would do
...

<Table aria-label="Example table">
    <TableHeader>
        <TableColumn>Col A</TableColumn>
        <TableColumn>Col B</TableColumn>
    </TableHeader>
    <TableBody>
        {data.map(item => (
            <MyRow item={item} />
        ))}
    </TableBody>
</Table>

So honestly I don't understand the logic of not allowing this, this issue should remain opened imo since no solution was provided.

defguru avatar Nov 09 '23 16:11 defguru

It's not a limitation we wanted, just a technical trade off required by internal implementation details. React actually makes it extremely difficult to build components that are aware of their children like Table needs to be in order to implement things like keyboard navigation.

That said, in React Aria Components, our new component library built on top of React Aria hooks, we found a new way of implementing collection components that lifts this restriction. The goal is to extract this new implementation out once it is stable and offer it to React Aria hooks users such as Next UI as well. So hopefully in the near future Next UI will be able to lift the restriction as well.

In the meantime, we've recommended moving the logic to a component inside the item rather than a wrapper. You could refactor your example like the following:

<TableBody>
  {data.map(item => (
    <TableRow key={`example-${item.id}`}>
      <TableCell>{item.foo}</TableCell>
      <TableCell><ItemStatus item={item} /></TableCell>
    </TableRow>
  ))}
</TableBody>

function ItemStatus({item}) {
  const status = useFetchItemStatus(item)
  return status || 'loading status';
}

devongovett avatar Nov 09 '23 19:11 devongovett

How I can add TableBody to <ReactSortable list={(column?.tasks || []).map((task) => ({ ...task, chosen: true, }))} setList={(newTasks) => { setColumns((prevColumns) => prevColumns.map((prevColumn) => prevColumn.id === column.id ? { ...prevColumn, tasks: newTasks } : prevColumn ) ); }} group="tasks" animation={150} tag={TableBody} >

dailytravel avatar Nov 21 '23 07:11 dailytravel

Have the same issue when trying to use DnD to reorder tabs. I'm assuming this is the same reliance on React Aria Components causing the underlying issue. Look forward to the updates.

spot-developers avatar Dec 12 '23 06:12 spot-developers

What makes this quite cumbersome is that say if I have AccordionItems that do not render due to some logic inside them, now all that logic needs to be pulled up into the parent component so the AccordionItems can be conditionally removed. It basically promotes leaky abstractions. 😞

I did try to just assign getCollectionNode to my custom components. It does stop complaining but it doesn't actually render the custom component properly.

sohrab- avatar Dec 24 '23 10:12 sohrab-

So is there currently no way to breakout NextUI Table components into separate components? If so that is pretty disappointing considering the benefits you can gain from splitting things into server and client components.

rydonahue1 avatar Jan 28 '24 19:01 rydonahue1

Same issue with the <Listbox> component... Considering the entire point of React is to breakdown complex UI into smaller components, this is a bit of a weird and unexpected behaviour.

lazharichir avatar Feb 23 '24 14:02 lazharichir

@lazharichir MenuItem also

divyam234 avatar Feb 23 '24 17:02 divyam234

Yeah tried using the table again and got this same error. Unfortunately, it makes this table component fairly useless. Especially if you need to manage state for individual rows. There are refactoring work arounds but most of them are anti-patterns for react. Love this library otherwise.

christo9090 avatar Mar 17 '24 16:03 christo9090

Ran into this same issue, had to workaround using the nuqs library to re-render the table on query parameters update. Hope we can get a proper way to implement logic/state per row soon!

Jaimayal avatar Mar 20 '24 20:03 Jaimayal

also got this issue with: DropdownItem

viinaji avatar Mar 23 '24 15:03 viinaji

@devongovett @jrgarciadev How do you suggest using something like <InfiniteScroll /> from react-infinite-scroller with an array of AccordionItems?

ameytessact avatar Apr 02 '24 07:04 ameytessact

@viinaji I have the same problem with DropdownItem, were you able to solve it?

alvaroaxo avatar Apr 10 '24 23:04 alvaroaxo

@alvaroaxo My goal was to separate each item in the menu, However, I encountered that error when using the component. To address this, I turned to the collections feature https://react-spectrum.adobe.com/react-stately/collections.html and the following is how I implemented it:

<DropdownMenu aria-label="Card Menu" items={CardMenuSections()}>
  {section => (
    <DropdownSection {...section}>
      {/* @ts-ignore:disable-next-line */}
      {item => <DropdownItem {...item} />}
    </DropdownSection>
  )}
</DropdownMenu>
type DropDownSectionType = {
  key: string;
  title?: string;
  showDivider?: boolean;
  items: (DropdownItemProps | undefined)[];
};
const CardMenuSections = (): DropDownSectionType[] => {
  return [
    {
      key: 'options',
      title: 'Options',
      showDivider: true,
      items: [LaunchConfig(), Extensions(), Pin()],
    },
    {
      key: 'update',
      title: 'Update',
      showDivider: true,
      items: [Update(), CheckForUpdate(), AutoUpdate()],
    },
    {
      key: 'about',
      title: 'About',
      items: [DocPage(), Info()],
    },
    {
      key: 'danger-zone',
      items: [Uninstall()],
    },
  ];
};
export const Pin = (): DropdownItemProps => {
  const {cardId} = useCardData();

  const isPinned = useIsPinnedCard(cardId);

  const onPress = () => window.api.storageMgr.pinnedCards(isPinned ? 'remove' : 'add', cardId);

  return {
    key: 'pin-unpin',
    title: isPinned ? 'Unpin' : 'Pin',
    startContent: GetIconByName('Pin', {
      className: `${isPinned ? 'rotate-45' : 'rotate-0'} transition duration-500`,
    }),
    className: 'cursor-default',
    onPress,
  };
};

viinaji avatar Apr 11 '24 09:04 viinaji

I am facing same problem. Still no fix?

neelhabib avatar Jun 10 '24 01:06 neelhabib

Why is this discussion marked as closed?

emmanuel-salvo avatar Jun 12 '24 21:06 emmanuel-salvo

Why is this discussion marked as closed?

Indeed... I just ran into this issue myself. Pretty disappointing...

Reading the history of the bug, it seems at some point the answer was "some smartie pants created some super smart scheme in some library and didn't think this thru so now this is a limitation, tough luck.. here is a workaround and go on with your lives"

Workaround is that instead of doing something like (in my case it happened with an Accordion component, but same thing applies to Tables etc)

<Accordion>
  <MyPrettyComponent>
    { item => <AccordionItem ..} ... >...</AccordionItem> } 
  </MyPrettyComponent>
</Accordion>

You refactor this to:

<MyNowNotSoPrettyComponent>{
  items =>
    <Accordion>{
        items.map(item => 
          <AccordionItem ...} ..> .. </AccordionItem>)
   }</Accordion>
 </MyNowNotSoPrettyComponent>

Worked for me.. and if you can't refactor your component for whatever reason, you are f.. ried.

codemonkeylabs-de avatar Jun 13 '24 09:06 codemonkeylabs-de

I've found this issue to persist in almost every NextUI components that utilizes list items like Dropdown, ListBox, Select, Tabs, etc. To make a consistent theme in my App, I need to export the base component and all of its child component like this(image). This way I can just change any props from here and it'll be applied globally to my project. But this issue has created scattered abstraction everywhere I've to use the child component directly from nextui package instead of my @components. Really hoping for some help if someone finds any solution. image

niraj-khatiwada avatar Jun 14 '24 06:06 niraj-khatiwada

@devongovett

It's not a limitation we wanted, just a technical trade off required by internal implementation details. React actually makes it extremely difficult to build components that are aware of their children like Table needs to be in order to implement things like keyboard navigation.

That said, in React Aria Components, our new component library built on top of React Aria hooks, we found a new way of implementing collection components that lifts this restriction. The goal is to extract this new implementation out once it is stable and offer it to React Aria hooks users such as Next UI as well. So hopefully in the near future Next UI will be able to lift the restriction as well.

In the meantime, we've recommended moving the logic to a component inside the item rather than a wrapper. You could refactor your example like the following:

<TableBody>
  {data.map(item => (
    <TableRow key={`example-${item.id}`}>
      <TableCell>{item.foo}</TableCell>
      <TableCell><ItemStatus item={item} /></TableCell>
    </TableRow>
  ))}
</TableBody>

function ItemStatus({item}) {
  const status = useFetchItemStatus(item)
  return status || 'loading status';
}

Are there any updates on this?

Honestly, this argument really rings hollow. Everyone in the react ecosystem knows how to handle this. You are partially there; you just need to use inversion of control.

Instead of making it impossible to use standard and good practice for building one's react components, you just document the requirements of any child component(i.e. it must be forwardref, it must take this set of props and spread them on the top level element, etc) and use render props.

I've run into this issue trying to use almost every piece of this library and it's really making me reconsider using it, which is too bad. It was a candidate for becoming our primary internal component library at work and in my own personal projects. Unfortunately, the way it works now means that any react developer in our company that tries to use this library in a very sensible and reasonable way will run into extremely opaque errors. I would basically need to onboard everyone onto this issue specifically.

saevarb avatar Aug 05 '24 10:08 saevarb

Are there any updates on this?

Yes! In our last React Aria release, we introduced a new collection implementation that supports custom wrappers, available in the @react-aria/collections package. Here's an example of using it with React Aria hooks: https://stackblitz.com/edit/react-aria-collections?file=src%2FApp.tsx,package.json&terminal=dev

Next UI will need to do some work to adopt this in their implementation. One wrinkle is that there is a small breaking API change: you'll need to pass an id prop instead of/in addition to a key since we can no longer access React's key in the new implementation (React does not expose it inside component props). This means Next UI will either need to release a major version, or provide a way to opt-into the new API. I'm happy to work with the team to help with that if needed. Otherwise, after the internal changes in Next UI are made, it should be pretty transparent to users.

devongovett avatar Aug 05 '24 15:08 devongovett

@niraj-khatiwada were you able to find a solution?

alexgradinar avatar Aug 22 '24 07:08 alexgradinar

@niraj-khatiwada were you able to find a solution?

Nope, no solution.

niraj-khatiwada avatar Aug 22 '24 12:08 niraj-khatiwada