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.
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:
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:
In React, assigning unique class names to CSS classes achieves modular styling. These are some of the methodologies that achieve modular styling:
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.
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.
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.
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.
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:
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:
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:
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:
Various patterns are commonly used in file/folder structure. Some of these methodologies include:
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.
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.
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:
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.
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:
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:
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.
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:
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}
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.
Balanced Analysis
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.
Tutorial
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.
Deep Dive
A retrospective outlook on our process of migrating to Netlify CMS and the challenges we faced.
Tutorial
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.