Architecture Breakdowns

Building Scalable Styling Architecture in React

Published by

Junior Felix

featured image Building Scalable Styling Architecture in React

React is one of the most widely adopted technologies in front-end web development. One of the reasons for React’s popularity is the freedom it allows developers in terms of tooling and architecture. Over the years, React has continued to keep its unopinionated nature and has shown no signs of doing otherwise.

However, React’s flexibility comes at a price of responsibility for one’s own tooling and organization. Consequently, it is up to a developer to decide how to organize styling in a React application. Occasionally, these applications grow in complexity; thus, the styling architecture enforced in the early stages does not scale well.

What is a Styling Architecture in React?

Styling architecture in the context of React refers to how everything related to the look, feel, and design of React components is organized within an application.

The Relationship Between Design Systems and Styling Architecture

As styling architecture refers to the general organization of styling elements within an application, a design system, if present, defines it. Design systems involve a holistic approach to implementing User Interfaces, which involves designers, developers, and other stakeholders in the project. Besides the actual building of components, a design system defines documentation and workflow procedures.

In this article, we will outline practices that will aid in developing a scalable styling architecture by focusing on the following:

  • Reusability: Maximizing how reusable a component and its styling reduces the chances of duplication as the application grows. Consequently, as the application’s complexity increases, it requires less code to be written.
  • Ease of Maintenance: Since we expect the codebase to evolve, making changes should be as painless as possible.
  • Consistency: A consistent styling architecture enables you to make educated guesses on the organization of its structure and its coding style.

Modular Styling

Modular styling refers to breaking down the styling of an application into smaller, independent pieces that are reusable throughout the application.

To a styling architecture, modular styling has the following benefits:

  • Ease of Maintenance: Since the styles are only limited to a single component or geared towards achieving a singular look, making changes is easier because we can pinpoint what we need to change. Additionally, since styles are self-contained, making changes in one component will not affect other components.
  • Reusability: By limiting the scope that styling files are responsible for, we can reuse them when we need to replicate the same design they achieve.
  • Ease of Understanding: It is difficult to determine which styling rules depend on others without modularizing styles. Hence, it is more challenging to understand how an application achieves certain designs.

In React, assigning unique class names to CSS classes achieves modular styling. These are some of the methodologies that achieve modular styling:

Using Naming Conventions

Naming conventions such as BEM provide a mechanism through which a developer can create unique class names that allow reusability. This technique ensures that the class names are human-readable.

An example of this approach can be illustrated as follows:

Let’s say we want to create a button component that can serve as a success or a danger button, depending on the props it receives. Using the BEM approach, we can define the button component as follows:

1interface ButtonStatus {
2  status: "success" | "danger"
3  children: React.ReactNode
4}
5
6export const Button = (props: ButtonStatus) => {
7  return (
8    <button className={`button button_status_${props.status}`}>
9      {props.children}
10    </button>
11  )
12}

The styling of this component can be defined in an accompanying CSS file as follows:

1.button {
2  padding: 5px 10px;
3  border: none;
4  border-radius: 3px;
5}
6.button_status_success {
7  color: #fff;
8  background-color: #198754;
9}
10.button_status_danger {
11  color: #fff;
12  background-color: #fa113d;
13}

In the example given above, both the “danger” and “success” buttons share the same padding, border, and border-radius properties; hence we have a common CSS class for them. However, their individual CSS classes define properties unique to each case: color and background color. If we wish to change the color scheme for each case, we only need to make changes to the relevant CSS classes.

This approach can be cumbersome as the application grows since the developer must constantly think of CSS class names. Aside from naming conventions, other solutions are available that generate unique class names without requiring the developer to remember which naming convention they are enforcing.

Using CSS Modules

CSS modules, by default, limit style sheets to specific scopes, as explained in the official repository. In this methodology, unique class names, based on the specified scope, are generated during the build step using a module bundler like Webpack.

An illustration of CSS Modules can be made using a Create React App example.

Let’s say we want to style a success button. First, we would create a button CSS file with the name button.module.css and add the relevant CSS rules as follows:

1.success {
2  background-color: #198754;
3  color: #fff;
4}

The corresponding button component will be defined as follows:

1import styles from "./button.module.css"
2
3export const Button = () => {
4  return <button className={styles.success}>Success</button>
5}

Inspecting the button component using browser dev tools will reveal something similar to this:

1<button class="Button_success__U5Foi">Success</button>

It is also important to note that both Gatsby and Next.Js support CSS Modules.

Using CSS-in-JS libraries

This methodology defines CSS in JavaScript files as opposed to conventional CSS files. By default, unique CSS class names are generated, which eliminates the possibility of class name duplication.

Emotion and Styled Components are widely adopted options in implementing CSS-in-JS.

Using Utility-First CSS Frameworks

Utility-first CSS Frameworks such as TailwindCSS and Tachyons utilize single-responsibility classes to create complex components. This approach departs from the convention that usually declares a set of CSS styles within a class, then assigns the class to an element. An example of this utility-first approach can be illustrated as follows:

1<p class="font-sans font-semibold">
2  The quick brown fox jumps over the lazy dog.
3</p>

In this example, two different CSS classes style font-family and font-weight properties. If we use the conventional approach, one CSS class would be responsible for both properties. Afterward, we assign the class to the element.

Since each class in a Utility-first does exactly one thing, the styling aspect is modular; hence it is easier to compose complex components.

However, challenges may occur in the readability of the code since some components are bound to use numerous classes to achieve the desired look. For example, if we were to build a card component, we may end up with a component that resembles:

1<div className="flex items-baseline mt-4 mb-6 pb-6 border-b border-slate-200"></div>

In other cases, the number of utility classes can go up to 20, making it less readable than assigning multiple CSS properties to one class.

Planning your Tooling and Core Libraries Deliberately

The tools and libraries we choose significantly impact how our application scales in the future. For instance, if we choose a library that has discontinued support for a couple of years, we might run into unexpected errors that would slow down development.

To a styling architecture, proper tooling and library planning have the following benefits:

  • Consistency: Good tools and libraries are characterized by their consistent nature. Consequently, React projects built with good tools and libraries enjoy consistency as a benefit.
  • Ease of Maintenance: Good tools and libraries get updates that fix bugs and add new features. Therefore, we only need to update our tools and libraries to adopt these changes.
  • Reduction in Development Time: Instead of building certain functionalities by hand, choosing a good tool or library that achieves the same result saves significant development time and effort. For instance, Storybook allows us to develop and test components in isolation, thus speeding up development.

In styling architecture, whether or not to use a component library is a key decision that needs to be made. This choice is informed by weighing application needs and the existing component libraries. When choosing an existing component library, the following criteria can be used:

  • Customization: The extent we can customize a component varies from one component library to another. Consequently, some component libraries significantly limit the amount of changes you can make to a component. Therefore, before picking a library, it is important to evaluate if the desired look can be achieved using the library under consideration. Alternatively, you can choose to build your own component library.
  • Community Support: The activity level around a component library is important in determining which to use. Github activity, such as the number of open issues, number of Github stars, and requests for new features, is a good indicator of how active a community is regarding a community library. An active community is a good gauge of the longevity of a component library.
  • Accessibility: Ensuring that a component library has built-in accessibility features removes the need to implement them, saving time.
  • Cross-Browser and Cross-Device Compatibility: A good component library ensures that its pre-built components are compatible with multiple devices and browsers. Github issues of the component library under consideration can offer insight on whether browser/device compatibility issues are prevalent.

Building your own component library is a viable option if the existing component libraries do not suit your needs. React’s ecosystem has tools that can aid in developing a robust component library. These tools include:

  • Storybook: In addition to allowing us to build components in isolation, Storybook provides a playground where we can view our components.
  • Jest: This is a testing framework that runs tests, makes assertions, and supports mocking during tests. Consequently, it is indispensable when setting up tests for a component library.
  • React Testing Library: This is a library that allows us to mimic a user’s actions as they interact with our application.
  • Chromatic UI: This is a tool that publishes component libraries and incorporates a workflow that supports UI review and visual regression testing.

Having a Consistent File/Folder Structure

Since React does not enforce a specific folder structure, different React projects will likely have different folder structures. In a project that has significant growth potential, a consistent folder structure provides the following benefits:

  • Eases maintenance: When making changes or fixing bugs, it is easier to locate the relevant files if a consistent file/folder structure is maintained.
  • Eases understanding: If a file/folder structure is consistent, you can intuitively know the purpose of each file. For instance, if we have named our test files using the convention ComponentName.test.js, we will be able to discern that the tests of the ComponentName are defined in the file.

Various patterns are commonly used in file/folder structure. Some of these methodologies include:

Feature-based Folder Structure

In this pattern, folders/files are organized based on a specific application feature. Consequently, the application folder structure mirrors the business aspect instead of the technology used to build it.

File-type-based Folder Structure

In this pattern, files/folders of the same type are grouped together. For example, all components are placed into the components folder, and hooks are placed into the hooks folder. This setup is easy to achieve; however, as the application grows, it may prove challenging to maintain.

Implementing Testing

Testing is an industry standard in software engineering; thus, we should not ignore it in React.

For a scalable styling architecture, defining good tests has the following benefits:

  • Enforces consistency: When tests are defined, they outline the expected behavior of the relevant code. Consequently, we can detect unanticipated behavior as they happen.
  • Eases maintenance: When changes are introduced to a React application, tests ensure that these adjustments do not contradict the intended behavior.
  • Eases understanding: Well-defined tests can document the intended purpose of the relevant part of the application. Therefore, testing contributes to the understanding of the application.

Unit tests, integration tests, and End-to-End tests can be used to test how a user interacts with the application and make the relevant assertions. In addition to these tests, visual regression testing is used to catch UI changes. Adding a testing step in the CI/CD pipeline reduces the chances of unintended bugs occurring and builds confidence in the code changes made.

During testing, flaky tests are a common pitfall that can do more harm than good in the development process.

Flaky Tests and How to Avoid Them

A flaky test gives inconsistent results when run several times without making changes to the code. Flaky tests instill a lack of confidence in the working of your application. Consequently, they slow down the development process.

The following strategies reduce the chances of running into flaky tests:

  • Running tests in an isolated environment. To achieve this, ensure that all external services utilized in a test are mocked. The latter reduces the chances of using unpredictable data in your tests. Additionally, tests should be able to run independently and in random order.
  • Keep your tests simple.When a test covers multiple test cases and a significant chunk of code, it becomes more difficult to predict the outcome of the test. Consequently, it becomes harder to debug failing tests.

Using Themes

Theming refers to the capability of applying different styles to an application, depending on context. For instance, to implement dark and light mode in a React application, theming is an option that helps apply light and dark CSS styles depending on the user’s preference.

To a styling architecture, theming has the following benefits:

  • Consistency: In theming, some styling properties of an application, such as color palette, are defined at a single point. Therefore, we can implement these precise properties in multiple application parts while maintaining the same look and feel.
  • Eases Maintenance: In theming, properties such as color codes are abstracted from the components that use them. Consequently, if we change the whole application’s design, we only need to adjust these properties’ specifications.
  • Increases Reusability: Using theming, we can apply the same styling properties to multiple components.

Theming in React is widely adopted. Popular UI libraries such as Material UI and Chakra UI implement theming. Furthermore, CSS-in-Js libraries such as Styled Components and Emotion support theming out of the box.

Using Component Composition When Possible

Component composition in React refers to the passing React components as props; resulting in new React components. This pattern has the following benefits to styling architecture:

  • Maximizes reusability: This practice keeps components as generic as possible, which culminates in these components being reused in various places, reducing the probability of code duplication.
  • Eases maintenance: By keeping the components as generic as possible, debugging becomes simpler since each component has less complex implementation details.
  • Eases testing: The more generic our components are, the easier it becomes to test them. The latter is because the number of use cases we have to test reduces as the implementation details get less complex.

An example of component composition can be illustrated in the following example:

Let’s say we want to create a Button. There are two ways to go about it:

1interface ButtonProps {
2  label: string
3}
4
5export const Button = ({ label }: ButtonProps) => {
6  return <button>{label}</button>
7}
1interface ButtonProps {
2  children: React.ReactNode
3}
4
5export const Button = ({ children }: ButtonProps) => {
6  return <button>{children}</button>
7}

The first approach limits what the button displays to string and, thus, is more specific. Using the second approach, which expects a React component as a prop, is more generic and can accept other prop types aside from string, such as follows:

1export const ButtonGroup = () => {
2  return (
3    <div>
4      <Button>
5        <img src="icon.png" alt="add button" />
6      </Button>
7      <Button>Add</Button>
8    </div>
9  )
10}

Conclusion

While scalable styling architecture can be implemented using different methodologies, focusing on reusability, ease of maintenance and consistency is likely to produce the desired results.

It is also important to note that while the practices mentioned above provide a blueprint to create a scalable styling architecture, they are not the only measures that can achieve the desired results. Furthermore, best practices are constantly evolving as React grows in web development.

icon_emailaddress

Get our latest software insights to your inbox

More insights you might be interested in

Are Certifications Worth It?

Balanced Analysis

Are Certifications Worth It?

Explore various perspectives surrounding the controversial issue of the value of certificates in the tech community. Get an in-depth rundown of the pitfalls that arise from the use of professional certificates and how to use certificates optimally.

Why Containerize Your Gatsby Application?

Tutorial

Why Containerize Your Gatsby Application?

Learn why you should containerize you gatsby application using docker and how to do so with easy to follow steps regardless of the Operating System you are using.

Our Failed Migration To Netlify CMS: Post Mortem & Lessons Learned

Deep Dive

Our Failed Migration To Netlify CMS: Post Mortem & Lessons Learned

A retrospective outlook on our process of migrating to Netlify CMS and the challenges we faced.

Seeding Data Into MongoDB Using Docker

Tutorial

Seeding Data Into MongoDB Using Docker

Learn how to seed data into MongoDB using docker and docker compose and use the generated data in a React application, through step-by-step instructions.