Custom component that extend MUI SvgIcon cannot accept `component` prop [duplicate] - reactjs

Can anyone please explain how Material-UI extends the props of its Button component with the props of my component if I pass a specific component in the component prop?
interface MyLinkProps extends ButtonBaseProps {
someRandomProp: string
}
const MyLink: React.FC<MyLinkProps> = () => {
return <div></div>
}
<Button component={MyLink} someRandomProp="random">Something</Button>
As in this case, the Button component is now aware of the someRandomProp prop that belongs to my component; which is being passed to component prop on the Button component.
I would like to achieve the same effect. I have an Image component which has a prop component which I would like to infer the props of the component that is being passed.
For example, if there is something like:
<MyImage component={NextImage} {...propsOfNextImage} />
Basically, I would like MyImage to auto-detect and extend the props of NextImage.

You can see the type definition of OverridableComponent here, this one is responsible for merging the props of the overridable component with the original props of the component.
For reference, see how it's used in a MUI component here. The first generic type parameter is a type with the following properties:
props: The original props of your component before being merged with the props of the overridable component.
defaultComponent: The default root component if you don't provide any.
import { OverridableComponent } from '#mui/material/OverridableComponent';
interface MyImageProps {
src: string;
myCustomProps?: string;
}
interface MyImageTypeMap {
props: MyImageProps;
defaultComponent: 'img';
}
const MyImage: OverridableComponent<MyImageTypeMap> = (props) => {
const RootComponent = props.component || 'img';
return (
<RootComponent src={props.src} {...props}>
{props.children}
</RootComponent>
);
};
Usage
{/* normal component with MyImageProps. The root component is an img element */}
<MyImage src={src} />
{/* component with MyImageProps & ButtonProps. The root component is a Button*/}
<MyImage src={src} component={Button} variant="contained">
I am actually a button in disguise
</MyImage>
Live Demo

Related

How does Material-UI Button component infer the props for the component passed to `component` prop?

Can anyone please explain how Material-UI extends the props of its Button component with the props of my component if I pass a specific component in the component prop?
interface MyLinkProps extends ButtonBaseProps {
someRandomProp: string
}
const MyLink: React.FC<MyLinkProps> = () => {
return <div></div>
}
<Button component={MyLink} someRandomProp="random">Something</Button>
As in this case, the Button component is now aware of the someRandomProp prop that belongs to my component; which is being passed to component prop on the Button component.
I would like to achieve the same effect. I have an Image component which has a prop component which I would like to infer the props of the component that is being passed.
For example, if there is something like:
<MyImage component={NextImage} {...propsOfNextImage} />
Basically, I would like MyImage to auto-detect and extend the props of NextImage.
You can see the type definition of OverridableComponent here, this one is responsible for merging the props of the overridable component with the original props of the component.
For reference, see how it's used in a MUI component here. The first generic type parameter is a type with the following properties:
props: The original props of your component before being merged with the props of the overridable component.
defaultComponent: The default root component if you don't provide any.
import { OverridableComponent } from '#mui/material/OverridableComponent';
interface MyImageProps {
src: string;
myCustomProps?: string;
}
interface MyImageTypeMap {
props: MyImageProps;
defaultComponent: 'img';
}
const MyImage: OverridableComponent<MyImageTypeMap> = (props) => {
const RootComponent = props.component || 'img';
return (
<RootComponent src={props.src} {...props}>
{props.children}
</RootComponent>
);
};
Usage
{/* normal component with MyImageProps. The root component is an img element */}
<MyImage src={src} />
{/* component with MyImageProps & ButtonProps. The root component is a Button*/}
<MyImage src={src} component={Button} variant="contained">
I am actually a button in disguise
</MyImage>
Live Demo

Spreading a component's props across two different child components in React

I am trying to create a custom email input component (for a form) that wraps a Material-UI TextField component inside a custom gridding component that I made. Ideally, I would like to be able to pass any TextField prop I want into this component and have it applied to the inner TextField component by spreading the props, but I also would like to be able to pass any props for the custom gridding component and apply them to the grid component also via spreading.
Example (where variant is a TextField prop and width is a CustomGrid prop):
// CustomEmailField.tsx
...
export const CustomEmailField: React.FC<TextFieldProps & CustomGridProps> = (props) => {
return(
<CustomGrid {...props as CustomGridProps}>
<TextField {...props as TextFieldProps} />
</CustomGrid>
);
};
// index.tsx
...
const App = () => {
return(
<>
<h1>Enter your email</h1>
<CustomEmailField variant={'outlined'} width={2} />
</>
);
};
However, when spreading the props for the gridding component, I get an error message saying that the TextField props (variant in this example) do not exist for this gridding component, and likewise that the gridding component's props (width in this example) don't exist for the TextField component.
What would be a good way to solve this issue so that I can still have flexibility over the props I pass in to each (child) component without having to hardcode what props can be accepted by the email (parent) component?
Just create a new props type.
export type CustomEmailFieldProps = {
textField: TextFieldProps;
customGrid: CustomGridProps;
}
export const CustomEmailField: React.FC<CustomEmailFieldProps> = ({textField, customGrid}) => {
return(
<CustomGrid {...customGrid}>
<TextField {...textField} />
</CustomGrid>
);
};
To use just create an object of the props you want to pass to each.
// index.tsx
...
const App = () => {
return(
<>
<h1>Enter your email</h1>
<CustomEmailField textField={{variant: 'outlined'}} customGrid={{width: 2}} />
</>
);
};

Infer type of props based on component passed also as prop

Is it possible to infer correct types for props from an unknown component passed also as a prop?
In case of known component (that exists in current file) I can get props:
type ButtonProps = React.ComponentProps<typeof Button>;
But if I want to create a generic component Box that accepts a component in as prop and the component's props in props prop. The component can add some default props, have some behavior, it doesn't matter. Basically its similar to higher-order components, but its dynamic.
import React from "react";
export interface BoxProps<TComponent> {
as?: TComponent;
props?: SomehowInfer<TComponent>; // is it possible?
}
export function Box({ as: Component, props }: BoxProps) {
// Note: it doesn't have to be typed within the Box (I can pass anything, I can control it)
return <Component className="box" title="This is Box!" {...props} />;
}
function MyButton(props: {onClick: () => void}) {
return <button className="my-button" {...props} />;
}
// usage:
function Example() {
// I want here the props to be typed based on what I pass to as. Without using typeof or explicitly passing the generic type.
return (
<div>
<Box
as={MyButton}
props={{
onClick: () => {
console.log("clicked");
}
}}
>
Click me.
</Box>
</div>
);
}
requirements:
must work without passing the generic type (is it possible?), because it would be used almost everywhere
must work with user-defined components (React.ComponentType<Props>)
would be great if it worked also with react html elements (a, button, link, ...they have different props), but not necessary
You can use the predefined react type ComponentProps to extract the prop types from a component type.
import React from "react";
export type BoxProps<TComponent extends React.ComponentType<any>> = {
as: TComponent;
props: React.ComponentProps<TComponent>;
}
export function Box<TComponent extends React.ComponentType<any>>({ as: Component, props }: BoxProps<TComponent>) {
return <div className="box" title="This is Box!">
<Component {...props} />;
</div>
}
function MyButton(props: {onClick: () => void}) {
return <button className="my-button" {...props} />;
}
// usage:
function Example() {
// I want here the props to be typed based on what I pass to as. Without using typeof or explicitly passing the generic type.
return (
<div>
<Box
as={MyButton}
props={{ onClick: () => { } }}
></Box>
</div>
);
}
Playground Link
Depending on you exact use case the solution might vary, but the basic idea is similar. You could for example turn the type around a little bit and take in the props as the type parameter for the BoxProps. That way you can constrain the component props to have some specific properties you can supply inside the Box component:
export type BoxProps<TProps extends {title: string}> = {
as: React.ComponentType<TProps>;
} & {
props: Omit<TProps, 'title'>;
}
export function Box<TProps extends {title: string}>({ as: Component, props }: BoxProps<TProps>) {
return <div className="box" title="This is Box!">
<Component title="Title from box" {...props as TProps} />;
</div>
}
Playground Link
If you want to take in intrinsic tags, you can also add keyof JSX.IntrinsicElements to the TComponent constraint:
export type BoxProps<TComponent extends React.ComponentType<any> | keyof JSX.IntrinsicElements> = {
as: TComponent;
props: React.ComponentProps<TComponent>;
}
export function Box<TComponent extends React.ComponentType<any>| keyof JSX.IntrinsicElements>({ as: Component, props }: BoxProps<TComponent>) {
return <div className="box" title="This is Box!">
<Component {...props} />;
</div>
}
Playground Link

Migration to material-ui v4, I get warning for Modal component

Since upgrade material-ui to v4, I got some warnings with Modal component.
E.g
Failed prop type: Invalid prop children supplied to ForwardRef(Modal).
Function components cannot be given refs.
This is Modal code (warning 1):
import React from 'react';
import Proptypes from 'prop-types';
...
class DetailDialog extends React.Component {
render() {
const { classes, open, onClose } = this.props;
return (
<Dialog
open={open}
>
...
</Dialog>
);
}
}
DetailDialog.propTypes = {
open: Proptypes.bool,
onClose: Proptypes.func,
};
export default DetailDialog;
This is Modal code (warning 2):
import React from 'react';
import PropTypes from 'prop-types';
...
class SelectCatDialog extends React.Component {
render() {
const { open, list, onClose } = this.props;
return (
<Dialog
open={open}
onClose={onClose}
>
...
</Dialog>
)
}
}
SelectCatDialog.propTypes = {
open: PropTypes.bool,
onClose: PropTypes.func,
}
SelectCatDialog.defaultProps = {
list: [],
}
export default SelectCatDialog;
This is call Modal page code:
import React from 'react';
import DetailDialog from './components/DetailDialog';
import SelectProcDialog from './components/SelectProcDialog';
class Index extends React.Component {
render() {
return (
...
<DetailDialog
open={this.state.detailDialog}
entity={this.state.currentDetail}
onClose={this.handleDetailClose.bind(this)}
/>
<SelectProcDialog
open={this.state.catalogDialog}
list={this.props.catalog}
onOk={(value) => this.handleSelectCatalogOk(value)}
onClose={() => this.setState({ catalogDialog: false })}
/>
...
)
}
}
export default Index;
What happened? Working fine in v3 version. Can someone answer?
Since V4, Dialog and Modal children must be able to hold a ref.
The following can hold a ref:
Any Material-UI component
class components i.e. React.Component or React.PureComponent
DOM (or host) components e.g. div or button
React.forwardRef components
React.lazy components
React.memo components
The error declares that you provide function component as a child to modal.
To fix the error, change the function component to something that can hold a ref (e.g class component).
I just faced the exact same issue after migrating from Material-UI v3 to v4.
I totally agree with the accepted answer, nevertheless, I post here another approach, in the case someone would rather keep using a functional component (as I did).
Functional components can't hold a ref as is.
So the way of providing a ref to a child component is actually to forward it.
The React documentation has a well explained example about this, and this issue on the ReactJS Github details it a bit more.
So basically, what could be done here is :
const Transition = React.forwardRef((props, ref) => (
<Slide direction="up" {...props} ref={ref} />
));
to be able to forward the ref through the Transition component, down to the Slide component.
Typescript way
⚠️ Be aware that the previous code will throw a TS2322 error if used in a Typescript project.
This error is described and solved in this Material-UI Github issue.
So, in a Typescript project, the best way (imho) to use a functional component as Transition component for a Material-UI v4 Dialog component, is the following :
import { SlideProps } from '#material-ui/core/Slide';
const Transition = React.forwardRef<unknown, SlideProps>((props, ref) => (
<Slide direction="up" {...props} ref={ref} />
));
And the code above fixes both errors :
Failed prop type: Invalid prop children supplied to ForwardRef(Modal).
[TypeScript] Error: TransitionComponent type not assignable to 'ForwardRefExoticComponent'

React HOC, instanceof, wrapped with?

I use HOC and the compose method from Redux to wrap a component in components. A simple example :
const Textinput = class Textinput extends Component {
...
}
export default compose(
Theme,
Validator,
)(Textinput);
The result rendered by react is :
<theme>
<validator>
<textinput />
Imagine another component with this children :
<Textinput />
<span>Test</span>
<Select />
<br/>
Textinput and Select are wrapped in the Theme/Validator components with HOC. I want to enumerate each element in this component :
const children = React.Children.map(this.props.children, (child) => {
}
But :
how to know if a rendered element is wrapped in a specific component (like Validator or Theme) ?
how to know if the component behind an element is an instanceof Select or Textinput (and not Theme) ?
Another solution is to add a new property on each HOC object like :
Theme.innerInstances = {
instance: WrappedComponent,
innerInstances: WrappedComponent.innerInstances,
};
Validator.innerInstances = {
instance: WrappedComponent,
innerInstances: WrappedComponent.innerInstances,
};

Resources