nextui-org / nextui

🚀 Beautiful, fast and modern React UI library.
https://nextui.org
MIT License
21.21k stars 1.35k forks source link

[BUG] - Typescript error with DropdownItem #1691

Closed matteo-pkxp closed 1 week ago

matteo-pkxp commented 11 months ago

NextUI Version

2.1.13

Describe the bug

Hello, I have a type issue when using the Dropdown component, this is the code triggering the ts error:

<DropdownMenu aria-label="Profile Actions" variant="flat">
    { menuItems.map((entry) => (
        <DropdownItem key={entry.link} showDivider={entry.divider}>
            {entry.name}
        </DropdownItem>
    ))}
    <DropdownItem key="custom">Test123</DropdownItem>
</DropdownMenu>

The error is: Type 'Element[]' is not assignable to type 'CollectionElement<object>'. The code runs fine and the result is as expected, that is a list of DropdownItems followed by the non-iterated "Test123" item, but the error (that I couldn't suppress) prevents me from building the app for production.

If I wrap it with a fragment <></>, which makes the TS error disappear, this time the error is at runtime and becomes TypeError: Cannot convert a Symbol value to a string.

Any idea on how to make it work? I have custom element at the bottom of this dropdown, that doesn't match the structure of the iterated elements.

Your Example Website or App

No response

Steps to Reproduce the Bug or Issue

I'm using Typescript 5.1.6 and NextJS 13.4.13

Expected behavior

There should be either no runtime or linter errors.

Screenshots or Videos

No response

Operating System Version

Windows with Linux Subsystem 2.0 (Debian LTS)

Browser

Chrome

matteo-pkxp commented 11 months ago

I found a not-so-elegant solution to ignore the TS error:

<DropdownMenu aria-label="Profile Actions" variant="flat">
{[
  // @ts-ignore
  menuItems.map((entry) => (
      <DropdownItem key={entry.link}>
          {entry.name}
      </DropdownItem>
  )),
  <DropdownItem key="custom">Test123</DropdownItem>,
]}
</DropdownMenu>

This suppresses the TS error, renders the Dropdown as expected and throws no runtime errors, but I'm sure there's a better solution.

matteo-pkxp commented 11 months ago

Option number 2, which is probably the best one:

 <DropdownMenu aria-label="Profile Actions" variant="flat">
   <DropdownSection>
      { menuItems.map((entry) => (
          <DropdownItem key={entry.link} showDivider={entry.divider}>
              {entry.name}
          </DropdownItem>
      ))}
    </DropdownSection>
    <DropdownItem key="custom">Test123</DropdownItem>
</DropdownMenu>

The only downside here is the aria-label warning, because It's a titleless section.

Not sure if I should close the issue though. I don't think the errors I encountered are expected.

lalalazero commented 11 months ago

Hello~

I did some research to figure this out. so the underlying of menu is using react-aria-components. if you wish to render dynamic items, I think the proper way should go with this

 <DropdownMenu aria-label="Profile Actions" variant="flat" items={menuItems}>
   {(item) => <DropdownItem key={item.key} showDivider={item.divider} />}
</DropdownMenu>

you can find the reason why suggest to use items props instead of Array.map here

matteo-pkxp commented 11 months ago

Hi! Thanks for the suggestion, this is surely something that's good to know. Unfortunately, it doesn't fix the issue.

Type '(item: MenuEntry) => JSX.Element' is not assignable to type 'CollectionChildren<object>'.
Type '(item: MenuEntry) => JSX.Element' is not assignable to type '(item: object) => CollectionElement<object>'.
Types of parameters 'item' and 'item' are incompatible.
Property 'name' is missing in type '{}' but required in type 'MenuEntry'.ts(2322)

Looks like it's a Typescript incompatibility of some sort. The dropdown gets rendered as intended, it's just the compiler that goes crazy.

Also, if I do something like this below, only the "custom" element gets rendered:

 <DropdownMenu aria-label="Profile Actions" variant="flat" items={menuItems}>
   {(item) => <DropdownItem key={item.key} showDivider={item.divider} />}
   <DropdownItem key="custom">Test123</DropdownItem>
</DropdownMenu>

The only solution is to use the DropdownSection component, that supports the items prop.

 <DropdownMenu aria-label="Profile Actions" variant="flat">
   <DropdownSection  items={menuItems} aria-label="I'd rather not set this, it has no meaning to me in this case">
      {(item) => <DropdownItem key={item.key} showDivider={item.divider} />}
   </DropdownSection>
   <DropdownItem key="custom">Test123</DropdownItem>
</DropdownMenu>
carlosriveroib commented 11 months ago

I'm having same issue

xavier-villelegier commented 9 months ago

Hey ! I see you closed this in 2.2.0 @jrgarciadev ?

I'm still having this issue with the following in 2.2.1:

<Dropdown placement="bottom-end">
  <DropdownMenu aria-label="Profile Actions" variant="flat">
    {session.user?.isAdmin && (
      <DropdownSection title="Admin" showDivider>
        <DropdownItem key="prisma">Whatever</DropdownItem>
      </DropdownSection>
    )}
  </DropdownMenu>
</Dropdown>

Same symptoms, ts-error, then runtime error if wrapping it inside a fragment 😕

aaronhawkey commented 9 months ago

My code that is experiencing the same error in 2.2.8:

        {
          notificationsData.map((notification) => {
            return (
              <DropdownItem key={notification.id}>
                <Notification
                  body={notification.body}
                  date={notification.createdAt}
                  read={notification.read}
                />
              </DropdownItem>
            )
          })
        }

@xavier-villelegier Do you know of a work around?

jrgarciadev commented 9 months ago

Hey guys I'm trying to figure out ways to fix this, the react-aria collection API only accepts specific components as children (Item, Section) to build the collection, that's why it fails when trying to pass custom components.

@aaronhawkey did you try using the Dropdown dynamic render?

import React from "react";
import {Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Button} from "@nextui-org/react";

export default function App() {

  const notificationsData = [
    {
      key: "1",
      read: false,
      body: "First message",
      createdAt: null
    },
    {
      key: "2",
      read: false,
      body: "Second Message",
      createdAt: null
    },
    {
      key: "3",
      read: false,
      body: "Third message",
      createdAt: null
    },
  ];

  return (
    <Dropdown>
      <DropdownTrigger>
        <Button 
          variant="bordered" 
        >
          Open Menu
        </Button>
      </DropdownTrigger>
      <DropdownMenu aria-label="Notifications" items={notificationsData}>
        {(notification) => (
          <DropdownItem  key={item.key} textValue={notification.body}>
             <Notification
                  body={notification.body}
                  date={notification.createdAt}
                  read={notification.read}
                />
          </DropdownItem>
        )}
      </DropdownMenu>
    </Dropdown>
  );
}
louisescher commented 9 months ago

Hey, I'm struggling with a similar issue, although in my case it's with the DropdownSection:

<DropdownSection showDivider key={"notifications"} items={notifications}>
  {(item: any) => (
    <DropdownItem
      key={item.id} 
      onPress={notificationPressHandler}
      className={styles.notification__wrapper}
    >
      <span className={styles.notification__topic}>{item.topic}</span>
      <p className={styles.notification__content}>{item.content}</p>
    </DropdownItem>
  )}
</DropdownSection>

This is only a TypeScript error, I'll be using ts-ignore for now to avoid it. Components render fine.

begalinsaf commented 8 months ago

Hey ! I see you closed this in 2.2.0 @jrgarciadev ?

I'm still having this issue with the following in 2.2.1:

<Dropdown placement="bottom-end">
  <DropdownMenu aria-label="Profile Actions" variant="flat">
    {session.user?.isAdmin && (
      <DropdownSection title="Admin" showDivider>
        <DropdownItem key="prisma">Whatever</DropdownItem>
      </DropdownSection>
    )}
  </DropdownMenu>
</Dropdown>

Same symptoms, ts-error, then runtime error if wrapping it inside a fragment 😕

i have same isue for this

begalinsaf commented 5 months ago

you can try this bad solution

 let dropdownItems = [
    {
      key: 'profile',
      label: 'Profile',
    },
    {
      key: 'signout',
      label: 'Signout',
    },
  ];

  if (auth?.user.role !== 'USER') {
    dropdownItems.push({
      key: 'dashboard',
      label: 'Dashboard',
    });
  }

  //return

             <DropdownMenu
                aria-label='dynamic'
                items={dropdownItems}
              >
                {(item) => (
                  <DropdownItem
                    key={item.key}
                    color={item.key === 'signout' ? 'danger' : 'default'}
                    className={item.key === 'signout' ? 'text-danger' : ''}
                  >
                    {item.label}
                  </DropdownItem>
                )}
              </DropdownMenu>
savchukartur commented 5 months ago

const list = [ { key: '1', value: 'item_1'}, { key: '2', value: 'item_2'}, ]

{list.map((el) => {el.value})} Logout It works for me
aaronhawkey commented 5 months ago

Hey guys I'm trying to figure out ways to fix this, the react-aria collection API only accepts specific components as children (Item, Section) to build the collection, that's why it fails when trying to pass custom components.

@aaronhawkey did you try using the Dropdown dynamic render?

import React from "react";
import {Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Button} from "@nextui-org/react";

export default function App() {

  const notificationsData = [
    {
      key: "1",
      read: false,
      body: "First message",
      createdAt: null
    },
    {
      key: "2",
      read: false,
      body: "Second Message",
      createdAt: null
    },
    {
      key: "3",
      read: false,
      body: "Third message",
      createdAt: null
    },
  ];

  return (
    <Dropdown>
      <DropdownTrigger>
        <Button 
          variant="bordered" 
        >
          Open Menu
        </Button>
      </DropdownTrigger>
      <DropdownMenu aria-label="Notifications" items={notificationsData}>
        {(notification) => (
          <DropdownItem  key={item.key} textValue={notification.body}>
             <Notification
                  body={notification.body}
                  date={notification.createdAt}
                  read={notification.read}
                />
          </DropdownItem>
        )}
      </DropdownMenu>
    </Dropdown>
  );
}

Sorry for the late reply. I did and it worked for me.

rkrajewski commented 4 months ago

None of the above solutions work for me for conditional rendering. I managed to implement a slightly tricky solution by using Type Guard, like so:

function testT<T>(input: T | false | undefined | null): input is T {
  return Boolean(input)
}

function filter<T>(input: (T | false | undefined | null)[]): T[] {
  return input.filter(testT)
}

...

        <DropdownSection showDivider>
          {/* here comes the above 'filter 'function */}
          {filter([
            hasUnpublished && (
              <DropdownItem key="publish" className="text-success">
                Publish
              </DropdownItem>
            ),
            hasPublished && (
              <DropdownItem key="unpublish" className="text-warning">
                Unpublish
              </DropdownItem>
            ),
          ])}
        </DropdownSection>
pablojsx commented 4 months ago

If you use a ternary if ? : you can return a empty DropdownItem with a className of "hidden". It will work. Or just hide the the DropdownItem with a "hidden" class. If it's a client component then you'll be sending the javascript of all anyways.

gamlerd13 commented 1 month ago

This should work

<DropdownMenu aria-label="Profile Actions" variant="flat">
      {menuItems ? (
        menuItems.map((entry) => (
          <DropdownItem key={entry.link} showDivider={entry.divider}>
            {entry.name}
          </DropdownItem>
        ))
      ) : (
        <DropdownItem>handle this, if there is no data</DropdownItem>
      )}
      <DropdownItem key="custom">Test123</DropdownItem>
    </DropdownMenu>
HM-Suiji commented 1 month ago

I think this is a big bug,DropdownMenu no longer seems to accept the prop of children. in vscode,eslint error:不能将类型“{ children: (item: { link: string; name: string; }) => Element; "aria-label": string; items: { link: string; name: string; }[]; }”分配给类型“IntrinsicAttributes & Props & { ref?: Ref | undefined; }”。 类型“IntrinsicAttributes & Props & { ref?: Ref | undefined; }”上不存在属性“children”。ts(2322) and see the web ,error too: image Error: Functions are not valid as a child of Client Components. This may happen if you return children instead of <children /> from render. Or maybe you meant to call this function rather than return it. <... aria-label=... items={[...]} children={function children}>

pablojsx commented 1 month ago

update nextui

MFRamon commented 1 week ago

August 2024 and the issue is still happening is there a fix in the way?

wingkwong commented 1 week ago

@MFRamon can you share your code reproducing the issue? I saw some usages aren't really bugs but misuse of the collection-based components.

MFRamon commented 1 week ago

Sure @wingkwong .

Actually a few hours back I figured out the issue. Apparently in version 2.4.6 is not happening, what I did was delete the node_modules folder and reinstall dependencies. Maybe I had the older version from nextui installed. Now It's rendering and working now. But posting the code here just in case you have any suggestions or comments.

  <Dropdown placement="right-end">
      <DropdownTrigger>
        <button>Button</button>
      </DropdownTrigger>
      <DropdownMenu variant="faded" aria-label="Dropdown menu title" className="w-[17vw] !p-0">
        <DropdownSection title={undefined} className="" id="section-titulos">
          <DropdownItem endContent={<button>Mark all as read</button>}>NOTIFICATIONS</DropdownItem>
        </DropdownSection>
        <DropdownSection title={undefined}>
          {items.map((item) => (
            <DropdownItem className="" key={item.key}>
              {item.label}
            </DropdownItem>
          ))}
        </DropdownSection>
        <DropdownSection title={undefined} className="" id="section-titulos">
          <DropdownItem>NOTIFICATIONS</DropdownItem>
        </DropdownSection>
      </DropdownMenu>
    </Dropdown>
Version in package.json is : 

 "@nextui-org/react": "^2.4.6",
 .
 .
 .

 Hopefully it's helpful for someone else
wingkwong commented 1 week ago

I'm closing this issue since I couldn't reproduce any typescript error. Please note that this is a collection-based component and make sure there is at least one child there. The following example would fail because if session.user?.isAdmin is false, then there won't be any child which breaks the parsing.

<Dropdown placement="bottom-end">
  <DropdownMenu aria-label="Profile Actions" variant="flat">
    {session.user?.isAdmin && (
      <DropdownSection title="Admin" showDivider>
        <DropdownItem key="prisma">Whatever</DropdownItem>
      </DropdownSection>
    )}
  </DropdownMenu>
</Dropdown>