A Quick Guide to Reusable, Readable and Maintainable Components in React

Today, I want to discuss maintaining a clean and organised codebase in web and mobile development. This crucial aspect of development, which ensures long-term project sustainability and ease of collaboration, often doesn't receive the attention it deserves. While many developers focus on the broader aspects of codebase organisation, such as folder structures and component interconnectivity, there's another layer of organisation that's equally vital yet frequently overlooked: the internal structure of the components themselves.

Through my decade-long experience in web and mobile development, I've encountered my fair share of messy components. This experience has led me to champion a component-centered design as the most effective architecture for user-facing applications. It benefits developers and enhances performance handling, error handling, and collaboration between UX and engineering teams.

However, many "components" I've come across in various companies are not genuine components but rather monstrous files. These files misuse the component framework to amalgamate dozens of pieces of UI and business logic, contravening age-old programming principles like the Single Responsibility Principle. This principle, I firmly believe, should extend to components, ensuring they are isolated, modular pieces of logic that can be manoeuvred like Lego bricks.

Unfortunately, the reality of many development projects is far from this ideal. Components often resemble Lego structures glued together, leading to a fragile system where a single change can cause widespread disruption.

To combat this, I propose a framework of thought for maintaining clean, readable, and maintainable components within a team. I divide my component structure into three distinct 'blocks' or ‘sections’:

  • State, Props & Variables Block
  • Render Block
  • Named Functions Block 

Before diving into these blocks, let's discuss an example component I created to illustrate my in-component organisation strategy. This component displays a task's title, description, and date range, along with its urgency, and an onPress, which shows an alert with task details.

The specifics of the code are less important than the organisation strategy it exemplifies, showcasing a practical approach to structuring components.

A framework of thinking about your components

State, Props & Variables Block

This is the first block in a component, utilised for state, props, and variables. While most developers handle this part adequately, I often encounter messiness in this section due to including elements that don't belong. Consider the chaos at the top of your components; how familiar does this scenario look?

Components with unreadable variable declarations

From my experience, such disorganisation becomes challenging to maintain or even comprehend. My rule of thumb is straightforward: if a piece of code within the State, Props & Variables Block extends beyond one line—exceptions notwithstanding—I typically move it to the Named Functions Block. Similarly, if I catch myself writing JSX in this block, it signals the need for a new component dedicated to that specific piece of logic. This transition smoothly leads us to the Render Block, the sole domain for your JSX.

Render Block

The Render Block should solely be responsible for displaying UI and NOT for the following:

  • Conditional Rendering Logic: While some conditional rendering is unavoidable for dynamic UIs, complex conditions should be resolved before reaching the Render Block. Use boolean variables or dedicated functions in the State, Props & Variables Block to handle complex conditions, keeping the Render Block straightforward and focused on the UI.
  • Inline Styles or Class Definitions: Defining styles or class manipulations directly within the Render Block can lead to clutter and complexity. Instead, manage styles outside the Render Block or use CSS/SCSS modules or styled-components to keep the Render Block clean.
  • Direct API Calls or Data Fetching: These should be strictly handled outside the Render Block, preferably within lifecycle methods or hooks like useEffect for React components. The Render Block should only concern itself with presenting the data, not fetching or manipulating it.
  • Complex Data Manipulation: Any form of data reshaping, filtering, or sorting should be done outside the Render Block. Prepare your data in a way that it's ready to be displayed by the time it reaches the Render Block.

This means your Render Block should not resemble the example below. How often have you encountered a Render Block like this one, only to realize how much of a nightmare it is to maintain? Updating the UI, whether it's switching the order of elements or restyling an item, becomes an arduous task due to the convoluted logic and variable manipulation embedded within:

A component with an unreadable render block

In this example, we see logic evaluation, onPress handling, and variable deconstruction within the Render Block—a clear violation of its intended purpose. Such responsibilities should be allocated to the State, Props & Variables Block or the Named Functions Block. The Render Block's function is to present the UI in its simplest form, ensuring code readability and maintainability. This approach not only streamlines the development process but also facilitates easier updates and modifications. Now, let's proceed to the final segment.

Named Functions Block

This particular section of our component structure often sparks debate. For those interested, I’ve previously detailed this concept in my Clean Code Hack: A Short Guide to useEffect Usage in React, where I applied the Named Functions Block to useEffect. It's curious to note how some developers have strong opinions against naming functions. I've witnessed instances where developers were required to use only arrow functions throughout their codebase, a practice I find perplexing.

The Named Function Block provides a dedicated space to name and annotate functions within our component. Naming functions forces us to consider their purpose and ensure we adhere to a single responsibility. It simplifies identifying commonalities in our code, allowing us to extract a function from one component and refactor it into a utility that can be reused across multiple components without sifting through dense JSX.

Naming functions clarify their intent and promote code reuse and organisation. I encourage you to experiment with this by naming your functions within the Named Functions Block of your component, which typically involves declaring functions just below the return statement. By doing so, you will likely find that your component logic becomes cleaner and more organised.

Now, reflect on the component structure I introduced at the beginning. Our components become easier to read and modify, illustrating the benefits of this way of thinking:

A quick caveat, as with all my articles, I am exploring an idea. I am not saying this is what you must do, or that you must religiously follow this framework. Use it if it benefits you. At the end of the day, if it helps make your application more readable and maintainable, that's a benefit, especially in a team.

Back to blog

Leave a comment