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?
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.
Please make sure that the Bit binary is installed on your machine:
npx @teambit/bvm install
The first thing we need to do is create a new React Workspace:
Don't forget to replace [Your-Bit-Cloud-Account-Name]
with your Bit Cloud account name.
Then create a new React component:
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:
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>
);
}
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.tsximport 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>
);
Let's write a very simple test:
button.spec.tsximport 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();
});
Next, let's install missing dependencies:
And run the Workspace development server:
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:
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.
To keep things simple, we will just fork the theme component:
Let's take a quick look at the implementation of the theme provider:
theme-provider.tsximport 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>;
}
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.
Once again, we will fork the environment component:
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.tsimport { 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);
The next step will be to override some dependencies:
react-with-mui.main.runtime.tsimport { 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);
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
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:
And that's it!
We can now tag:
And we can publish the component to the world:
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
You can now consume either the environment or the button components from the remote scope.
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.