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

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'

Related

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

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

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

Sending a React.FunctionComponent<React.SVGProps<SVGSVGElement>> as a prop to another component

I'm attempting to import a React functionComponent from an SVG and then send that to another component as a prop to render that svg. With the setup below, this compiles fine, but eventually crashes when trying to render the svg in browser with:
Error: Objects are not valid as a React child (found: object with keys {$$typeof, render}). If you meant to render a collection of children, use an array instead.
Classes below are simplified. But the gist of what I'm trying to do is:
In overlay.tsx:
import { ReactComponent as icon } from "/icon.svg";
import CustomItem from "/customItem";
const Overlay: React.FC<OverlayProps> = () => {
return (
<div>
<CustomItem icon={icon}/>
</div>
);
export default Overlay;
}
and in customItem.tsx:
import React from "react";
export interface CustomItemProps {
icon: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
}
const CustomItem: React.FC<CustomItemProps> = ({icon}) => {
return (
<div>
{icon}
</div>
);
};
export default ApplicationsDropdownItem;
I assume my problem is somewhere around the syntax of {icon}, but I can not for the life of me find out what I'm suppose to use instead.
Answer
The icon you are importing is a component, therefore it must be called to render the JSX.
<Icon {...props}/> (correct) or {Icon(props)} (not recomended)
Since it is a component, you should also name it Icon and not icon.
Take a look at this blog post that explains SVGR.
TL;DR - Best approach for rendering components
A. Call the component in your render method with component syntax <MyComponent/> not MyComponent().
B. Instantiate your component as a variable, and pass that to your render method's JSX block.
More info
#DustInCompetent brought to light the issue of calling a component as a function inside a JSX block.
As explained here and here, that will lead to react not registering a components hooks and lead to state and other problems.
If you are implementing a High Level Component (HOC), then you should not call a component within the render method (return statement in functional components), as this leads to problems for similar registration issues of the component.
import React from "react";
import { ReactComponent as SampleIcon } from "/sample_icon.svg";
export interface CustomItemProps {
Icon: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
}
const CustomItem: React.FC<CustomItemProps> = (props) => {
const Temp = props.Icon as React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
return (
<div>
<Temp/>
</div>
);
};
<CustomItem Icon={SampleIcon}/>
I think you should use <Icon /> instead of {icon} because it's a component.

React Material UI Syntax with Props Explained

I understand the basic of react code such as passing data to child component and passing data back to parent with props. I encountered this code below from React Material UI which is abit confusing. Was hoping to get some explanation.
import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '#material-ui/core/styles';
import CircularProgress from '#material-ui/core/CircularProgress';
import purple from '#material-ui/core/colors/purple';
const styles = theme => ({
progress: {
margin: theme.spacing.unit * 2,
},
});
function CircularIndeterminate(props) {
const { classes } = props;
return (
<div>
<CircularProgress className={classes.progress} />
<CircularProgress className={classes.progress} size={50} />
<CircularProgress className={classes.progress} color="secondary" />
<CircularProgress className={classes.progress} style={{ color: purple[500] }} thickness={7} />
</div>
);
}
CircularIndeterminate.propTypes = {
classes: PropTypes.object.isRequired,
};
export default withStyles(styles)(CircularIndeterminate);
Here is what I understand.
1.The component takes in props so I can pass data down to this child component and before render it will extract the classes property from props.classes.
2.There is a styles function that takes in a theme variable and returns an object progress which has some styling which is used by classes.progress
3.withStyle is some sort of higher order component.
But with this knowledge, this code is abit confusing.
How come the code can do classes.progress, why does classes variable have anything to do with the progress style
What is the purpose of
CircularIndeterminate.propTypes = {
classes: PropTypes.object.isRequired,
};
classes variable that is that is passed in as a prop is an object with progress being one of its key. The classes object might look like
classes = {
progress: 'half'
}
Now the value of classes.progress which in the above classes object is half is assigned as a className prop to the CircularProgress component.
Regarding
CircularIndeterminate.propTypes = {
classes: PropTypes.object.isRequired,
};
The above syntax is to add typechecking capabilities to your component. React has some built-in typechecking abilities. To run typechecking on the props for a component, you can assign the special propTypes property.
In your case, it means that the CircularIndeterminate expects a prop classes which is of type object and is a Required prop.
To read more about PropTypes, please visit the docs

React Transition Group

I have been reading about React Transition Group. 95% of the material talks about CSSTransitionGroup. My understanding is that CSSTransitionGroup just builds off of TransitionGroup which simply provides callback methods that correspond to various animation events.
So I've wrapped my component up in a TransitionGroup element and given it an animation event but it is never fired.
import React, { Component } from "react";
import { TransitionGroup, Transition } from "react-transition-group";
class Test extends Component {
componentWillAppear(cb) {
console.log('componentWillAppear')
cb()
}
render() {
return <div> test </div>
}
}
class App extends Component {
render() {
return (
<TransitionGroup>
<Test />
</TransitionGroup>
)
}
}
export default App;
You can use Transition or CSSTransition without TransitionGroup, but you can't use TransitionGroup without one of the others.
From the react-transition-group docs:
The <TransitionGroup> component manages a set of <Transition> components in a list. Like with the <Transition> component, <TransitionGroup>, is a state machine for managing the mounting and unmounting of components over time.
...As items are removed or added to the TodoList the in prop is toggled automatically by the <TransitionGroup>.
Try changing your Test component's render to something like this:
render() {
return (
<Transition timeout={150}>
{(status) => (
<div className={`fade fade-${status}`}>
test
<div>
)}
</Transition>
)
}

Resources