Skip to content
Composition

Composition

Material-UI tries to make composition as easy as possible.

Wrapping components

In order to provide the maximum flexibility and performance, we need a way to know the nature of the child elements a component receives. To solve this problem we tag some of our components when needed with a muiName static property.

You may, however, need to wrap a component in order to enhance it, which can conflict with the muiName solution. If you wrap a component verify if that component has this static property set.

If you encounter this issue, you need to use the same tag for your wrapping component that is used with the wrapped component. In addition, you should forward the properties, as the parent component may need to control the wrapped components props.

Let's see an example:

const WrappedIcon = props => <Icon {...props} />;
WrappedIcon.muiName = Icon.muiName;

Component property

Material-UI allows you to change the root node that will be rendered via a property called component.

How does it work?

The component will render like this:

return React.createElement(this.props.component, props)

For example, by default a List component will render a <ul> element. This can be changed by passing a React component to the component property. The following example will render the List component with a <nav> element as root node instead:

<List component="nav">
  <ListItem>
    <ListItemText primary="Trash" />
  </ListItem>
  <ListItem>
    <ListItemText primary="Spam" />
  </ListItem>
</List>

This pattern is very powerful and allows for great flexibility, as well as a way to interoperate with other libraries, such as react-router or your favorite forms library. But it also comes with a small caveat!

Caveat with inlining

Using an inline function as an argument for the component property may result in unexpected unmounting, since you pass a new component to the component property every time React renders. For instance, if you want to create a custom ListItem that acts as a link, you could do the following:

import { Link } from 'react-router-dom';

const ListItemLink = ({ icon, primary, secondary, to }) => (
  <li>
    <ListItem button component={props => <Link to={to} {...props} />}>
      {icon && <ListItemIcon>{icon}</ListItemIcon>}
      <ListItemText inset primary={primary} secondary={secondary} />
    </ListItem>
  </li>
);

⚠️ However, since we are using an inline function to change the rendered component, React will unmount the link every time ListItemLink is rendered. Not only will React update the DOM unnecessarily, the ripple effect of the ListItem will also not work correctly.

The solution is simple: avoid inline functions and pass a static component to the component property instead. Let's change our ListItemLink to the following:

import { Link as RouterLink } from 'react-router-dom';

class ListItemLink extends React.Component {
  renderLink = React.forwardRef((itemProps, ref) => (
    // with react-router-dom@^5.0.0 use `ref` instead of `innerRef`
    <RouterLink to={this.props.to} {...itemProps} innerRef={ref} />
  ));

  render() {
    const { icon, primary, secondary, to } = this.props;
    return (
      <li>
        <ListItem button component={this.renderLink}>
          {icon && <ListItemIcon>{icon}</ListItemIcon>}
          <ListItemText inset primary={primary} secondary={secondary} />
        </ListItem>
      </li>
    );
  }
}

renderLink will now always reference the same component.

Caveat with shorthand

You can take advantage of the properties forwarding to simplify the code. In this example, we don't create any intermediary component:

import { Link } from 'react-router-dom';

<ListItem button component={Link} to="/">

⚠️ However, this strategy suffers from a little limitation: properties collision. The component providing the component property (e.g. ListItem) might not forward all its properties to the root element (e.g. dense).

React Router Demo

Here is a demo with React Router DOM:

Current route: /drafts

With TypeScript

You can find the details in the TypeScript guide.

Caveat with refs

This section covers caveats when using a custom component as children or for the component prop.

Some of the components need access to the DOM node. This was previously possible by using ReactDOM.findDOMNode. This function is deprecated in favor of ref and ref forwarding. However, only the following component types can be given a ref:

If you don't use one of the above types when using your components in conjunction with Material-UI, you might see a warning from React in your console similar to:

Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

Be aware that you will still get this warning for lazy and memo components if their wrapped component can't hold a ref.

In some instances we issue an additional warning to help debugging, similar to:

Invalid prop component supplied to ComponentName. Expected an element type that can hold a ref.

We will only cover the two most common use cases. For more information see this section in the official React docs.

- const MyButton = props => <div role="button" {...props} />;
+ const MyButton = React.forwardRef((props, ref) => <div role="button" {...props} ref={ref} />);
<Button component={MyButton} />;
- const SomeContent = props => <div {...props}>Hello, World!</div>;
+ const SomeContent = React.forwardRef((props, ref) => <div {...props} ref={ref}>Hello, World!</div>);
<Tooltip title="Hello, again."><SomeContent /></Tooltip>;

To find out if the Material-UI component you're using has this requirement, check out the the props API documentation for that component. If you need to forward refs the description will link to this section.

Caveat with StrictMode or unstable_ConcurrentMode

If you use class components for the cases described above you will still see warnings in React.StrictMode and React.unstable_ConcurrentMode. We use ReactDOM.findDOMNode internally for backwards compatibility. You can use React.forwardRef and a designated prop in your class component to forward the ref to a DOM component. Doing so should not trigger any more warnings related to the deprecation of ReactDOM.findDOMNode.

class Component extends React.Component {
  render() {
-   const { props } = this;
+   const { forwardedRef, ...props } = this.props;
    return <div {...props} ref={forwardedRef} />;
  }
}

-export default Component;
+export default React.forwardRef((props, ref) => <Component {...props} forwardedRef={ref} />);