Skip to content

Commit

Permalink
[docs][material-ui] Revamp Composition guide (#43266)
Browse files Browse the repository at this point in the history
Signed-off-by: Zeeshan Tamboli <[email protected]>
Co-authored-by: Aarón García Hervás <[email protected]>
  • Loading branch information
ZeeshanTamboli and aarongarciah authored Aug 19, 2024
1 parent a9292a0 commit 63d2dfa
Show file tree
Hide file tree
Showing 4 changed files with 22 additions and 109 deletions.
2 changes: 0 additions & 2 deletions docs/data/material/components/tooltips/tooltips.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,6 @@ const MyComponent = React.forwardRef(function MyComponent(props, ref) {
</Tooltip>;
```

You can find a similar concept in the [wrapping components](/material-ui/guides/composition/#wrapping-components) guide.

If using a class component as a child, you'll also need to ensure that the ref is forwarded to the underlying DOM element. (A ref to the class component itself will not work.)

```jsx
Expand Down
102 changes: 20 additions & 82 deletions docs/data/material/guides/composition/composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,106 +26,44 @@ WrappedIcon.muiName = Icon.muiName;

Material UI allows you to change the root element that will be rendered via a prop called `component`.

### How does it work?

The custom component will be rendered by Material UI like this:

```js
return React.createElement(props.component, props);
```

For example, by default a `List` component will render a `<ul>` element.
This can be changed by passing a [React component](https://react.dev/reference/react/Component) to the `component` prop.
The following example will render the `List` component with a `<nav>` element as root element instead:
The following example renders the `List` component with a `<menu>` element as root element instead:

```jsx
<List component="nav">
<ListItem button>
<ListItemText primary="Trash" />
<List component="menu">
<ListItem>
<ListItemButton>
<ListItemText primary="Trash" />
</ListItemButton>
</ListItem>
<ListItem button>
<ListItemText primary="Spam" />
<ListItem>
<ListItemButton>
<ListItemText primary="Spam" />
</ListItemButton>
</ListItem>
</List>
```

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

### Inlining & caveat

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

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

function ListItemLink(props) {
const { icon, primary, to } = props;

const CustomLink = (props) => <Link to={to} {...props} />;

return (
<li>
<ListItem button component={CustomLink}>
<ListItemIcon>{icon}</ListItemIcon>
<ListItemText primary={primary} />
</ListItem>
</li>
);
}
```

:::warning
However, since we are using an inline function to change the rendered component, React will remount the link every time `ListItemLink` is rendered. Not only will React update the DOM unnecessarily but the state will be lost, for example the ripple effect of the `ListItem` will also not work correctly.
:::
### Passing other React components

The solution is simple: **avoid inline functions and pass a static component to the `component` prop** instead.
Let's change the `ListItemLink` component so `CustomLink` always reference the same component:
You can pass any other React component to `component` prop. For example, you can pass `Link` component from `react-router-dom`:

```tsx
import { Link, LinkProps } from 'react-router-dom';

function ListItemLink(props) {
const { icon, primary, to } = props;

const CustomLink = React.useMemo(
() =>
React.forwardRef<HTMLAnchorElement, Omit<RouterLinkProps, 'to'>>(
function Link(linkProps, ref) {
return <Link ref={ref} to={to} {...linkProps} />;
},
),
[to],
);
import { Link } from 'react-router-dom';
import Button from '@mui/material/Button';

function Demo() {
return (
<li>
<ListItem button component={CustomLink}>
<ListItemIcon>{icon}</ListItemIcon>
<ListItemText primary={primary} />
</ListItem>
</li>
<Button component={Link} to="/react-router">
React router link
</Button>
);
}
```

### Prop forwarding & caveat

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

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

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

:::warning
However, this strategy suffers from a limitation: prop name collisions.
The component receiving the `component` prop (for example ListItem) might intercept the prop (for example to) that is destined to the leaf element (for example Link).
:::

### With TypeScript

To be able to use the `component` prop, the type of the props should be used with type arguments. Otherwise, the `component` prop will not be present.
Expand All @@ -148,9 +86,9 @@ The other props of the `Typography` component will also be present in props of t

You can find a code example with the Button and react-router-dom in [these demos](/material-ui/integrations/routing/#component-prop).

#### Generic
### Generic

It's also possible to have a generic `CustomComponent` which will accept any React component, and HTML elements.
It's also possible to have a generic custom component which accepts any React component, including [built-in components](https://react.dev/reference/react-dom/components/common).

```ts
function GenericCustomComponent<C extends React.ElementType>(
Expand Down
12 changes: 1 addition & 11 deletions docs/data/material/integrations/routing/ListRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,7 @@ import Divider from '@mui/material/Divider';
import InboxIcon from '@mui/icons-material/Inbox';
import DraftsIcon from '@mui/icons-material/Drafts';
import Typography from '@mui/material/Typography';
import {
Link as RouterLink,
Route,
Routes,
MemoryRouter,
useLocation,
} from 'react-router-dom';
import { Link, Route, Routes, MemoryRouter, useLocation } from 'react-router-dom';
import { StaticRouter } from 'react-router-dom/server';

function Router(props) {
Expand All @@ -37,10 +31,6 @@ Router.propTypes = {
children: PropTypes.node,
};

const Link = React.forwardRef(function Link(itemProps, ref) {
return <RouterLink ref={ref} {...itemProps} role={undefined} />;
});

function ListItemLink(props) {
const { icon, primary, to } = props;

Expand Down
15 changes: 1 addition & 14 deletions docs/data/material/integrations/routing/ListRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,7 @@ import Divider from '@mui/material/Divider';
import InboxIcon from '@mui/icons-material/Inbox';
import DraftsIcon from '@mui/icons-material/Drafts';
import Typography from '@mui/material/Typography';
import {
Link as RouterLink,
LinkProps as RouterLinkProps,
Route,
Routes,
MemoryRouter,
useLocation,
} from 'react-router-dom';
import { Link, Route, Routes, MemoryRouter, useLocation } from 'react-router-dom';
import { StaticRouter } from 'react-router-dom/server';

function Router(props: { children?: React.ReactNode }) {
Expand All @@ -39,12 +32,6 @@ interface ListItemLinkProps {
to: string;
}

const Link = React.forwardRef<HTMLAnchorElement, RouterLinkProps>(
function Link(itemProps, ref) {
return <RouterLink ref={ref} {...itemProps} role={undefined} />;
},
);

function ListItemLink(props: ListItemLinkProps) {
const { icon, primary, to } = props;

Expand Down

0 comments on commit 63d2dfa

Please sign in to comment.