Creating a custom material-ui library with Bit components

Leveraging Bit for Seamless Custom Material-UI Library Creation

Victor Yakubu
Bits and Pieces

--

A component library is crucial to the creation of a comprehensive design system solution. It helps in keeping the entire design team on the same page and adds consistency to the products and services created by the organization. Moreover, the availability of design elements significantly reduces the time and effort required to create end products.

In the process of building a design system, one tool stands out for its effectiveness in creating and managing components — Bit. Bit is an open-source platform that allows developers to build, share, and collaborate on independent components. It simplifies the process of creating a design system by enabling individual component versioning, management, and updates.

Why Bit for Building a Component Library?

One of Bit’s key strengths is its ability to simplify component management and versioning. With Bit, each component is versioned separately, allowing developers to manage updates on a component-by-component basis. This granularity in versioning is beneficial when you need to update a specific component without affecting others.

Bit also allows independent component updates. This means that when a component is updated, only that component’s version changes, not the entire library. This feature is particularly useful in a large project where updating the entire library could be cumbersome and potentially risky.

Moreover, Bit plays a crucial role in creating a scalable UI library. It allows developers to build a system of independent components that can be reused across different projects. This means that as your project grows, you can easily add or modify components without worrying about breaking existing functionality. Bit’s approach to component management fosters a collaborative environment where multiple developers can work on different components simultaneously, leading to a more efficient development process.

Design Thinking in Building a Component Library

Choosing the right component library to build on is critical. This choice should be based on factors such as the team’s familiarity with a particular JavaScript framework, the features offered by the library, and its compatibility with the project’s requirements.

Building on a well-established library like Material-UI has its advantages, such as having a comprehensive set of ready-made components and customization options. However, creating a custom library allows for more flexibility and control over the design and functionality of the components.

Building a Custom Material UI Library with Bit

To start the process of creating a custom MUI component library, follow the steps below:

Step 1: Install Bit

First, you need to install Bit on your local machine using this command

npx @teambit/bvm install

Step 2: Create a remote bit scope on Bit cloud

This scope will be used to host your components. To do that, go here.

Step 3: Creating the bit workspace

Next, you’ll need to create a Bit workspace that points to the remote scope. This workspace allows you to manage tour components.

Command:

bit new react mui-tutorial --env teambit.react/react-env --default-scope my-org.my-scope

Replace my-org.my-scope with [YOUR-BIT-USERNAME].material-ui. In my case, it'll be aviatorscodes.material-ui.

If the command runs successfully, you should get this result

Next, open the workspace using a code editor of your choice and run bit start to view your local development server:

Step 4: Creating our custom components

Here the goal is to create a custom LoaderButton component using Material UI. To achieve this, you will need to create Button and Loader components, then combine both components to form a single component.

To create a command in Bit, use this syntax:

bit create react [component-name]

In this case, you will create three react components (Button, Loader, and LoaderButton)

bit create react inputs/button inputs/loader ui/loader-button

This command will create 3 components, button and loader components will be located in the inputs folder, while the loader-button components will be in the ui folder.

Since the components will be based on material-ui, you will need to install material-ui and its required dependencies using this command

bit install @mui/material @emotion/react @emotion/styled --type peer

Next, let’s update the button.tsx file with the following code:

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

export type ButtonProps = { value: string } & MuiButtonProps;

export function Button({ value, ...rest }: ButtonProps) {
return <MuiButton {...rest}>{value}</MuiButton>;
}

Also, implement the code for the loader.tsx component with the following code:

import type { ReactNode } from 'react';
import React from 'react';
import CircularProgress from '@mui/material/CircularProgress';

export type LoaderProps = {
children?: ReactNode;
};

export function Loader({ children }: LoaderProps) {

return (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<CircularProgress/>
</div>
);
}

Building a Composite Component: LoaderButton

In building a component library, there will be instances where a single component may not suffice to represent a UI element. This is where composite components come into play. Composite components are essentially a composition of two or more components to form a new, complex component. This approach helps in reducing code redundancy and improving the reusability of components

We will illustrate this concept by building a composite component that combines a loader and a button, named LoaderButton. This component will display a loading spinner inside a button when an asynchronous operation is in progress, providing immediate feedback to the user.

In the loader-button.tsxfile, implement the code for this component

import React, { useState } from 'react';
import { Button, ButtonProps } from '@aviatorscodes/material-ui.inputs.button';
import { Loader, LoaderProps } from '@aviatorscodes/material-ui.inputs.loader';
import { red } from '@mui/material/colors';
import { ThemeProvider, createTheme } from '@mui/material/styles';

export type LoaderButtonProps = ButtonProps & LoaderProps;

export function LoaderButton({ value, children, ...rest }: LoaderButtonProps) {
const [loading, setLoading] = useState(false);

const theme = createTheme({
palette: {
primary: {
main: red[500],
},
},
});

const handleClick = () => {
setLoading(true);
// Simulate an asynchronous operation
setTimeout(() => {
setLoading(false);
}, 4000);
};
return (
<ThemeProvider theme={theme}>
<Button
{...rest}
onClick={handleClick}
disabled={loading}
value="Click Me"
variant="contained"
>
{value}
</Button>
{loading ? <Loader /> : null}
</ThemeProvider>
);
}

What we’ve done is we’ve made the LoaderButton component use the Button and Loader components as its children.

Next, run bit tag && bit export to export these components onto Bit Cloud. Upon doing so, visit your Bit Scope on Bit Cloud, and you'll see the screenshot below.

This will let you work on your local computer and build the necessary components.

Managing Updates and Dependencies (Introduction to Ripple CI)

Bit provides a robust way of handling updates and dependencies for your component library. It allows each component to be updated independently, ensuring that changes in one component do not affect others. This feature is particularly crucial when managing a large component library with numerous dependencies.

Bit also manages dependencies between components. It tracks the dependencies of each component and ensures that they are updated when the component is updated. This feature ensures that your components always use the most recent and compatible versions of their dependencies.

Figure: Dependency graph

After making changes to a component, you can use Ripple CI, a continuous integration tool that works well with Bit by default, to automatically update components they make use of the updated component.

For instance, when a change is made to a component, it appears in the change request section, when that change is merged, Bit automatically triggers Ripple CI which then traverses through all of the usages of that component (across thousands of projects) and causes a Ripple Build to update all usages. This is pretty powerful when it comes to maintaining your component library.

Let’s see how…

Let’s say you want to make a change to the Loader component. To do that, you will need to create a bit lane; think of Lane as a Git branch containing changes you made to a component(s).

bit lane create adding-progressbar --scope aviatorscodes.material-ui

Ensure that you replace aviatorscodes.material-ui with your username and scope name. After you've done this, you'll get the output:

When the lane has been created, proceed to make changes to yourLoader component, here is the code:

import type { ReactNode } from 'react';
import React, { useState } from 'react';
import CircularProgress from '@mui/material/CircularProgress';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';

export type LoaderProps = {
children?: ReactNode;
};

function CircularProgressWithLabel(props) {
return (
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
<CircularProgress variant="determinate" {...props} />
<Box
sx={{
top: 0,
left: 0,
bottom: 0,
right: 0,
position: 'absolute',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Typography variant="caption" component="div" color="text.secondary">
{`${Math.round(props.value)}%`}
</Typography>
</Box>
</Box>
);
}
export function Loader({ children }: LoaderProps) {
const [progress, setProgress] = useState(10);

React.useEffect(() => {
const timer = setInterval(() => {
setProgress((prevProgress) =>
prevProgress >= 100 ? 0 : prevProgress + 10
);
}, 800);
return () => {
clearInterval(timer);
};
}, []);

return (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<CircularProgressWithLabel value={progress} />
</div>
);
}

After implementing your change, you will then snap it, bit snap creates an immutable and exportable component snapshot

bit snap aviatorscodes.material-ui/inputs/loader --message "adding a progress bar"

Ensure that you replace aviatorscodes.material-ui with your username and scope name. After you've done this, you'll get the output:

Next run bit export to export the component to bit cloud

Now, your changes have been pushed to bit cloud. You will find it in the “Change request” section.

You can now proceed to merge the changes. This implies that a new version of that component is available:

Bit will then trigger Ripple CI to propagate that new change to those components that use the LoaderButton component in their project. Cool right?

From the screenshot above, you can see how a change in the Loader component has affected the loader-button component since it is dependent on it. This way components in your library are always up-to-date.

To follow along with this tutorial, you can find scope and code for components used for this article here:

Wrapping Up

In this session, we’ve explored the creation of a custom Material-UI component library using Bit. We’ve discussed the importance of component libraries, the role of Bit as a tool for creating and managing components, and how Bit simplifies component management and versioning. We’ve also touched on the concept of composite components and how they can be used to create complex components. Finally, we’ve looked at how Bit handles updates and dependencies, and how it integrates with a Continuous Integration (CI) system like Ripple CI.

Bit plays a crucial role in managing a custom Material UI library. It provides a platform for developing, sharing, and collaborating on independent components, making it easier to build and manage a component library. With Bit, each component is versioned separately, allowing for granular updates and precise versioning. Furthermore, Bit’s ability to calculate the dependency graph and determine affected components when a component is updated ensures isolated updates and reduces the risk of breaking changes.

Looking ahead, the future of component libraries with Bit is promising. With Bit’s ability to handle updates and dependencies, create composite components, and integrate with CI systems for automatic updates, it provides a robust solution for managing component libraries. This allows developers to focus on building high-quality components while Bit takes care of the rest. As such, Bit is set to become a standard infrastructure for components, fostering a collaborative environment where multiple developers can work on different components simultaneously, leading to a more efficient development process.

--

--

Software Developer and Technical Writer. I am passionate about simplifying the web, one post at a time.