How to build Material UI components with Bit

ni
nitsan7702 years ago

Material UI is a popular open-source (mui/material-ui0.0K) UI component library that combines Material Design with React. Material UI is great, but did you know that you can also combine it with Bit?

bit with mui

Material UI development with Bit offers many advantages. First of all, you can customize each component and release it as an independent package. Once released, it can be used in any project.

Since each Bit component has documentation, you can easily discover components and extend them to meet your needs. It is possible to extend the button component to create a button that opens a dialog box. We'll call it a dialog-button. The component will now be easily found every time someone searches for it.

With this tutorial, we will show you how to use Material-UI with Bit. A button will be created (I know you'll like it!) that will extend the functionality of a regular MUI button. After that, we will explore how to create a custom environment that runs a theme component to illustrate variations of a button in workspaces UI.

Let's begin!

This guide assumes that you have a bit.cloud account and know how to open a remote scope.

Setting up our Workspace

Please make sure that the Bit binary is installed on your machine:

npx @teambit/bvm install
CopiedCopy

The first thing we need to do is create a new React Workspace:

$bit
Copiedcopy

Don't forget to replace [Your-Bit-Cloud-Account-Name] with your Bit Cloud account name.

Then create a new React component:

$bit
Copiedcopy

Now we finally start coding. 🕺

The implementation of our component is fairly basic. Here, we create a Button component file that renders a Button component from the Material UI library:

button.tsx
import React from 'react';
import {
  Button as MuiButton,
  ButtonProps as MuiButtonProps,
} from '@mui/material';

export type ButtonProps = { message?: string } & MuiButtonProps;

const defaultMessage = 'This is a customised MUI button!';

export function Button({
  message = defaultMessage,
  children,
  className,
  ...rest
}: ButtonProps) {
  function handleClick() {
    alert(message);
  }

  return (
    <MuiButton classes={className} onClick={handleClick} {...rest}>
      {children}
    </MuiButton>
  );
}
CopiedCopy

We customized the native MUI button component by adding an onClick function that displays an alert message of our choice whenever the button is clicked.

Now that the button component has been created, we can now visualize the button in the workspace UI by creating some compositions:

button.composition.tsx
import React from 'react';
import { Button } from './button';

export const MuiButtonVariants = () => {
  const variants = ['contained', 'outlined', 'text'] as const;

  return (
    <div className="vertical">
      {variants.map((v, index) => (
        <>
          <Button key={index} variant={v}>
            MUI Button {v} variant
          </Button>
          <br />
          <br />
        </>
      ))}
    </div>
  );
};

export const MuiButtonColors = () => {
  const colors = [
    'inherit',
    'primary',
    'secondary',
    'success',
    'error',
    'info',
    'warning',
  ] as const;

  return (
    <>
      {colors.map((c, index) => (
        <Button key={index} color={c}>
          MUI Button {c}
        </Button>
      ))}
    </>
  );
};

export const MuiButtonSizes = () => {
  const sizes = ['small', 'medium', 'large'] as const;

  return (
    <div className="vertical">
      {sizes.map((s, index) => (
        <>
          <Button key={index} variant="outlined" size={s}>
            {s} MUI Button
          </Button>
          <br />
          <br />
        </>
      ))}
    </div>
  );
};

export const MuiButtonDisabled = () => (
  <Button variant="contained" disabled>
    Disabled :(
  </Button>
);
CopiedCopy

Let's write a very simple test:

button.spec.tsx
import React from 'react';
import { render } from '@testing-library/react';
import { MuiButtonVariants } from './button.composition';

it('should render with the correct text', () => {
  const { getByText } = render(<MuiButtonVariants />);
  const rendered = getByText('MUI Button contained variant');
  expect(rendered).toBeInTheDocument();
});
CopiedCopy

Next, let's install missing dependencies:

$bit
Copiedcopy

And run the Workspace development server:

$bit
Copiedcopy

Please notice how we install each of these dependencies as a peer dependency. The reason is that we want to make sure that there is always one instance of the MUI library in the Workspace.

In the Workspace UI, we can now see compositions. It looks good:

Workspace ui showing compositions.

You must like it! Images alone aren't enough. You need to feel it. Here are the compositions. Check out the wonderful alert functionality by clicking on them!

So far, everything seems to be going well. We face challenges, however, when we attempt to customize the default MUI theme.

Generating a custom theme

To keep things simple, we will just fork the theme component:

$bit
Copiedcopy

Let's take a quick look at the implementation of the theme provider:

theme-provider.tsx
import React, { ReactNode } from 'react';
import {
  ThemeProvider as MuiThemeProvider,
  createTheme,
  Theme,
} from '@mui/material';

export type ThemeProviderProps = {
  /**
   * a custom MUI theme object. If none is supplied, will use default theme objet
   */
  theme?: Theme;
  children: ReactNode;
};

const defaultTheme = createTheme({
  palette: {
    primary: {
      main: '#FF0000',
    },
    secondary: {
      main: '#FFFF00',
    },
  },
});

export function ThemeProvider({
  theme = defaultTheme,
  children,
}: ThemeProviderProps) {
  return <MuiThemeProvider theme={theme}>{children}</MuiThemeProvider>;
}
CopiedCopy

As you can see, the theme provider provides a default theme, and we can customize it by overriding the theme prop. You can read more about the theming in React and how you can create theme components that are decoupled from your app here.

Having the design tokens in a separate Bit component is also recommended. It is easy to update design tokens when there is a single source of truth.

In the next step, we'll use the theme provider in all of our compositions. The naive approach would be to add a theme provider to every composition, however, this would result in a lot of boilerplate code.

The best solution here is to create a custom environment component that will inject the theme provider into all compositions in the Workspace UI.

Composing a custom environment

Once again, we will fork the environment component:

$bit
Copiedcopy

Let's take a closer look at the customization we have made to the environment component.

The first thing we need to do is register the theme provider in the environment preview file. Once registered, it will be available to every composition in the workspace.

react-with-mui.preview.runtime.ts
import { PreviewRuntime } from '@teambit/preview';
import { ReactAspect, ReactPreview } from '@teambit/react';
import { ThemeProvider } from '@nitsan770/mui.theme.theme-provider'; // <----- Replace this with your theme provider

import { ReactWithMuiAspect } from './react-with-mui.aspect';

export class ReactWithMuiPreviewMain {
  static runtime = PreviewRuntime;

  static dependencies = [ReactAspect];

  static async provider([react]: [ReactPreview]) {
    const reactWithMuiPreviewMain = new ReactWithMuiPreviewMain();

    react.registerProvider([ThemeProvider]); // <----- Register your theme provider here

    return reactWithMuiPreviewMain;
  }
}

ReactWithMuiAspect.addRuntime(ReactWithMuiPreviewMain);
CopiedCopy

The next step will be to override some dependencies:

react-with-mui.main.runtime.ts
import { MainRuntime } from '@teambit/cli';
import { ReactAspect, ReactMain } from '@teambit/react';
import { EnvsAspect, EnvsMain } from '@teambit/envs';
import { ReactWithMuiAspect } from './react-with-mui.aspect';

export class ReactWithMuiMain {
  static slots = [];

  static dependencies = [ReactAspect, EnvsAspect];

  static runtime = MainRuntime;

  static async provider([react, envs]: [ReactMain, EnvsMain]) {
    const templatesReactEnv = envs.compose(react.reactEnv, [
      /**
       * override dependencies here
       */
      react.overrideDependencies({
        dependencies: {
          '@mui/material': '-',
          react: '-',
          'react-dom': '-',
          '@testing-library/react': '-',
          '@nitsan770/mui.theme.theme-provider': '-',
        },
        devDependencies: {
          '@mui/material': '-',
          react: '-',
          'react-dom': '-',
          '@testing-library/react': '-',
          '@nitsan770/mui.theme.theme-provider': '-',
        },
        peerDependencies: {
          '@mui/material': {
            version: '5.2.1',
            resolveFromEnv: true,
          },
          '@testing-library/react': {
            version: '^5.0.6',
            resolveFromEnv: true,
          },
          react: {
            version: '^17.0.2',
            resolveFromEnv: true,
          },
          'react-dom': {
            version: '^17.0.2',
            resolveFromEnv: true,
          },
          '@nitsan770/mui.theme.theme-provider': {
            version: 'latest',
            resolveFromEnv: true,
          },
        },
      }),
    ]);
    envs.registerEnv(templatesReactEnv);
    return new ReactWithMuiMain();
  }
}

ReactWithMuiAspect.addRuntime(ReactWithMuiMain);
CopiedCopy

Here we have overridden the dependencies and added the resolveFromEnv flag to the dependencies. In this way, component compositions can communicate with the context provider - i.e. the environment's preview provider - through context as they will use the env's instance of context. This does not affect how your component is used in a consuming application. Soon, the resolveFromEnv flag will be removed from Bit, and dependencies will be resolved automatically from the env's dependencies.

Lastly, we will add our custom environment to the button component. We can do this with a single command:

bit env set ui/button envs/react-with-mui
CopiedCopy

This sets the environment of ui/button to envs/react-with-mui.

Running the Workspace dev server again, we can now see some colors changing:

custom env

And that's it!

Publishing the Component to the world

We can now tag:

$bit
Copiedcopy

And we can publish the component to the world:

$bit
Copiedcopy

Success!

3 component(s) tagged
(use "bit export [collection]" to push these components to a remote")
(use "bit untag" to unstage versions)

new components
(first version for components)
     > plugins/react-with-mui@0.0.1
     > theme/theme-provider@0.0.1

modified components
     > nitsan770.mui/ui/button@0.0.2

exported the following 3 component(s):
nitsan770.mui/theme/theme-provider
nitsan770.mui/plugins/react-with-mui
nitsan770.mui/ui/button
CopiedCopy

You can now consume either the environment or the button components from the remote scope.

install mui components

Summary

Combining Material UI with Bit is a great way to start building your own composable UI library. We have demonstrated the steps required to get up and running with both technologies and have learned how to create a Bit component that uses the MUI library. We have also seen how to version, build, and test it using Bit. Finally, we looked at how to create a custom theme provider and apply it to your composition in the workspace UI.

We hope you have found this tutorial useful and learned how to use Material UI with Bit.

Please share your beautiful compositions on our Slack channel. See you next time.