DEV Community

Cover image for Part 3: Component Structure - Building Reusable and Maintainable Components in React!
Sathish Kumar N
Sathish Kumar N

Posted on

Part 3: Component Structure - Building Reusable and Maintainable Components in React!

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.

// ❌ 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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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

// ✅ 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 };
Enter fullscreen mode Exit fullscreen mode

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.

// 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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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,

// ✅ 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
└── ...
Enter fullscreen mode Exit fullscreen mode

You can check it out the whole codebase in codesandbox.

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.

// ✅ 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} />
  );
};
Enter fullscreen mode Exit fullscreen mode

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.

// ✅ 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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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.

// ✅ 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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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.

// ✅ 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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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.

// ✅ Pages

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

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.

// ✅ 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 };
Enter fullscreen mode Exit fullscreen mode

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!😊👩‍💻👨‍💻

Top comments (8)

Collapse
 
yoglib profile image
Yogev Boaron Ben-Har

I really like the concept of Atomic Design, it really makes the code a low more readable and testable...

Collapse
 
rv90904 profile image
Ramesh Vishnoi

Great article. You explained very important concepts which usually are not easily available and most neglated as well.

I liked the Atomic design principles concepts. Keep up the good work.

Collapse
 
iamhectorsosa profile image
Hector Sosa

Solid article and very well resourced!

Collapse
 
patzi275 profile image
Patrick Zocli

Congratulations, it's very informative. But I would have preferred you to briefly introduce us to the structure of the app before coming to the code. But it's really instructive.

Collapse
 
sathishskdev profile image
Sathish Kumar N • Edited

Thank you for your comment!

Collapse
 
muditchoudhary profile image
Mudit Choudhary

Thank you for this detailed guide I appreciate it. I understood both principles quite good with how you explained. I'm working on a personal project. I struggled a lot with code design and folder structure.

Collapse
 
borobudur01 profile image
borobudur01 • Edited

I am sorry to say I think this is very bad code because you are going way to far into abstracting and destructuring your features. Most of the principles your code is based on are good of course. You understand the how's but just don't understand the why's. As a result yes your code is highly "reusable" (but only in theory), very modular (way too modular) but extremely time consuming to maintain. Way.. way to complex.
Same for your project structures, it could be a lot simpler.
It's a nightmare for the guys coming after you my friend. May be learn about beauty and simplicity? I believe React totally destroyed a generation of devs. And your code is what we get now. That's actually sad.
Please guys, do not push those good practices to the extreme. They become bad practices. Understand what the goal of those practices are. Look at you code, is it easy to understand? Is everything at is place and coherent? Is making something hypothetically reusable by moving it miles away really needed? Is the structure of your code makes sense, could a non dev understand it at first glance? How many different scripts do you have to open to see how is a function functioning? Is your code pleasant to see or does it gives you a migraine after 10 minutes? Don't follow "good practices" blindly.

Collapse
 
benjaminv profile image
benjaminv

great bonus section.