Part 3: Component Structure - Building Reusable and Maintainable Components in React!
Tags: react, bestpractice, javascript, componentstructure
Published: 2023-05-22 03:30:00

Welcome to Part 3 of our **"React Best Practices in 2023"** series! In this part, we will explore the importance of component structure and how it contributes to creating components that are highly reusable, modular, and easy to maintain.

> Building reusable and maintainable components in React is not just about writing code; it's about adopting best practices and following sound architectural principles.

By carefully structuring our components, adhering to the **Single Responsibility Principle**, and embracing concepts like **Atomic Design** and **Component Composition**, we can create code that is more **modular**, **easier to test**, and **simpler to maintain**. 

This approach leads to a more efficient development process and ultimately results in **high-quality**, scalable React applications.

Let's consider an example where we have a Todo application implemented in React.

```js
// ❌ Bad code with multiple responsibilities
import React, { useState } from 'react';

const TodoApp = () => {

  // Handling state ❌ 
  const [todos, setTodos] = useState([]);
  const [newTodo, setNewTodo] = useState('');

  // Handle input change ❌ 
  const handleInputChange = (e) => {
    setNewTodo(e.target.value);
  };

 // Handle todo logic ❌ 
  const handleAddTodo = () => {
    if (newTodo.trim() !== '') {
      const updatedTodos = [...todos, newTodo];
      setTodos(updatedTodos);
      setNewTodo('');
    }
  };

  const handleDeleteTodo = (index) => {
    const updatedTodos = todos.filter((_, i) => i !== index);
    setTodos(updatedTodos);
  };

  const handleCompleteTodo = (index) => {
    const updatedTodos = todos.map((todo, i) => {
      if (i === index) {
        return { ...todo, completed: !todo.completed };
      }
      return todo;
    });
    setTodos(updatedTodos);
  };

  // ❌  It doesn't provide a clear separation of smaller reusable components. 
  return (
    <div>
      <h1>Todo App</h1>
      <input type="text" 
value={newTodo} onChange={handleInputChange} />
      <button onClick={handleAddTodo}>Add Todo</button>
      <ul>
        {todos.map((todo, index) => (
          <li key={index}>
            <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>{todo.text}</span>
            <button onClick={() => handleDeleteTodo(index)}>Delete</button>
            <button onClick={() => handleCompleteTodo(index)}>
              {todo.completed ? 'Mark Incomplete' : 'Mark Complete'}
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
};
```

Above codebase contains a single component that handles everything from **rendering the UI** to handling data and **state management**. This monolithic approach leads to a lack of separation of concerns and violates the **SRP** and **Atomic Design principles**.

To improve the code, we can follow the SRP and Atomic Design principles:

## Single Responsibility Principle (SRP)

This principle states that a class or component should have a **single responsibility** or **single reason to change**. By keeping components focused on a **specific task**, you improve code **readability**, **maintainability**, and **reusability**.

It promotes breaking down complex functionality into **smaller**, **focused parts** that are easier to understand, test, and maintain.

It encourages components to have **clear** and **specific responsibilities**, enhancing their reusability and maintainability.

It helps in avoiding **tightly coupled** components by keeping them focused on specific tasks.

Let's breakdown the monolith,

- **TodoInput**: Extract the input handling logic into a separate `useTodoInput` custom hook and component `TodoInput`. 

> Responsible for handling user input and adding new todos.

- **TodoList**: Extract the todo list handling logic into a separate `useTodoList` custom hook and component `TodoList`. 

> Responsible for rendering the list of todos.

- **TodoItem**: Move the rendering logic for individual todos into a separate `TodoItem` component. 

> Responsible for rendering an individual todo item.

By separating the **state** and **event handling** logic into custom hooks or components, we ensure that each component has a following single responsibility.

### Todo Input

The useTodoInput **custom hook** can manage the **input state** using the useState hook and handle the **input change event**

**useTodoInput.js**

```js
// ✅ Responsible for manage state and UI events

import { useState } from "react";

const useTodoInput = (onAddTodo) => {
  const [inputValue, setInputValue] = useState("");
  const [disabled, setDisabled] = useState(true);

  const handleSubmit = (e) => {
    e.preventDefault();
    onAddTodo(inputValue);
    clearInput();
  };

  const handleInputChange = (e) => {
    const value = e.target.value;
    setInputValue(value);
    setDisabled(value.trim() === "");
  };

  const clearInput = () => {
    setInputValue("");
    setDisabled(true);
  };

  return {
    disabled,
    inputValue,
    handleInputChange,
    handleSubmit
  };
};

export { useTodoInput };
```

By utilizing custom hooks, we can encapsulate the state and event handling logic in a reusable and modular way, promoting code reusability and maintainability.

**TodoInput.jsx**

Move the JSX code related to the input field, "Add Todo" button, and todo list into separate JSX file.

```js
// TodoInput.jsx

// ✅ Responsible for rendering TodoInput UI

const TodoInput = ({ onAddTodo }) => {
  const {
    disabled,
    inputValue,
    handleInputChange,
    handleSubmit
  } = useTodoInput(onAddTodo);

  return (
    <form className="todo-input" onSubmit={handleSubmit}>
      <input
        type="text"
        value={inputValue}
        onChange={handleInputChange}
        placeholder="Add a todo"
      />
      <button
        className={`add-button ${disabled ? "disabled" : ""}`}
        disabled={disabled}
        type="submit"
      >
        Add
      </button>
    </form>
  );
};
```

By separating the JSX code into individual files, we can improve **code organization** and **readability**, making it **easier to maintain** and understand the component structure.

Like this we need to split our **TodoItem** and **TodoList**.

This refactoring approach adheres to the SRP by assigning single responsibilities to each component, utilizes custom hooks for state and event handling, and separates the JSX code into reusable components, promoting modularity and maintainability in the React application.

Finally, the component structure will look like below,

```js
// ✅ Component Stucture

components/
├── todo-input/
│   ├── TodoInput.jsx
│   ├── useTodoInput.js
│   └── TodoInput.css
├── todo-item/
│   ├── TodoItem.jsx
│   └── TodoItem.css
├── todo-list/
│   ├── TodoList.jsx
│   ├── useTodoList.js
│   └── TodoList.css
└── ...
```
You can check it out the whole codebase in codesandbox.

{% embed https://codesandbox.io/embed/todo-srp-atom-wroc4o?fontsize=14&hidenavigation=1&module=%2Fsrc%2FTodo%2Fcontainer%2FTodo.jsx&theme=dark&view=editor %}

We can further refactor this codebase using Atomic Design principles.

## Atomic Design Principles

Atomic Design is a methodology for designing and organizing components in a **hierarchical manner** based on **their level of abstraction** and **complexity**.

It classifies components into five levels: Atoms, Molecules, Organisms, Templates, and Pages, with each level having a specific responsibility.

- **Atoms**: At the lowest level, atoms represent the smallest and most basic UI elements, such as buttons, inputs, or icons. 

> They have a single responsibility, focusing on their visual appearance and basic functionality.

- **Molecules**: Molecules are combinations of atoms that work together to create more complex UI elements. 

> They have a slightly higher level of responsibility, representing a group of related atoms.

- **Organisms**: Organisms are composed of molecules and atoms, representing larger and more self-contained sections of a user interface. 

> They have more complex behavior and may include state management and interaction logic.

- **Templates**: Templates are specific arrangements of organisms that provide a basic structure for a page or section. 

> They define the overall layout and composition of the UI.

- **Pages**: Pages are instances where templates are populated with real data, creating actual content for the user to interact with.

Let's take an example of same todo app. I will give an high level code design using the Atomic Design Pattern:

### Atoms

Atoms contains small, reusable UI components like **Button** and **Input**.

```js
// ✅ Atoms

// Button.jsx
const Button = ({ onClick, children }) => {
  return (
    <button className="button" onClick={onClick}>
      {children}
    </button>
  );
};

//Input.jsx
const Input = ({ value, onChange }) => {
  return (
    <input className="input" type="text" value={value} onChange={onChange} />
  );
};
```

Each atom has its own JavaScript file (`Button.jsx`, `Input.jsx`) and CSS file (`Button.css`, `Input.css`).

### Molecules

The molecules directory contains combinations of atoms (Button.jsx) that form more complex components, such as the the **TodoItem** component.

```js
// ✅ Molecules

// TodoItem.jsx
const TodoItem = ({ todo, onDelete, onComplete }) => {
  return (
    <li className="todo-item">
      <span className={todo.completed ? 'completed' : ''}>{todo.text}</span>
      <Button onClick={onDelete}>Delete</button>
      <Button onClick={onComplete}>
        {todo.completed ? 'Mark Incomplete' : 'Mark Complete'}
      </Button>
    </li>
  );
};
```

It has its own JavaScript file (**TodoItem.js**) and CSS file (**TodoItem.css**).

### Organisms

The organisms directory contains larger, more feature-rich components, such as the **TodoForm** and **TodoList** components. 

```js
// ✅ Organisms

// TodoForm.jsx
const TodoForm = ({ onAddTodo }) => {
  const {inputChange, addTodo} = useTodoForm();

  return (
    <div className="todo-form">
      <Input value={newTodo} onChange={inputChange} />
      <Button onClick={addTodo}>Add Todo</Button>
    </div>
  );
};

// TodoList.jsx
const TodoList = ({ todos, onDeleteTodo, onCompleteTodo }) => {
  return (
    <ul className="todo-list">
      {todos.map((todo, index) => (
        <TodoItem
          key={index}
          todo={todo}
          onDelete={() => onDeleteTodo(index)}
          onComplete={() => onCompleteTodo(index)}
        />
      ))}
    </ul>
  );
};
```

They are composed of **molecules** and/or **atoms** and have their own JSX(TodoForm.jsx, TodoList.jsx), Custom Hooks(useTodoForm.js) and **CSS** files.

### Templates

The templates contains components that provide the overall structure of a page or layout. In this case, the **Todo** template is responsible for rendering the **TodoForm** and **TodoList** components. 

```js
// ✅ Templates

// Todo.jsx
const Todo = () => {
  const { 
    todos, 
    addTodo, 
    deleteTodo, 
    completeTodo 
  } = useTodo();
  
  return (
    <div className="todo-app">
      <h1>Todo App</h1>
      <TodoForm onAddTodo={addTodo} />
      <TodoList
        todos={todos}
        onDeleteTodo={deleteTodo}
        onCompleteTodo={completeTodo}
      />
    </div>
  );
};
```

It has its own JSX file (`Todo.jsx`) and Custom Hook (`useTodo.js`) and CSS file (`Todo.css`).

### Pages

The pages directory components that represent a specific page in the application. In this example, there is a **HomePage** component that serves as the main entry point of the Todo app. 

```js
// ✅ Pages

// HomePage.js
const HomePage = () => {
  return (
    <div className="home-page">
      <TodoApp />
    </div>
  );
};
```

This example demonstrates how the Todo app codebase can be structured using the Atomic Design pattern. Each component is responsible for a single concern, and they can be easily reused and composed to build the complete Todo app.

## Final Thoughts

When designing your React app, it's essential to avoid assigning multiple responsibilities to a single component. Here are some practical strategies to help you achieve a cleaner and more maintainable codebase:

**1. Identify clear responsibilities**: Clearly define the purpose of each component. Break down complex functionalities into smaller, focused components with well-defined responsibilities.

**2. Separation of concerns**: Separate concerns by dividing your app into distinct components based on their functionality. Each component should have a specific role and handle a single responsibility.

**3. Component composition**: Instead of building large components that handle multiple tasks, compose your UI by combining smaller, reusable components. This promotes reusability and modularity.

**4. Single-task functions**: Extract complex logic from components into separate functions or utility modules. By encapsulating specific functionalities in separate functions, you keep your components focused on rendering and UI-related tasks.

**5. Follow the SOLID principles**: Adhere to SOLID principles, such as the Single Responsibility Principle (SRP), which states that a component should have only one reason to change. This principle helps you design components that are focused, maintainable, and easier to test.

**6. Use custom hooks:** Extract common logic into custom hooks that can be shared across components. This allows you to reuse logic without introducing unnecessary complexity to individual components.

**7. Modular architecture**: Organize your codebase using a modular architecture, such as the feature-based folder structure. This approach promotes separation of concerns and helps in keeping components focused on their specific responsibilities.

By consciously designing your React app with these practices in mind, you can avoid assigning multiple responsibilities to components. This leads to cleaner, more maintainable code that is easier to understand, test, and extend.

## Bonus - Component Hierarchy

It is generally recommended to follow a specific component hierarchy to maintain consistency and readability in your codebase. 

```js
// ✅ Component Hierarchy

// External dependencies
import React, { useState, useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';

// Internal dependencies
import { TodoItem } from './TodoItem';
import { TodoUtils } from '../utils';
import { useTodo } from '../hooks';
import { withTimer } from '../hoc';
import { TodoType } from '../enums';

// Stylesheets
import './Component.css';
import '../styles/common.css';

// Assets
import todoImage from '../assets/todoImage.png';

const Todo = () => {
  // State logic
  const [todos, setTodos] = useState([]);

  // Ref
  const inputRef = useRef(null);

  // Variable
  const title = 'Todo List';

  // Custom hook
  const {addTodo} = useTodo();

  // Higher-order component
  const timer = 
withTimer(TodoItem);

  // Component lifecycle methods (useEffect)
  useEffect(() => {
   //...
  }, []);

  // Component render
  return (
    <div>
      {/* Component JSX */}
    </div>
  );
}

Todo.propTypes = {
  // Prop types declaration
};

export { Todo };
```

By structuring your component hierarchy in a consistent and organized manner, you can improve the readability, maintainability, and scalability of your React app. 

A well-defined hierarchy helps developers navigate the codebase, understand component relationships, and make modifications efficiently.

Stay tuned for more tips and tricks on building high-quality React applications in my future blog posts!

Happy coding!😊👩‍💻👨‍💻