mui / material-ui

Material UI: Ready-to-use foundational React components, free forever. It includes Material UI, which implements Google's Material Design.
https://mui.com/material-ui/
MIT License
91.86k stars 31.57k forks source link

[prop component] ButtonBaseProps not accepting a component property #15827

Closed Slessi closed 10 months ago

Slessi commented 4 years ago

Expected Behavior 🤔

component property should be accepted

Current Behavior 😯

component property is not accepted

Steps to Reproduce 🕹

const props: ButtonProps<'span'> = {
  component: 'span',
  ^^^^^^^^^^^^^^^^^ 'component' does not exist in type ...
  onClick(event: React.MouseEvent<HTMLSpanElement>) {},
};

Context 🔦

Need for integration with a third-party routing library

Your Environment 🌎

Tech Version
Material-UI v4.0.0
TypeScript v4.1.0

Edit @eps1lon: Heavily editorialized the original issue which was inaccurate and misleading.

Most of the issue has been solved, see https://next.material-ui.com/guides/typescript/#usage-of-component-prop

eps1lon commented 4 years ago

While the error message isn't helpful I fail to see why const MyButton = () => <ButtonBase component="lol" />; should be accepted by the type-checker. It will trigger a warning in React which is why we want to prevent it at the type level.

Please include code that should be accepted in your opinion.

piotros commented 4 years ago

I guess that @Slessi used lol only as an example.

The problem is that there is no way to pass component prop to Button.

Just try to compile TS code from this example: https://next.material-ui.com/components/buttons/#third-party-routing-library

Slessi commented 4 years ago

I guess that @Slessi used lol only as an example.

thank you @piotros :) i thought this was obvious

piotros commented 4 years ago

Am I right that the problem occurs because of lack of OverridableComponent in Button.d.ts typings?

eps1lon commented 4 years ago

I guess that @Slessi used lol only as an example.

thank you @piotros :) i thought this was obvious

Which is why I ask for a specific example. We're have no issue with the example. Our docs are type-checked as well.

Just try to compile TS code from this example: next.material-ui.com/components/buttons/#third-party-routing-library

Am I right to assume that this example is not working with TypeScript 3.4.5?

bengry commented 4 years ago

To add to this, the following doesn't compile in TypeScript in MUIv4, but should:

// MyButton.ts
import { Button } from '@material-ui/core'
import { ButtonProps } from '@material-ui/core/Button'
import React from 'react'

export const MyButton = React.forwardRef<HTMLButtonElement, ButtonProps>((btnProps, ref) => (
  <Button
    ref={ref}
    variant="contained"
    color="primary"
    {...btnProps}
  />
))

// MyPage.ts

function MyPage() {
  return (
    <MyButton component={Link}> // <-- error on this
      Click me
    </MyButton>
  )
}

error message is:

Type '{ children: string; component: FunctionComponent<{}>; }' is not assignable to type 'IntrinsicAttributes & Pick<{ action?: ((actions: ButtonBaseActions) => void) | undefined; buttonRef?: ((instance: unknown) => void) | RefObject | null | undefined; centerRipple?: boolean | undefined; ... 6 more ...; TouchRippleProps?: Partial<...> | undefined; } & { ...; } & CommonProps<...> & Pick<...>, "c...'. Property 'component' does not exist on type 'IntrinsicAttributes & Pick<{ action?: ((actions: ButtonBaseActions) => void) | undefined; buttonRef?: ((instance: unknown) => void) | RefObject | null | undefined; centerRipple?: boolean | undefined; ... 6 more ...; TouchRippleProps?: Partial<...> | undefined; } & { ...; } & CommonProps<...> & Pick<...>, "c...'.

I'm using TypeScript 3.3.3, but also checked 3.4.5, and getting the same error

eps1lon commented 4 years ago

@bengry I don't think we can support this particular pattern while also supporting the generic types from the component prop. I would recommend using the props key in the theme to override default props: https://material-ui.com/customization/globals/#default-props

eps1lon commented 4 years ago

Just try to compile TS code from this example: next.material-ui.com/components/buttons/#third-party-routing-library

A fresh clone from this example (edit in codesandbox, download zip, unzip, yarn, yarn add typescript, yarn start) starts without issues.

Without the code and tsconfig I'm not able to debug this issue.

bengry commented 4 years ago

@eps1lon Take a look at https://github.com/bengry/material-ui-v4-button-component-ts-repro, and more specifically: https://github.com/bengry/material-ui-v4-button-component-ts-repro/blob/master/src/App.tsx#L9

Due to ComponentWorkaroundProps the types work fine. Without them TypeScript complains about component prop not being declared on MyButton.

I don't see a reason why ButtonProps can't include what's introduced in ComponentWorkaroundProps inside the library. Am I missing something?

eps1lon commented 4 years ago

There's a lot of types implementation details that goes into this. Generic props are currently impossible to get right. ButtonProps cannot include component which is working fine with just <Button component={Link} /> due to this limitation.

However, you don't have to create that additional component wrapper to begin with. As I said you could provide a theme to your Buttons that uses contained as the default variant.

This would avoid all the type workarounds. Your MyButton is currently not sound. If I pass an achor as the component the type in the ref would be incorrect. It's one of the reasons why our Button is typed the way it is. To fix issues with generic parts of the props (ref, events etc). It's probably best if you just cast your MyButton to our Button e.g. const MyButton = React.forwardRef(...) as Button.

bengry commented 4 years ago

I agree the typing for this is hard to get right, but using a theme is not an option in this case, since our case is a bit more complex - I simplified it for the example, but basically - we pass some default props to Button and export a variant of it, only some of the components use, and only sometimes. Is there a better way to create a variant of an existing MUI component that injects some props to it?

eps1lon commented 4 years ago

but using a theme is not an option in this case, since our case is a bit more complex - I simplified it for the example, but basically

I need the actual example to help you properly. Otherwise it's just guesswork what might help you.

Slessi commented 4 years ago

@eps1lon kk right u r, my example actually is fine, my problem lies with ButtonBaseProps

const AdapterLink = React.forwardRef<HTMLAnchorElement, LinkProps>((props, ref) => (
    <Link innerRef={ref as any} {...props} />
));

function MyButton(props: ButtonBaseProps) {
    return <ButtonBase {...props} />;
}

function ButtonRouter() {
    return (
        <MyButton color="primary" component={AdapterLink} to="/">
            Simple case
        </MyButton>
    );
}

Property 'component' does not exist on type 'IntrinsicAttributes & { action?: ((actions: ButtonBaseActions) => void) | ...

eps1lon commented 4 years ago

@Slessi Looks like the same issue as @bengry. You don't need MyButton though. Would help if you include a concrete example.

Slessi commented 4 years ago

@eps1lon Come on you're just nitpicking now, in my particular case I want to share a styled button. There could be many reasons to have a shared ButtonBase component

const useStyles = makeStyles((theme: Theme) => ({
    fancy: {
        // Magical JSS goes here
    },
}));

function MyFancyButton(props: ButtonBaseProps) {
    const classes = useStyles();

    return <ButtonBase className={classes.fancy} {...props} />;
}
eps1lon commented 4 years ago

There could be many reasons to have a shared ButtonBase component

Probably. There are also many reasons to not use a wrapper components. I can't recommend an implementation without the full picture. I'm just trying to avoid over-abstracting things. And if I don't know what you're doing I can't explain to you why this is happening.

In this particular case wrapper components are quite hard to type right because of the generic nature of our components. This is why I need concrete examples so that I can evaluate if another approach might be better.

This is quite common in open source repositories. You will rarely see maintainers working on generic issues.

Hope this justification is sufficient for you and we can finally work on the issue.

I don't have a good answer for you yet. TypeScript has quite a few limitations currently in the JSX domain. If your wrappers don't have any additional props it's probably best to just cast them back to the Material-UI button e.g. export default MyFancyButton as ButtonBase.

Slessi commented 4 years ago

@eps1lon if they do have additional props?

bengry commented 4 years ago

@eps1lon here's a more realistic example:

const CancelButton = React.forwardRef<
  HTMLButtonElement,
  Omit<ButtonProps, 'component'> & ComponentWorkaroundProps<LinkProps<void>>
>(({ children, ...props }, ref) => {
  return (
    <Button variant="contained" {...props} ref={ref} color="secondary">
      {children || 'Cancel'}
    </Button>
  );
});

const ConfirmButton = React.forwardRef<
  HTMLButtonElement,
  Omit<ButtonProps, 'component'> & ComponentWorkaroundProps<LinkProps<void>>
>(({ children, ...props }, ref) => {
  return (
    <Button variant="contained" {...props} ref={ref} color="primary">
      {children || 'Confirm'}
    </Button>
  );
});

These two buttons should be used throughout the app in cases where you want a Confirm or a Cancel button (think inside Dialogs for example, but could go inside other components as well). However, sometimes you want these to be links (using the Link from @reach/router or react-router), but still have the same props passes, so you'd like to do:

<CancelButton component={Link} to="/" />

In this case the cancel button does a navigation back to the home page. This does not currently compile in MUI v4, due to the aforementioned issue in this thread.

gunn4r commented 4 years ago

Just want to chime in and am having this same issue. Trying to get our codebase migrated to v4 and this issue has been a real headache!

I created a stackoverflow issue regarding it with a code sandbox and such. My use case is basically identical to @bengry's. We have various wrapped buttons for things like Cancel, Confirm, etc with some custom styling / props and what not.

Here's the stackoverflow if interested: https://stackoverflow.com/questions/56300136/when-extending-button-component-property-not-found-what-is-the-correct-way-to

Would love to get past this issue! Thanks for all the awesome work on v4!

eps1lon commented 4 years ago

~Closing this in favor of #15695. For issues related to invalid hooks call please open a separate issues. This issue was about typescript related issues.~ Wrong tab

ClassicDarkChocolate commented 4 years ago

Use React.forwardRef<T, Omit<LinkProps, "forwardedRef">> instead of React.forwardRef<T, LinkProps> solves the problem.

Example codes:

import { Button } from "@material-ui/core";
import React from "react";

interface LinkProps {
    forwardedRef: React.Ref<HTMLAnchorElement>;
    href?: string;
    onClick?: VoidFunction;
}

const Link: React.FunctionComponent<LinkProps> = ({ forwardedRef }) => {
    return <a ref={forwardedRef} />;
};

const ForwardedLink = React.forwardRef<
    HTMLAnchorElement,
    Omit<LinkProps, "forwardedRef">
>((props, ref) => <Link forwardedRef={ref} {...props} />);

const AnotherComponent: React.FunctionComponent<{}> = () => {
    return (
        <div>
            <Button
                component={ForwardedLink}
                href={"123"}
                onClick={() => {
                    return;
                }}
            />
            <ForwardedLink />
        </div>
    );
};
cozmy commented 4 years ago

I also have this problem. TypeScript complains that the "component" prop is missing on ButtonBase.

The docs state that it exists, whereas in this commit we can see it removed.

eps1lon commented 4 years ago

The docs state that it exists, whereas in this commit we can see it removed.

Because in that commit it was added at another location.

WillSquire commented 4 years ago

Using:

interface IButtonBaseProps extends ButtonBaseProps {
  component?: HTMLElement
}

In the interim.

CorayThan commented 4 years ago

@WillSquire How does that help? The button doesn't magically use that interface instead of its normal one.

WillSquire commented 4 years ago

No it doesn't, but because the exported components do have component fields I can extend the imported prop definition that's missing it in my case.

import Tab, { TabProps } from '@material-ui/core/Tab'
import React from 'react'
import { Link, LinkProps } from 'react-router-dom'
import { Omit } from '../../../util'

// Work around for issue: https://github.com/mui-org/material-ui/issues/15827
interface ITabProps extends TabProps {
  component?: HTMLElement
}

export type ILinkTab = ITabProps & Omit<LinkProps, 'innerRef'>

const CollisionLink = React.forwardRef<
  HTMLAnchorElement,
  Omit<LinkProps, 'innerRef'>
>((props, ref) => <Link innerRef={ref} {...props} />)

export const LinkTab: React.FC<ILinkTab> = props => {
  return <Tab component={CollisionLink} {...props} />
}
eps1lon commented 4 years ago

@WillSquire Your types would accept <LinkTab component={NotALink} />. Is this actually expected?

CarsonF commented 4 years ago

If I may jump in here. The ButtonProps type accepts two generics and this worked perfectly for me.

export type ButtonLinkProps = ButtonProps<typeof Link, LinkProps>;

export const ButtonLink = (props: ButtonLinkProps) => (
  <Button {...props} component={Link} />
);

The pain points for me are with the other components that use ExtendButtonBase instead of ExtendButtonBaseTypeMap - IconButton, Tab, MenuItem, etc.

CarsonF commented 4 years ago

I can also confirm changing the IconButton type definitions to match fixes the typescript errors for me.

Modified IconButton types:

import { IconButton as MuiIconButton, PropTypes } from '@material-ui/core';
import { ExtendButtonBase, ExtendButtonBaseTypeMap } from '@material-ui/core/ButtonBase';
import { IconButtonClassKey } from '@material-ui/core/IconButton';
import { OverrideProps } from '@material-ui/core/OverridableComponent';
import * as React from 'react';

type IconButtonTypeMap<P, D extends React.ElementType> = ExtendButtonBaseTypeMap<{
  props: P & {
    color?: PropTypes.Color;
    disableFocusRipple?: boolean;
    edge?: 'start' | 'end' | false;
    size?: 'small' | 'medium';
  };
  defaultComponent: D;
  classKey: IconButtonClassKey;
}>;

export const IconButton: ExtendButtonBase<IconButtonTypeMap<{}, 'button'>> = MuiIconButton;

export type IconButtonProps<D extends React.ElementType = 'button', P = {}> = OverrideProps<
  IconButtonTypeMap<P, D>,
  D
>;

Usage:

export const FacebookLink = (props: IconButtonProps<'a'>) => (
  <IconButton
    href="https://www.facebook.com/"
    target="_blank"
    rel="noopener noreferrer"
    {...props}
  >
    <FacebookIcon color="inherit" fontSize="inherit" />
  </IconButton>
);

FYI, I'm migrating from v3 right now, so I haven't tried running my app yet.

ypresto commented 4 years ago

Implemented @CarsonF's solution in #16487!

alexeychikk commented 4 years ago

Is this about to be fixed or is it even considered a bug? I just copy-pasted the code from the docs for my link component with minor changes and the TS error shows up:

Property 'component' does not exist on type 'IntrinsicAttributes...


import Button, { ButtonProps } from "@material-ui/core/Button";
import { withStyles, WithStyles } from "@material-ui/core/styles";
import { GatsbyLinkProps, Link } from "gatsby";
import * as React from "react";

import { styles } from "./ButtonLink.styles";

export type ButtonLinkProps = WithStyles & ButtonProps & GatsbyLinkProps<{}> & { partiallyActive?: boolean; replace?: boolean; to: string; };

const LinkComponent = React.forwardRef<HTMLAnchorElement, GatsbyLinkProps<{}>>( ({ ref, innerRef, ...props }, linkRef) => ( <Link innerRef={linkRef as Function} {...props} /> ), );

class ButtonLink extends React.PureComponent { public render() { const { classes, innerRef, ...props } = this.props; return ( <Button {/ ^^^^^^ Property 'component' does not exist on type 'IntrinsicAttributes... /} component={LinkComponent} activeClassName={classes.active} {...props}> {this.props.children} ); } }

eps1lon commented 4 years ago

For the current workaround check: https://material-ui.com/guides/typescript/#usage-of-component-prop

Just a quick summary why this is currently an issue and hasn't been fixed yet:

Usually the type of a component and the type of its props are closely related. Basically the type of the component is just something like (props: Props) => JSX.Element. In this world higher-order components or wrapping the component by simply forwarding the same props type works without any issue. However, the base component would not accept excess props applied to the overwritten component nor would it adjust event handler types to recognize the specific Element i.e. polymorphism does not really work with just a props interface.

<Button component={RouterLink} to="/home" />; // didn't used to be type checked
<Button component="a" onClick={event => { /* event.currentTarget would either be implicitly any or HTMLButtonElement */}} />; //

In order to fix the last issues we need to overload the call signature of the function which means higher-order components won't work anymore. They usually infer a prop type which does not support polymorphism. Forwarding doesn't work either because again it uses a props type rather than a function type.

Even if we would revert the change to allow component on decorated components <StyledButton component={RouterLink} to="/home" /> would still not work. But we are able to make <Button component={RouterLink} to="/home" /> completely type safe.

I'm currently exploring a different API that wouldn't accept a component type but a render prop. It would simplify typings (for humans and compilers) by a lot but comes with a bit manual wiring and doesn't look quite nice (if you care about it).

I would appreciate it if any further reports only include the actual code that produced the error and then only if that error got reported by a build tool. IDEs (especially Webstorm) have issues with overloaded call signatures of components as well which is another issue (and one we likely can't fix anyway).

duvet86 commented 4 years ago

If it can help this is how I fixed it: component={"div" as ElementType} I know it's a hack but...

const MenuItemSelectAll: React.SFC<IProps> = ({ classes, value }) => (
  <MenuItem
    divider
    component={"div" as React.ElementType}
    className={classes.menuItem}
    value={
      value === SelectEnum.AllLabel
        ? SelectEnum.SelectNone
        : SelectEnum.SelectAll
    }
  >
    <Checkbox checked={value === SelectEnum.AllLabel} />
    <ListItemText primary={"Select all"} />
  </MenuItem>
);
TidyIQ commented 4 years ago

If it can help this is how I fixed it: component={"div" as ElementType} I know it's a hack but...

const MenuItemSelectAll: React.SFC<IProps> = ({ classes, value }) => (
  <MenuItem
    divider
    component={"div" as ElementType}
    className={classes.menuItem}
    value={
      value === SelectEnum.AllLabel
        ? SelectEnum.SelectNone
        : SelectEnum.SelectAll
    }
  >
    <Checkbox checked={value === SelectEnum.AllLabel} />
    <ListItemText primary={"Select all"} />
  </MenuItem>
);

What's your type definition for ElementType? I get the error:

Cannot find name 'ElementType'

Code:

<Button  component={"a" as ElementType}>foo</Button>
fzaninotto commented 4 years ago

Just a reminder, #16487 does not solve the issue.

This works:

import React from "react";
import { Button } from '@material-ui/core';
import { MemoryRouter, Link } from 'react-router-dom';

function App() {
  return (
    <MemoryRouter>
        <Button component={Link} to="http://www.google.com">hello</Button>
    </MemoryRouter>
  );
}

This does not work:

import React from "react";
import { Button } from '@material-ui/core';
import { ButtonProps } from '@material-ui/core/Button';
import { MemoryRouter, Link } from 'react-router-dom';

const MyButton = (props: ButtonProps) => <Button {...props} />

function App() {
  return (
    <MemoryRouter>
        <MyButton component={Link} to="http://www.google.com">hello</MyButton>
    </MemoryRouter>
  );
}

This type problem is forcing react-admin to cast button props as any...


Edit: solution, using https://next.material-ui.com/guides/typescript/#usage-of-component-prop: https://codesandbox.io/s/containedbuttons-material-demo-forked-mrwfw?file=/demo.tsx

import { Button } from "@material-ui/core";
import { ButtonProps } from "@material-ui/core/Button";
import { MemoryRouter, Link as RouterLink } from "react-router-dom";
import React from "react";

function GenericCustomComponent<C extends React.ElementType>(
  props: ButtonProps<C, { component?: C }>
) {
  return <Button {...props} />;
}

export default function App() {
  return (
    <MemoryRouter>
      <GenericCustomComponent component={RouterLink} to="http://www.google.com">
        hello
      </GenericCustomComponent>
    </MemoryRouter>
  );
}
kelly-tock commented 4 years ago

Chiming in, we should be able to extend the types properly for all components.

zhouzi commented 4 years ago

Here's another example throwing the error (using the TypeScript compiler or Webpack):

import * as React from 'react';
import { Avatar } from '@material-ui/core';
import { AvatarProps } from '@material-ui/core/Avatar';

interface Props {
  user: {
    firstname: string;
    lastname: string;
  };
}

class UserAvatar extends React.Component<Props & AvatarProps> {
  public render() {
    const { user, ...props } = this.props;

    return (
      <Avatar {...props}>
        {`${user.firstname.charAt(0)}${user.lastname.charAt(0)}`}
      </Avatar>
    );
  }
}

export default UserAvatar;

Most of the time, we use this component this way:

<UserAvatar user={user} />

But there are situations where the avatar appears in a link <a>. Since inline elements cannot contain block elements, the avatar must not be a div but a span. So we use it this way:

<a href={...}>
  <UserAvatar user={user} component="span" />
</a>

That's where the compiler complains. Note that I have simplified the UserAvatar component. The real component deals with background colors, a set of sizes and a default icon when user is not provided.

EDIT: I'm having a similar issue that I believe is related. In the following code, the compiler complains about secondaryTypographyProps.component:

"Object literal may only specify known properties, and 'component' does not exist in type 'Partial<OverrideProps<TypographyTypeMap<{}, "span">, "span">>'".

<ListItemText
  secondary={`${user.firstname} ${user.lastname}`}
  secondaryTypographyProps={{ component: 'span' }}
/>
vdh commented 4 years ago

@Zhouzi I think this use of generics ought to solve your issue:

import React, { ElementType, Component } from 'react';
import Avatar, { AvatarProps } from '@material-ui/core/Avatar';

type UserAvatarProps<
  D extends ElementType = 'div',
  P = {}
> = {
  user: {
    firstname: string;
    lastname: string;
  };
} & AvatarProps<D, P>;

export default class UserAvatar<
  D extends ElementType = 'div',
  P = {}
> extends Component<UserAvatarProps<D, P>> {
  public render() {
    const { user, ...avatarProps } = this.props;

    return (
      <Avatar {...avatarProps}>
        {`${user.firstname.charAt(0)}${user.lastname.charAt(0)}`}
      </Avatar>
    );
  }
}

Which can also be a functional component:

/* … */

export default function UserAvatar<
  D extends ElementType = 'div',
  P = {}
>({
  user,
  ...avatarProps
}: UserAvatarProps<D, P>) {
  return (
    <Avatar {...avatarProps}>
      {`${user.firstname.charAt(0)}${user.lastname.charAt(0)}`}
    </Avatar>
  );
}
tqjoram commented 3 years ago

Hi, still fairly new to React and MaterialUI I seem unable to find the appropriate solution on this topic. Whats the suggested way to achieve the following in Typescript:

As of the code below, IntelliJ is complaining with

Property 'component' does not exist on type 'IntrinsicAttributes & Pick<Pick<OverrideProps<ButtonTypeMap<{}, "button">, "button">, "form" | "slot" | "style" | "title" | ... 279 more ... | "startIcon">, "form" | ... 281 more ... | "startIcon"> & RefAttributes<...>'
import SendIcon from '@material-ui/icons/Send'

export const SaveButton = React.forwardRef<any, Omit<ButtonProps, 'variant' | 'color' | 'endIcon'>>((props, ref) => (
  <Button ref={ref} variant="contained" color="primary" endIcon={<SendIcon/>} {...props} />
))

and

import { Link } from 'react-router-dom'
import { SaveButton } from '../SaveButton'

<SaveButton
    component={Link}
    to={`/step2`}
    disabled={selectedItems.length === 0}
    >
    Save me!
</SaveButton>

Thanks for your help.

kelly-tock commented 3 years ago

We ended up creating a wrapper and re-adding the ones we needed to a custom interface that extends from ButtonProps.

tqjoram commented 3 years ago

We ended up creating a wrapper and re-adding the ones we needed to a custom interface that extends from ButtonProps.

Thx, I'm casting mySaveButton as follows, which does the trick.

export const SaveButton = React.forwardRef<any, Omit<ButtonProps, 'variant' | 'color' | 'endIcon'>>((props, ref) => (
  <Button ref={ref} variant="contained" color="primary" endIcon={<SendIcon/>} {...props} />
)) as ExtendButtonBase<ButtonTypeMap>
paynecodes commented 3 years ago

@kelly-tock Can you elaborate on your solution, please? I'm curious to see how you were able to maintain component prop behavior.

kelly-tock commented 3 years ago
import { Button as MuiButton } from '@material-ui/core';
import { ButtonProps as MuiButtonProps } from '@material-ui/core/Button';

export interface ButtonProps extends Pick<MuiButtonProps, Exclude<keyof MuiButtonProps, 'color'>> {
  color?: ButtonColor;
  component?: React.ElementType;
  to?: string;
  target?: string;
}

const Button: React.FunctionComponent<ButtonProps> = React.forwardRef(

we overrode color to our needs, then added things as we came across them.

sparshsamir1993 commented 3 years ago
const renderAppointmentDates = () => {
    const dates = generateAppointmentDates();
    return dates.map((date) => {
      return (
        <MenuItem
          value={date}
          component={MyButton}
          key={date}
          children={date}
        />
      );
    });
  };

...
..
<Field
    name="Select Day"
    component={MaterialSelect}
    {...{
      initialValue: props.initialValues ? props.initialValues : "",
    }}
    >
    <MenuItem value="" component={MyButton}></MenuItem>
    {renderAppointmentDates()}
</Field>

I am facing the same problem here. Error is

 The `component` prop provided to ButtonBase is invalid.
Please make sure the children prop is rendered in this custom component.
tqjoram commented 3 years ago
import { Button as MuiButton } from '@material-ui/core';
import { ButtonProps as MuiButtonProps } from '@material-ui/core/Button';

export interface ButtonProps extends Pick<MuiButtonProps, Exclude<keyof MuiButtonProps, 'color'>> {
  color?: ButtonColor;
  component?: React.ElementType;
  to?: string;
  target?: string;
}

const Button: React.FunctionComponent<ButtonProps> = React.forwardRef(

we overrode color to our needs, then added things as we came across them.

May we see the implementation of const Button: React.FunctionComponent<ButtonProps> = React.forwardRef(? Do you wrap a Router.Link around it if the to prop is set? Do you need to cast the return value?

Still confused if its better to use

<Link to="/" component={SaveButton} />

or

<SaveButton to="/" component={Link} />

I appreciate your suggestions.

kelly-tock commented 3 years ago

We have a component called Anchor, which I can't share the full implementation of, but essentially if we want a button to be a link we do

<Button component={Anchor}

and Anchor for us has logic to determine if we're rendering a react router link or a regular link tag.

and for the Link component in material ui, we default the component prop to be our Anchor

we have a few cases where we want the page to just refresh, so there is some logic around that internal to Anchor. Hope that helps.

roykingtree commented 3 years ago

Will this ever be fixed? I can't even replicate any of the examples in the docs.

oliviertassinari commented 3 years ago

@roykingtree do you have a reproduction?

rosskevin commented 3 years ago

Here you go @oliviertassinari, a PR with broken tests https://github.com/mui-org/material-ui/pull/16315 closed in favor of existing issues. It doesn't cover all the cases here but it covers the most basic, fundamental use. This has been annoying for many typescript users for a fair amount of time.

I understand there are tradeoffs to be made, but this issue and related issues including https://github.com/mui-org/material-ui/issues/16245 have long been open and fail basic use.

As far as trade-offs are concerned, the frequency which this comes up is high and has been open a long time. I don't think the a trade-off should be made where basic separation of property declaration vs use is painful and requires extensive research and effort to use properly.

Perhaps my knowledge is too dated though? Perhaps it is totally different now and I'm wrong, but given the issues are still open, it seems not.

oliviertassinari commented 3 years ago

@rosskevin Thanks. I have moved it to codesandbox: https://codesandbox.io/s/immutable-fast-s9ycq?file=/src/App.tsx.

import * as React from "react";
import Button, { ButtonProps } from "@material-ui/core/Button";

const ButtonTest = (props: ButtonProps) => {
  return <Button {...props} />;
};

export default function App() {
  return (
    <div>
      <ButtonTest component="span" role="checkbox" aria-checked={false} />
// --------------^ Property 'component' does not exist on type 'IntrinsicAttributes &
    </div>
  );
}

This sounds like common problem UI libraries have. How are other libraries handling it?


Edit, solution using https://next.material-ui.com/guides/typescript/#usage-of-component-prop: https://codesandbox.io/s/stupefied-sea-fcvco?file=/src/App.tsx:0-348

import * as React from "react";
import Button, { ButtonProps } from "@material-ui/core/Button";

const ButtonTest = (props: ButtonProps<"span">) => {
  return <Button component="span" {...props} />;
};

export default function App() {
  return (
    <div className="App">
      <ButtonTest role="checkbox" aria-checked={false} />
    </div>
  );
}