Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[blog] Making customizable components #33183

Merged
merged 18 commits into from Aug 25, 2022
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/pages/blog/making-customizable-components.js
@@ -0,0 +1,7 @@
import * as React from 'react';
import TopLayoutBlog from 'docs/src/modules/components/TopLayoutBlog';
import { docs } from './making-customizable-components.md?@mui/markdown';

export default function Page() {
return <TopLayoutBlog docs={docs} />;
}
286 changes: 286 additions & 0 deletions docs/pages/blog/making-customizable-components.md
@@ -0,0 +1,286 @@
---
title: 'Making customizable components'
description: The use case of the data grid
date: 2022-06-23T00:00:00.000Z
authors: ['alexfauquette']
tags: ['MUI X']
---

MUI's components are used by hundreds of thousands of developers worldwide, encompassing the full range of implementation from minor side projects to massive company websites.

This variety of users presents a dilemma for us maintainers: hobbyists working on side projects want fully built components that work right out of the box, so they can focus on the application logic; many larger companies, by contrast, want to be able to fully customize components to respect their brand design.

Managing these contradictory needs only becomes more difficult as component complexity increases.

This article reviews several different approaches that a developer might take to customize UI components, as well as the various tradeoffs associated with each method.
Along the way, we’ll explore how these tradeoffs ultimately led to the solution that we’ve settled on for customizing MUI components.

## Style modification

(Already sold on using style libraries? Don’t hesitate to skip this section and move on to [Logic modification](#logic-modification).)

### Good old CSS

Let's start with the easiest part: modifying the style.
This will necessarily involve CSS—especially the notion of [specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity), which says that if an element is targeted by two CSS selectors, the browser will apply the more specific one.
Usually this means that the selector with more classes applied to it is more specific and therefore takes precedence.

For example, if we look at the Material UI `Switch` component, we have multiple subcomponents that we could expect to modify.
samuelsycamore marked this conversation as resolved.
Show resolved Hide resolved
For each of them, we assign a specific CSS class:

<img src="/static/blog/making-customizable-components/switchHighlighted.png" style="width: 532px; margin-top: 16px; margin-bottom: 16px;" alt="Switch component with highlighted sub components" />

Notice that each element is styled using only one CSS class—the thumb style, for example, is applied with the `css-jsexje-MuiSwitch-thumb` class, so any CSS selector that includes more than one class will override its style.

I’m not a designer, so I made an ugly switch example using only CSS—
you can play around with it in [CodeSandbox](https://codesandbox.io/s/fast-http-kv85p5?file=/src/App.js):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the name of this element bewteen "CSS" and "you"?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is every editor's favorite punctuation mark—the em dash! 😁


<img src="/static/blog/making-customizable-components/uglySwitches.png" style="width: 200px; margin-top: 16px; margin-bottom: 8px;" alt="Switch customized with CSS" />

```jsx
<Switch className="uglySwitch" />
```

samuelsycamore marked this conversation as resolved.
Show resolved Hide resolved
```css
/* two classes are more specific than the default single class selector */
.uglySwitch .MuiSwitch-thumb {
background-color: 'green';
}
.uglySwitch .MuiTouchRipple-root {
border: 'solid red 2px';
}
.uglySwitch .MuiSwitch-track {
background-color: 'orange';
opacity: 1;
}
```

### Let JS generate the CSS
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section is super short because modifying style is well explained in the doc, it does not require a blog article.

It's here to remind the reader to use JS libraries to simplify her life


Maybe you don’t want to spend your time switching between CSS and JavaScript files, or writing long and cluttered stylesheets.
To avoid these problems you can integrate the style directly into your JS code. 🎉

Because the level of customization varies across projects, MUI has added multiple ways to customize components.
If you want more information on this topic, you can have a look at the [documentation](https://mui.com/material-ui/customization/how-to-customize/).

## Logic modification

Styling is not the only thing you need to customize.
You may have to modify the logic, which will have to be handled by the component itself.

### Add a prop

Consider a component that lets users rate a product.
Going from 1 to 5 stars is good.
But maybe you need to go up to 10.
To do so we can add a `max` prop and render as many stars as the value of `max`.

That works well enough for a simple UI element, but what happens when the component in question has many more moving parts?

### It’s never that simple

Let’s play with a slightly more complex component: the [`DataGrid`](https://mui.com/x/react-data-grid/).
This component allows you to manage data by applying sorting, filtering, editing, exporting, and many other -INGs.

Consider the following feature request: And together we will try to answer an open issue I get.
alexfauquette marked this conversation as resolved.
Show resolved Hide resolved

:::info
[DataGrid] Sorting column options by alphabetical order

When I open the filterPanel, the input listing the names of the columns is sorted according to column position.
I would like to sort it by alphabetical order.
<img src="/static/blog/making-customizable-components/issueScreenshot.png" style="width: 796px; margin-top: 16px; margin-bottom: 8px;" alt="Screen shot of the filter panel with column selector un sorted" />
:::

This request makes sense.
When you have a lot of columns, sorting them can make it easier to browse the list.
But how should we implement this kind of customization?

Adding a prop called `filterPanelColumnInputSortingStrategy` could work, but please don’t do that.
It just doesn’t scale.
There are too many different props that developers might need to modify.
You will end up with API documentation so long that it will take an eternity to scroll to the end—so nobody will read it.

<img src="/static/blog/making-customizable-components/bruce.gif" style="width: 500px; margin-top: 16px; margin-bottom: 8px;" alt="Your user opening the list of props" />

Here are a few better solutions that can scale more efficiently with complex components.

### Other solutions

#### Don’t bother with components

Passing all the parameters as props of a single component does not work.
So just don’t create components.

It’s not a joke—that’s the approach of headless libraries such as react-table.
Instead of providing working components, they provide hooks for managing the features and let developers build their components on top of it.

If you’re willing to start from scratch, that's a nice approach.
Use one hook to manage filtering, another one to manage the sorting, and then build your UI using returned values.

This approach can scale because you can scope parameters to individual features.
The filtering hooks will only take into account parameters impacting the filtering, and so on—so you can split your code feature by feature.

But because this is a fully custom approach, it will take the most amount of work relative to all other options to construct a functional UI.
If your main priority is to get up and running quickly, then this may not be a viable solution.

#### Subdivide your components

Another approach I like is to provide subcomponents.
This is what we do for MUI Core components such as the [Menu](https://mui.com/material-ui/react-menu/).

This is also the approach used by [react-admin](https://marmelab.com/react-admin/) to provide a customizable administration interface.
Here is their quick start example.
The idea is to put the `Admin` component at the root level of the app.
It is a provider that’s responsible for managing all data fetching and passing that data back to components.

The second important component is `ListGuesser` which defines how the data should be displayed.

```jsx
import * as React from 'react';
import { Admin, Resource, ListGuesser } from 'react-admin';
import simpleRestProvider from 'ra-data-simple-rest';

const dataProvider = simpleRestProvider('https://domain.tld/api');

export default function App() {
return (
<Admin dataProvider={dataProvider}>
<Resource name="users" list={ListGuesser} />
</Admin>
);
}
```

If you’re unhappy with the rendering of the `ListGuesser`, then you can define your own components by reusing smaller components.
If you’re unhappy with the smaller components, you can replace them with custom ones, and so on.

So you start with a component that’s fully functional right out of the box, and you can rewrite any of its constituent elements as needed.

This approach has **one major advantage**: it gives you a lot of flexibility.
For example, you can easily modify the order of the components and their parent/children relationships.

This approach has also **one major drawback**:it gives you a lot of flexibility.
For example, you can easily modify the order of components in a bad way.
_The more freedom, the more bugs_.

##### Drawback example

To show you how easy it is to make a mistake using this technique, here is a personal example involving Material UI components.

I recently tried to wrap a `TextField` component in a `FormControl`, and was frustrated when it didn’t work.
But the reason why is quite simple: the `TextField` component is itself composed of an input wrapped inside of a `FormControl`, and neither TypeScript nor `console.error` messages could warn me that my rendered markup was redundant and broken.

```jsx
<FormControl>
<TextField>
</FormControl>
alexfauquette marked this conversation as resolved.
Show resolved Hide resolved

// Equivalent to

<FormControl>
<FormControl>
<Label />
<Input />
<HelperText />
</FormControl>
</FormControl>
```

This trade-off makes sense for react-admin, which is used for building complete websites.
Their users need complete freedom when it comes to rearranging components and introducing new components anywhere.

But MUI’s products exist at a lower level.
We focus on the building blocks, not the entire website (but we do have [templates](https://mui.com/templates/) for that 😉).
So that’s not the approach we took for the `DataGrid`.

#### Keep a single component

For `DataGrid`, we wanted to make it as simple as possible to add the DataGrid to your application, so we stuck with the individual component structure — e.g., to create a new data grid, all you need is `<DataGrid rows={...} columns={...} />`.

To customize this single component, we use what we call the slot strategy.

## The slot solution

Now we are back to the original problem, how to allow deep customization on a single component. Let’s look at how we use slots to balance the freedom to customize with the need to avoid building from scratch.

### Overriding default components

First let’s modify the appearance of the grid.
For color, spacing, and other basic properties you have CSS, but not everything is style related.

Here is a view of the grid with the filter panel open.
There’s an x icon on the left side of the panel for deleting the current filter.

Say you want to replace this x with a trash icon.
You can’t do it with CSS—you need DOM modification to replace the SVG icon.

<img src="/static/blog/making-customizable-components/FilterPanel.png" style="width: 796px; margin-top: 16px; margin-bottom: 16px;" alt="Default view of filter panel" />

To manage such a scenario, the `<DataGrid />` has a prop called `components`.
This prop lets you replace some internal grid components with your custom ones.
In this case, the component to replace is the `FilterPanelDeleteIcon`, which becomes `DeleteIcon`.

```jsx
<DataGrid {...data} components={{ FilterPanelDeleteIcon: DeleteIcon }} />
```

That’s all it takes.
For every icon, there is a corresponding key in `components` that we call a slot.
If you provide a component to a slot, your component will be used instead of the default one.

### Passing props

Slots make it simple to override small components.
But our initial goal was to modify the order of the column selector.
We can’t provide a slot to override this selector alone, or else we would need to provide one for all of the inputs and buttons, which are too numerous to keep track of.

We could use a slot to override the filter panel.
We provide this slot just in case you need a fully customized panel.
But honestly, who wants to rewrite an entire component for a simple sorting options?

What would be nice is to have a prop called `columnsSort` that lets you apply ascending and descending order on the column selector.
By adding this prop to the default filter panel, we can derive a customized filter panel from the default like this:

```jsx
import { GridFilterPanel } from '@mui/x-data-grid';

const CustomFilterPanel = (props) => (
<GridFilterPanel {...props} columnsSort="asc" />
);
```

But this strategy of adding props to customize components is a bit verbose.
So we added a way to pass props to an existing component using `componentsProps`.
You can pass props to every slot on `components` using `componentsProps`.
Here’s how to pass `columnsSort='asc'` to the filter panel slot:

```jsx
<DataGrid
componentsProps={{
filterPanel: {
columnsSort: 'asc',
},
}}
/>
```

This way of passing props is nice, because it scopes them.
Props for the filter panel are together in `componentProps.filterPanel`.
And the same goes for the toolbar, the column menu, and all other components.

It also works pretty well with TypeScript autocomplete, because none of the slots have very many props.
So as soon as you’ve specified which slot you want to pass props to, your IDE will make good recommendations.

## What should I use?

If your goal is to customize the style, please don’t start from scratch—use libraries to manage your CSS.
By using good standards on class management, you should be able to provide style which is easy to override.

You should add props to the component if they impact the entire component.
For example disabling filtering will impact the all grid.

You should add slots to override icons, because it is frequent to want to replace them by another ones, so it should be easy to do.

Slots should also be added when your component is somewhat independent from the main one.
For example a grid can exist without its filter panel, or without its toolbar.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.