How To Build Better React Components in 2024?

4 Patterns for Extensible and Reusable React Components

Nitsan Cohen
Bits and Pieces

--

Extensible and reusable components are fundamental in modern React development, especially when aiming for efficient and scalable user interface design.

This ensure that your React components are easy to maintain and are versatile enough to be adapted across various applications.

The key to their success lies in their straightforward design and adaptability, which enables them to fit seamlessly into different contexts within an application or across various projects. By focusing on extensibility and reusability, you can create components that serve various purposes while maintaining a consistent design and functionality.

So, this article aims on achieving exactly that! We will take a look at how we can implement highly scalable and reusable React components that you can use across multiple applications! But, to do so, we will be relying on a Build Engine — Bit.

Bit lets you design and build anything in reusable independent components. Not only that, but it offers a CI server — Ripple CI thats capable of propagating changes across components trees to maintain consistency.

At the end of this blog, you will be gain a practical perspective on how extensible and reusable patterns can be effectively implemented, enhancing collaboration and uniformity in UI development across diverse team projects.

Practise 01: Building Base UI Components with Bit: A Closer Look

What exactly is a Base UI Component?

A Base UI Component is a bare minimum component that has a pre-defined behavior that can be customized by the consumer. For example, take a look at this Basic UI Collection that I’ve hosted on Bit Cloud.

Figure: The Basic UI Component Collection

It has everything that you need, in bare-bones to let you get started with your next React application with minimum effort.

Every component that we’ll be using in this demo is located under the Scopebitdesign.basic-react.

This scope acts as a centralized hub, providing various foundational components that are independent yet easily accessible for teams working on React projects. It facilitates a structured yet flexible approach, allowing teams to efficiently access, use, and individually customize these essential UI components, ensuring consistency and adaptability in their development workflows.

In our bitdesign.basic-react Bit scope, we'll explore key components that demonstrate the principles of extensible and reusable design. These components are:

  • A Theme Component: This component establishes the design language and tokens for the Acme brand, ensuring consistency in styling across all UI elements.
  • A Button Component: A fundamental UI component designed for buttons, aligning with the Acme theme's guidelines.
  • An Environment Component: An environment component that forms the development setting for all base components. For example, it integrates the ACME theme, enabling each component to be developed and previewed with appropriate styling.

To build better components, you need traceability

You’re right. You need to keep track of what other components use a particular component. This can help track breaking changes, and highlight areas of impact right out of the gate!

With Bit, you can use its dependency graph to understand how your components interact. It illustrates the interconnectedness of components.

For example, in our scope,the ACME theme’s updates directly impact related components like button and the acme-react-env. This graph ensures that changes in design or functionality are uniformly applied across all dependent components, as shown below:

Figure: Observing the Dependency Tree

This dependency graph is particularly beneficial in large-scale projects involving multiple teams. It aids in efficient change management, guaranteeing that updates within a scope are in sync and align with the broader organizational standards. By utilizing this feature, teams can maintain a cohesive and unified design language across different project segments.

Practise 02: Exposing Component APIs for Enhanced Functionality and Type Safety

Exporting a component’s API and its types enhances its functionality and ensuring type safety, particularly when different teams are involved.

Here’s an example of how the API and types for the Button component are exposed in bitdesign.basic-react/buttons/button:

// index.ts in bitdesign.basic-react/buttons/button
export { Button } from './button';
export type { ButtonProps } from './button';

When another team needs to extend this Button component, they can import its type and adapt it according to their specific requirements:

import React from 'react';
import { ButtonProps as BaseButtonProps, Button as BaseButton } from '@bit/bitdesign.basic-react.buttons.button';

export type ExtendedButtonProps = BaseButtonProps & {
// Additional props specific to the extended button
};

export const ExtendedButton = ({ children, ...rest }: ExtendedButtonProps) => {
// Implementation that utilizes the base Button component
return <BaseButton {...rest}>{children}</BaseButton>;
};

This approach of extending types ensures that there’s a consistent interface across different implementations. It makes the most of TypeScript’s capabilities for improved compile-time checks and overall developer experience.

Moreover, thanks to Bit’s dependency graph, every time the API in the base component is updated, components that extend or use it will also be updated. This automatic synchronization ensures that all teams are working with the most current version of the component, maintaining consistency and reliability across the project. This feature is particularly valuable in complex projects where multiple teams rely on shared components, as it keeps everyone aligned with the latest changes and improvements.

Practise 03: Theming for Style Consistency with the Theme Component

To maintain a consistent look and feel, base components are designed to be themeable.

This is achieved by utilizing a theme component, like bitdesign.basic-react/acme-theme, which holds all design tokens as CSS variables. This setup allows for easy overrides and ensures that components adhere to a unified design language.

For example, the Button component might use the theme as follow

import { useTheme } from '@bitdesign.basic-react.acme-theme';

const Button = ({ className, children }) => {
const { primaryColor } = useTheme();
const style = { background: primaryColor };
return <button style={style} className={className}>{children}</button>;
};

The use of a theme component simplifies managing and applying design changes across all components, enhancing maintainability and scalability.

Practise 04: Enhancing Customizability and Extendability

Enhancing Customizability with ClassName Prop

The inclusion of a className prop in a component's API enhances its customizability. This feature allows users of the component to apply their own styles, making the component adaptable to different design contexts. Here’s an implementation example:

import classNames from 'classnames';
import styles from './button.module.scss';

export type ButtonProps = {
children?: ReactNode;
className?: string;
} & React.HTMLAttributes<HTMLButtonElement>;
export function Button({ className, children, ...rest }: ButtonProps) {
return (
<button {...rest} className={classNames(styles.button, className)}>
{children}
</button>
);
};

In this snippet, the className prop works alongside the classNames utility. The styles.button provides the default styling defined in the component's stylesheet. The className prop, however, is for additional style — adding or overriding the orginal base style, by the consumer of the component.

The sequence in which these class names are applied is significant. Placing the className prop last in the classNames function ensures that any styles provided by this prop override the default styles in case of any conflicts. This design allows users to maintain the fundamental design and functionality of the component while customizing its appearance to suit specific requirements. It strikes a balance between maintaining a consistent base style and offering flexibility for customization, a vital aspect of creating reusable, adaptable components.

Extendability with Spread Operator

The spread operator (...rest) is a powerful feature in React that enhances the extendability of components by passing all additional props to them. This ensures the component inherits all native properties of the underlying HTML element, making it more flexible and adaptable for different use cases.

Here’s an example of how to apply this pattern in thebitdesign.basic-react/typorgraphy/text component:

import React, { HTMLAttributes, ReactNode } from 'react';

type TextElements = 'span' | 'p';

export type TextProps = {
children?: ReactNode;
className?: string;
textElement?: TextElements;
} & HTMLAttributes<HTMLSpanElement | HTMLParagraphElement>;
export function Text({ children, className, textElement = 'p', ...rest }: TextProps) {
const TextElement = textElement;
return (
<TextElement {...rest} className={className}>
{children}
</TextElement>
);
}

In this Text component, the spread operator is used to pass any additional HTML attributes to the rendered text element. This means you can use the Text component like any standard HTML paragraph (p) or span (span) element, applying native HTML attributes directly:

<Text id="unique-text-id" style={{ color: 'blue' }}>Sample Text</Text>

Here, id and style are additional attributes passed to the Text component. They are applied directly to the underlying HTML element (p or span), demonstrating the component's extendability. This approach ensures that the Text component is not only customizable but also fully functional and compliant with standard HTML behavior, allowing for a wide range of applications in different contexts.

The Benefits of Extensible and Customizable Components in Bit

If you’ve noticed in this article, I’ve used Bit to create my React components and not npx.

Well I have a good reason for that.

Think of the bigger picture. Right now, I have one React component — Button. If I spin up 100 projects tomorrow, I can use this same Button without duplicating the component code inside each React project. Not only will I reduce huge lines of redundant code, but I’ll also have a guaranteed consistent outcome every time. With Bit’s dependency tree, I can guarantee that my apps will use the latest version of my Button without a single change in the rest of the app.

This promotes consistency in the user experience, as the underlying base components maintain a uniform standard while allowing for necessary variations.

Well, How Does Bit Handle Component Changes?

Simply put; efficient change management on Bit is powered using it’s CI Server — Ripple CI.

In a component-based software engineering, there is a constant need to efficiently manage and sync changes. Bit, particularly with its integration with Ripple CI, streamlines this process. Ripple CI facilitates the continuous integration and delivery of components, ensuring that updates and improvements are automatically propogated across the dependency graph.

A release of a new blog post propagates to the blog component and then to the bit.dev app

When a component is updated, Ripple CI helps in automatically running tests and building processes for modified components. This ensures that any changes made to a component are stable and ready for use. Once a component passes all checks, Ripple CI aids in propagating these changes to all dependent components across different teams. This mechanism is especially beneficial in environments where multiple teams work with shared components, as it keeps everyone up-to-date and aligned with the latest versions.

Wrapping Up

By embracing the patterns of creating extensible and reusable components in Bit, you contribute to a more collaborative and efficient UI development ecosystem. This approach not only results in the creation of robust and reusable components but also fosters an environment where these components can be easily shared and adapted to different contexts. The integration with tools like Ripple CI further enhances this ecosystem by ensuring that changes are smoothly and consistently propagated across various teams and projects.

This methodology of component development drives innovation and efficiency, ensuring that software development becomes a more cohesive process. The resulting codebase is not only easier to maintain but also more flexible and responsive to the evolving needs of different projects and teams. By adopting these practices, organizations can significantly improve their development workflows, leading to higher quality software and a better overall user experience.

I hope you found this article helpful.

Thank you for reading!

--

--

Making 𝘾𝙤𝙢𝙥𝙤𝙣𝙚𝙣𝙩-𝘿𝙧𝙞𝙫𝙚𝙣 Software a 𝐑𝐞𝐚𝐥𝐢𝐭𝐲 @ 𝐛𝐢𝐭.𝐝𝐞𝐯