Some checks failed
No response / noResponse (push) Has been cancelled
CI / Continuous releases (push) Has been cancelled
CI / test-dev (macos-latest) (push) Has been cancelled
CI / test-dev (ubuntu-latest) (push) Has been cancelled
CI / test-dev (windows-latest) (push) Has been cancelled
Maintenance / main (push) Has been cancelled
Scorecards supply-chain security / Scorecards analysis (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
245 lines
8.9 KiB
Markdown
245 lines
8.9 KiB
Markdown
# Composition
|
||
|
||
<p class="description">Material UI tries to make composition as easy as possible.</p>
|
||
|
||
## Wrapping components
|
||
|
||
To provide maximum flexibility and performance, Material UI needs a way to know the nature of the child elements a component receives.
|
||
To solve this problem, we tag some of the components with a `muiName` static property when needed.
|
||
|
||
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 props, as the parent component may need to control the wrapped components props.
|
||
|
||
Let's see an example:
|
||
|
||
```jsx
|
||
const WrappedIcon = (props) => <Icon {...props} />;
|
||
WrappedIcon.muiName = Icon.muiName;
|
||
```
|
||
|
||
{{"demo": "Composition.js"}}
|
||
|
||
### Forwarding slot props
|
||
|
||
Use the `mergeSlotProps` utility function to merge custom props with the slot props.
|
||
If the arguments are functions then they'll be resolved before merging, and the result from the first argument will override the second.
|
||
|
||
Special properties that merged between the two arguments are listed below:
|
||
|
||
- `className`: values are concatenated rather than overriding one another.
|
||
|
||
In the snippet below, the `custom-tooltip-popper` class is applied to the Tooltip's popper slot.
|
||
|
||
```jsx
|
||
import Tooltip, { TooltipProps } from '@mui/material/Tooltip';
|
||
import { mergeSlotProps } from '@mui/material/utils';
|
||
|
||
export const CustomTooltip = (props: TooltipProps) => {
|
||
const { children, title, sx: sxProps } = props;
|
||
|
||
return (
|
||
<Tooltip
|
||
{...props}
|
||
title={<Box sx={{ p: 4 }}>{title}</Box>}
|
||
slotProps={{
|
||
...props.slotProps,
|
||
popper: mergeSlotProps(props.slotProps?.popper, {
|
||
className: 'custom-tooltip-popper',
|
||
disablePortal: true,
|
||
placement: 'top',
|
||
}),
|
||
}}
|
||
>
|
||
{children}
|
||
</Tooltip>
|
||
);
|
||
};
|
||
```
|
||
|
||
If you added another `className` via the `slotProps` prop on the Custom Tooltip—as shown below—then both would be present on the rendered popper slot:
|
||
|
||
```js
|
||
<CustomTooltip slotProps={{ popper: { className: 'foo' } }} />
|
||
```
|
||
|
||
The popper slot in the original example would now have both classes applied to it, in addition to any others that may be present: `"[…] custom-tooltip-popper foo"`.
|
||
|
||
- `style`: object are shallow merged rather than replacing one another. The style keys from the first argument have higher priority.
|
||
- `sx`: values are concatenated into an array.
|
||
- `^on[A-Z]` event handlers: these functions are composed between the two arguments.
|
||
|
||
```js
|
||
mergeSlotProps(props.slotProps?.popper, {
|
||
onClick: (event) => {}, // composed with the `slotProps?.popper?.onClick`
|
||
createPopper: (popperOptions) => {}, // overridden by the `slotProps?.popper?.createPopper`
|
||
});
|
||
```
|
||
|
||
## Component prop
|
||
|
||
Material UI allows you to change the root element that will be rendered via a prop called `component`.
|
||
|
||
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 renders the `List` component with a `<menu>` element as root element instead:
|
||
|
||
```jsx
|
||
<List component="menu">
|
||
<ListItem>
|
||
<ListItemButton>
|
||
<ListItemText primary="Trash" />
|
||
</ListItemButton>
|
||
</ListItem>
|
||
<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.
|
||
|
||
### Passing other React components
|
||
|
||
You can pass any other React component to `component` prop. For example, you can pass `Link` component from `react-router`:
|
||
|
||
```tsx
|
||
import { Link } from 'react-router';
|
||
import Button from '@mui/material/Button';
|
||
|
||
function Demo() {
|
||
return (
|
||
<Button component={Link} to="/react-router">
|
||
React router link
|
||
</Button>
|
||
);
|
||
}
|
||
```
|
||
|
||
### 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.
|
||
|
||
The examples below use `TypographyProps` but the same will work for any component which has props defined with `OverrideProps`.
|
||
|
||
```ts
|
||
import { TypographyProps } from '@mui/material/Typography';
|
||
|
||
function CustomComponent(props: TypographyProps<'a', { component: 'a' }>) {
|
||
/* ... */
|
||
}
|
||
// ...
|
||
<CustomComponent component="a" />;
|
||
```
|
||
|
||
Now the `CustomComponent` can be used with a `component` prop which should be set to `'a'`.
|
||
In addition, the `CustomComponent` will have all props of a `<a>` HTML element.
|
||
The other props of the `Typography` component will also be present in props of the `CustomComponent`.
|
||
|
||
You can find a code example with the Button and react-router in [these demos](/material-ui/integrations/routing/#component-prop).
|
||
|
||
### Generic
|
||
|
||
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>(
|
||
props: TypographyProps<C, { component?: C }>,
|
||
) {
|
||
/* ... */
|
||
}
|
||
```
|
||
|
||
If the `GenericCustomComponent` is used with a `component` prop provided, it should also have all props required by the provided component.
|
||
|
||
```ts
|
||
function ThirdPartyComponent({ prop1 }: { prop1: string }) {
|
||
/* ... */
|
||
}
|
||
// ...
|
||
<GenericCustomComponent component={ThirdPartyComponent} prop1="some value" />;
|
||
```
|
||
|
||
The `prop1` became required for the `GenericCustomComponent` as the `ThirdPartyComponent` has it as a requirement.
|
||
|
||
Not every component fully supports any component type you pass in.
|
||
If you encounter a component that rejects its `component` props in TypeScript, please open an issue.
|
||
There is an ongoing effort to fix this by making component props generic.
|
||
|
||
## 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`:
|
||
|
||
- Any Material UI component
|
||
- class components, that is `React.Component` or `React.PureComponent`
|
||
- DOM (or host) components, for example `div` or `button`
|
||
- [React.forwardRef components](https://react.dev/reference/react/forwardRef)
|
||
- [React.lazy components](https://react.dev/reference/react/lazy)
|
||
- [React.memo components](https://react.dev/reference/react/memo)
|
||
|
||
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:
|
||
|
||
:::warning
|
||
Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
|
||
:::
|
||
|
||
Note that you will still get this warning for `lazy` and `memo` components if their wrapped component can't hold a ref.
|
||
In some instances, an additional warning is issued to help with debugging, similar to:
|
||
|
||
:::warning
|
||
Invalid prop `component` supplied to `ComponentName`. Expected an element type that can hold a ref.
|
||
:::
|
||
|
||
Only the two most common use cases are covered. For more information see [this section in the official React docs](https://react.dev/reference/react/forwardRef).
|
||
|
||
```diff
|
||
-const MyButton = () => <div role="button" />;
|
||
+const MyButton = React.forwardRef((props, ref) =>
|
||
+ <div role="button" {...props} ref={ref} />);
|
||
|
||
<Button component={MyButton} />;
|
||
```
|
||
|
||
```diff
|
||
-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 props API documentation for that component. If you need to forward refs
|
||
the description will link to this section.
|
||
|
||
### Caveat with StrictMode
|
||
|
||
If you use class components for the cases described above you will still see
|
||
warnings in `React.StrictMode`.
|
||
`ReactDOM.findDOMNode` is used 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`.
|
||
|
||
```diff
|
||
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} />);
|
||
```
|