DEV Community

Olexandr
Olexandr

Posted on • Originally published at techwood.substack.com

Three React MUI commandments

MUI (or Material UI) is a popular React library for building feature-rich UI. There are many great competitors like Ant Design, Shadcn, and so on. However, MUI is still my preferred choice when I need to create a robust enterprise frontend application. The library has a rich set of components, flexible theming capabilities, and strong accessibility out of the box.

Because MUI is powerful, things can often get messy. You’ve probably experienced this frustration firsthand:

Example of the component we’ll be refactoring in this article

I’ve been using the library since version 4 on many of my projects. In this article, I share 3 quick tips I learned the hard way. So you can save hours of refactoring and develop more maintainable and testable components:

Example of refactored component

Okay, let’s get started cleaning up the mess. Here’s the first rule.

Rule number uno - theme it or leave it

Always start exploring customization possibilities from global theme config.

The global theme config centralizes styles for all MUI components, ensuring consistent styling throughout the entire app. That’s why, when you make some adjustment to the Material UI component inside the global theme, it will be applied to all the instances of this component across the app.

Let’s take a button as an example. MUI’s default button is in Material Design 2 style:

Blue rectangular button with white text reading “CONTAINED.”

But what if we want it to match GitHub’s Primer looks:

Green rectangular button with white text reading “Contained.”

For that, we create a new theme config and customize the MuiButton component through it:

theme.tsx

// theme.tsx

import { createTheme } from "@mui/material";

export const theme = createTheme({
  components: {
    // Name of the component
    MuiButton: {
      styleOverrides: {
        root: {
          textTransform: "none",
          boxShadow: "none",
          padding: "0 12px",
          height: 32,
          borderRadius: 6,
          backgroundColor: "rgb(45, 164, 78)",
          border: "1px solid rgba(27, 31, 36, 0.15)",
        },
      },
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

NOTE: Always use colors from the palette. Don’t put them directly as strings into your theme config. I did to avoid a complex setup and focus strictly on the rule.

Don’t forget to use ThemeProvider to inject a custom theme into your application:

main.tsx

// main.tsx

//...
createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <CssBaseline />
    <ThemeProvider theme={theme}>
      <App />
    </ThemeProvider>
  </StrictMode>
);
Enter fullscreen mode Exit fullscreen mode

But what if the theme doesn’t give you enough flexibility? That’s where our second rule comes in.

Rule number two - styled component is what you do

Did you carefully investigate all the design tokens and props the theme provides and couldn’t find the desired one for the component you want to customize? It’s time to take the next step - create a styled component.

Let’s proceed with the button. Assume we want to have 2 shapes: round and square. By default, MUI doesn’t provide us with the shape property, but we can easily add it by creating a new styled component:

button.styled.ts

// button.styled.ts

import {
  Button as MuiButton,
  styled,
  type ButtonProps as MuiButtonProps,
} from "@mui/material";

// Determines whether a given prop should be forwarded to the underlying DOM element.
const shouldForwardProp = (prop: string) => {
  if (!prop || typeof prop !== "string") return false;

  return !prop.trim().startsWith("$");
};

type ButtonProps = MuiButtonProps & {
  $shape?: "round" | "square";
};

export const Button = styled(MuiButton, { shouldForwardProp })<ButtonProps>(
  ({ $shape = "square" }) => {
    const shapeStyle = {
      round: {
        borderRadius: 100,
      },
      square: {},
    };

    return shapeStyle[$shape];
  }
);
Enter fullscreen mode Exit fullscreen mode

Important takeaways from the code above:

  1. Always add dollar $ or another valid sign to the beginning of your custom property. Then use shouldForwardProp utility function to prevent the custom prop from being forwarded to the underlying DOM element. This stops MUI from throwing “Invalid attribute name: shape in the console. Read why unknown DOM props are bad.
  2. Use styled function from @mui/material package, not from @emotion/styled. Emotion has a different syntax and is not aware of all MUI components and internal structures, which Material UI adds on top of Emotion. That’s why you may run into errors or unsupported cases if you try to customize the MUI component directly with Emotion.

Now, make sure you import your styled Button component instead of MUI’s default one everywhere you use it:

App.tsx

// App.tsx

import { Stack } from "@mui/material";

// Import custom styled component, not MUI's one
**import { Button } from "./button.styled";**

function App() {
  return (
    <Stack p={2} spacing={1} sx={{ width: 100 }}>
      <Button variant="contained">Square</Button>
      <Button variant="contained" $shape="round">
        Round
      </Button>
    </Stack>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

The result:

Two green buttons labeled “Square” and “Round,” showing different corner styles.

If the theme doesn’t cover your needs, you may also be tempted to use the sx prop. Here’s why creating a styled component works better.

Why not to use sx?

The SX prop is fine for quickly adding 2-3 theme-aware inline styles. But if you have more complex styling logic, consider using styled components, like I showed above. With this approach, you keep your styling and business logic separate, which improves readability and maintainability.

Sometimes customization goes beyond styling. In that case, it’s time for our third rule.

Style can’t solve? React component's the resolve

Pretend we have a requirement to add a sound effect every time a user clicks on the button. Styled components cannot achieve this, and MUI doesn’t provide any design tokens or capabilities for adding sound either.

When customization involves complex logic, we create a new React wrapper around the styled button:

Button.tsx

// Button.tsx

import useSound from "use-sound";
import buttonSound from "../public/button.mp3";
import type { FC } from "react";
import { type ButtonProps, Button as ButtonStyled } from "./button.styled";

export const Button: FC<ButtonProps> = (props) => {
  const [play] = useSound(buttonSound);

  return <ButtonStyled {...props} onClick={() => play()}></ButtonStyled>;
};
Enter fullscreen mode Exit fullscreen mode

NOTE: To play sound, I use useSound hook. Find sound effects on freesound.org.

Now use the new button component inside your app:

App.tsx

// App.tsx

import { Stack } from "@mui/material";

// Import custom wrapper component, not MUI's or styled one
**import { Button } from "./Button";**

function App() {
  return (
    <Stack p={2} spacing={1} sx={{ width: 100 }}>
      <Button variant="contained">Square</Button>
      <Button variant="contained" $shape="round">
        Round
      </Button>
    </Stack>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Now that you’ve seen all three rules in action, let’s wrap it up.

Conclusion

Congratulations! 🎉

Now you know how to keep your MUI-based design system tidy. You’ve learned:

  1. Why and how to customize components through the global theme wherever it makes sense.
  2. In what cases use styled components.
  3. In what cases you should create a separate React wrapper over the MUI component.

Without this hierarchy, you’ll end up mixing theme overrides, inline sx, and wrapper hacks in random places.

By applying these three easy-to-follow rules, you’re going to build design systems that are easy to maintain, test, and document. Your colleagues and future self will very likely appreciate it 🙂

Want more? 🔥

Check my previous article “React MVVM Architecture with TanStack Router: An Introduction” where I teach good old React MVVM with a modern tech stack.


Creating content like this is not an easy ride, so I hope you’ll hit that like button and drop a comment. As usual, constructive feedback is always welcome!

🔗 Follow me on LinkedIn

📰 Read me on the platform of your choice: Substack, DEV.to

Till next time! 👋

Top comments (0)