Marcell Ciszek Druzynski

React as lego blocks

How to build and think about React components as lego blocks

December 31, 2023

Crafting a well-structured React app can be a challenging. While straightforward for simple hobby projects, testing, or quick prototypes—especially for seasoned React developers—it becomes progressively intricate when aiming for a more scalable and composable application.

The key lies in treating React components as Lego blocks, fostering an environment where the addition and removal of components are seamless, devoid of convoluted code. Unfortunately, the pitfalls of overburdened components often surface, complicating maintenance and comprehension.

In exploring this, the focus is on recognizing opportune moments to extract components, minimizing complexity. The goal is to cultivate components that are not only more comprehensible but also align with their designated functionality, adhering to the principle that each component should succinctly embody its name and purpose. It is good to know when to breake our larger components into smaller ones and how we name our component to make our code base more readble and easier to scale.

Naming our components is like labeling ingredients in a recepie. Imagine that we are cooking a dish with some ingredients, each ingredient has a special role in the dish, and knowing what each ingredient adds to the dish helps us to create a delicious and well-balanced dish.

Unveiling React Composition

In the realm of React, a component serves as a fundamental building block, akin to a Lego piece that we can seamlessly integrate into various facets of our application.

A React component possesses the ability to receive props and maintain its own internal state. Moreover, it can extend its functionality by rendering other components. This intrinsic capability, where a component renders additional components, encapsulates the essence of composition in React.

Consider the example of crafting a straightforward navbar. Within the navbar component, we might encapsulate a list of list-elements, with each list-element housing anchor tags (links). This hierarchical structuring of components illustrates the power and elegance of composition in React.

1function Nav() {
2 return (
3 <nav>
4 <NavList />
5 </nav>
6 );
7}
8
9function NavList() {
10 return (
11 <ul>
12 <li>
13 <Link href="/about">About</Link>
14 </li>
15 <li>
16 <Link href="/contact">Contact</Link>
17 </li>
18
19 <li>
20 <Link href="/blog">Blog</Link>
21 </li>
22 </ul>
23 );
24}
1function Nav() {
2 return (
3 <nav>
4 <NavList />
5 </nav>
6 );
7}
8
9function NavList() {
10 return (
11 <ul>
12 <li>
13 <Link href="/about">About</Link>
14 </li>
15 <li>
16 <Link href="/contact">Contact</Link>
17 </li>
18
19 <li>
20 <Link href="/blog">Blog</Link>
21 </li>
22 </ul>
23 );
24}

Thanks to composition we can build a desired and complete UI in React. It just a simple example but it shows how we can use composition to build a UI in React.

Container Components: Unlocking UI Composition

Now that we've grasped the fundamentals of composition, let's delve into how we can leverage composition techniques to craft a robust UI interface.

Container components stand out as one of the most potent composition tools in React, often overlooked by developers when constructing reusable components. When discussing container components, we refer to those components designed to receive children as props and render them within their body.

Consider the example of building a <Wrapper/> component, tasked with determining whether the content it envelops should be fluid or fixed-width. I employ a similar pattern on my site to dictate the layout of pages, based on whether I desire a fluid or fixed layout.

1function PageWrapper({
2 children,
3 className,
4 fluid = false,
5}: PropsWithChildren<Props>) {
6 return (
7 <main
8 className={cn(
9 "flex flex-col flex-1",
10 fluid ? "max-w-none" : "max-w-4xl w-full mx-auto px-2",
11 className,
12 )}
13 >
14 {children}
15 </main>
16 );
17}
1function PageWrapper({
2 children,
3 className,
4 fluid = false,
5}: PropsWithChildren<Props>) {
6 return (
7 <main
8 className={cn(
9 "flex flex-col flex-1",
10 fluid ? "max-w-none" : "max-w-4xl w-full mx-auto px-2",
11 className,
12 )}
13 >
14 {children}
15 </main>
16 );
17}

The beauty of the children prop lies in its versatility. It accommodates not only a single component but also a component that renders multiple components. For instance, when employing <PageWrapper/>, I encapsulate everything, including the site content, within the children. The role of <PageWrapper/> is to maintain a consistent layout and render its children, fostering a cohesive UI structure.

Perforemnece and benefits by using composition

Using wrapper components with composition in React, especially with the use of props.children, can have both performance benefits and code organization advantages.

Performance Benefits:

  1. Optimized Rendering:

    • Wrapper components with props.children often allow for optimized rendering. The parent component can decide when and how to render its children, optimizing the rendering process.
  2. Avoiding Unnecessary Rerenders:

    • By utilizing React's memoization and PureComponent, we can prevent unnecessary rerenders of the children components. This is useful in scenarios where only specific parts of the component tree need to rerender and get updated.

Code Organization Advantages:

  1. Reusability:

    • Wrapper components provide a way to encapsulate functionality and reuse it across different parts of our application.
  2. Abstraction of Logic:

    • Wrapper components can abstract away complex logic, leaving the wrapped components focused on specific tasks. This separation of concerns improves code readability and maintainability.
  3. Cleaner JSX Structure:

    • Using wrapper components often results in cleaner JSX structure. Instead of having complex JSX directly in our parent component, you can abstract parts of the UI into separate wrapper components, making the parent component's JSX more concise and readable.

Example:

Consider a Card wrapper component that can be used to wrap various content:

1// Card.jsx (Wrapper Component)
2const Card = ({title, children}) => {
3 return (
4 <div className="card">
5 <h2>{title}</h2>
6 <div className="card-content">{children}</div>
7 </div>
8 );
9};
10
11// Usage in a parent component
12const App = () => {
13 return (
14 <Card title="Example Card">
15 <p>This is the content of the card.</p>
16 {/* Other child components or content */}
17 </Card>
18 );
19};
1// Card.jsx (Wrapper Component)
2const Card = ({title, children}) => {
3 return (
4 <div className="card">
5 <h2>{title}</h2>
6 <div className="card-content">{children}</div>
7 </div>
8 );
9};
10
11// Usage in a parent component
12const App = () => {
13 return (
14 <Card title="Example Card">
15 <p>This is the content of the card.</p>
16 {/* Other child components or content */}
17 </Card>
18 );
19};

In this example, Card is a wrapper component that provides a consistent structure for cards in our application. It encapsulates the styling and layout, allowing the parent components to focus on the specific content.

Using the useState hook in the Card component triggers a re-render when the state updates, but the components within props.children remain unaffected. This awareness is crucial, enabling us to bypass React memoization with memo() and useMemo(). While memoization isn't inherently bad in React, I believe there's a tendency to overuse these techniques. In many cases, rewriting the logic can maintain performance and readability without relying heavily on memoization.

Using wrapper components with composition and props.children can contribute to a more performant and organized React application, promoting code reuse and maintainability.

Guidelines for Component Breakdown

Adhering to valuable principles shared by Nadia Makarevich, which I also find resonant in React development, entails the following:

  • Top-to-Bottom Approach: Begin from the bottom and progress upward within our module or file.

  • Extract Components Judiciously: Choose the opportune moment to extract components, especially when the code complexity escalates beyond a manageable threshold.

  • Prioritize Simplicity: Initially, strive for simplicity. Avoid unnecessary composition at the outset, keeping the structure as straightforward as possible.

I usually don't begin by breaking out components into separate files; that's something I do towards the end, just before submitting a pull request. I prefer keeping everything in one file for as long as possible to get a better overview. Then, I make decisions on what should be in a separate file or module.

I like to categorize components as private and public. When I break down larger components into smaller ones and observe that it's mainly for code readability, I keep those components in the same module, treating them as private. Once I identify a pattern where I use the same technique in another file, I abstract it into a separate module, making it a common/public component that can be shared across multiple components.

Naming Components

Naming components is a crucial aspect of React development, often overlooked by developers. A component's name should succinctly embody its purpose, communicating its functionality to other developers who may read or reuse it. Moreover, it should be intuitive and straightforward to comprehend, avoiding overly complex names.

Naming React components is crucial for several reasons:

  1. Readability and Maintainability: Descriptive and meaningful names make our code more readable and understandable. This is especially important for maintaining the code over time, as it helps you and others quickly grasp the purpose of each component.

  2. Code Navigation: Well-named components make it easier to navigate through our codebase. This is beneficial when you need to find and understand specific parts of our application.

  3. Reusability: Good names make it clear what a component does, making it easier to identify if it can be reused in other parts of our application or even in different projects.

  4. Collaboration: When working in a team, consistent and meaningful naming conventions contribute to better collaboration. Team members can understand each other's code more easily, leading to increased productivity.

If we name our components in a wrong or unclear way, several issues may arise:

  1. Confusion: Unclear names can confuse developers, including ourself, about the purpose or functionality of a component. This confusion can lead to mistakes and make it challenging to maintain the code.

  2. Increased Debugging Time: When names don't accurately reflect the component's purpose, debugging becomes more time-consuming. Developers may spend extra time trying to understand the role of a poorly named component.

  3. Reduced Reusability: Components with ambiguous names are less likely to be reused. Other developers may be hesitant to incorporate components into their codebase if they can't quickly understand their purpose.

  4. Inconsistency: Inconsistent naming conventions can lead to confusion within the codebase. Developers may adopt different styles, making it harder to maintain a cohesive and understandable codebase.

To avoid these issues, it's essential to follow consistent naming conventions, choose descriptive names, and ensure that our component names accurately reflect their functionality.

Summary

Writing effective and robust React applications involves recognizing the opportune moments to decompose our components into smaller units. It is good to avoid overly large components that necessitate scrolling in our editors to comprehend their purpose. Careful naming of components serves a dual purpose: it communicates the component's functionality to other developers who may read or reuse it, and it aids our own future understanding when revisiting the code.

Employing composition in React is the key to constructing robust and potent UIs. A profound comprehension of React's composition is crucial to prevent pitfalls as our codebase expands. Without employing composition techniques, such as Container components, scaling and maintaining our project can become increasingly challenging over time.