DEV Community

Cover image for How To Refactor Your Codebase
Juliet Ofoegbu
Juliet Ofoegbu

Posted on

How To Refactor Your Codebase

Introduction

Programming is the process of writing programs or code to solve problems. The process of writing code is only the beginning of the development journey.

As a project progresses and changes are made, the need to refactor the code arises and becomes unavoidable throughout the project’s lifecycle.

Nobody likes a messy codebase, especially maintaining one they didn't initially write. If you were asked to refactor someone's codebase or your messy codebase, how would you do this?

In this article, we'll learn what code refactoring is all about and how to go about it.

Code Refactoring

Code refactoring is the act of restructuring the existing code without affecting its behavior and functionality. The main idea behind code refactoring is to improve code readability and maintainability, hence contributing to the overall quality of software or an application.

The first step in refactoring a codebase is identifying “code smells”.

Code Smells

Code smells are signs that indicate that there is an issue with your code. Code smells are not necessarily bugs or errors and they won’t prevent your code from functioning as it should. They are like ‘signals’ that warn you of pending problems in your code.

Identifying these code smells early in the development process helps you avoid several issues later on. Regular code reviews, utilizing code analysis tools like SonarQube, running tests, and adopting clean coding practices are all necessary for early detection of code smells.

Benefits of Code Refactoring

Code refactoring offers you some benefits that contribute to the overall quality of your software project. Here are some key benefits of code refactoring:

  1. Improves code readability and reusability: Refactoring involves the process of simplifying code structure to make the codebase more readable, even for other developers who will read or use your code. It also often requires breaking down functions or classes into reusable components.

  2. Improves performance: Identifying and improving inefficient algorithms in the refactoring process can lead to more efficient software. This in turn enhances the performance of the application.

  3. Enhances code maintainability: Code refactoring increases code maintainability by breaking down large functions and structuring them logically. When large code functions are broken down into smaller units, they are easier to manage and maintain.

  4. Reduces technical debt: Technical debt is incurred when programmers implement quick solutions or shortcuts during software development which can result in bad code or an unstructured codebase. Regularly refactoring your code helps you reduce technical debt by reducing the accumulation of poorly structured code, eliminating code smells, improving code quality, and making future development smoother and more efficient.

  5. Enhance team collaboration: When a codebase is refactored, it becomes easier to understand and maintain. This fosters better collaboration among team members as they can understand each other's code more easily.

Code Refactoring Tools and Techniques

As a programmer, there are several tools and techniques that you can utilize to improve the quality and maintainability of your code.

Code Refactoring Tools

These tools are software applications that help programmers restructure and improve the quality of their code while maintaining its functionality.

Examples of code refactoring tools include:

  • CodeRush: CodeRush is a refactoring tool in Visual Studio Code for .NET development. It offers features for enhancing code quality, including code refactoring, code generation, and code analysis to improve code readability. Here's a guide on how to use the CodeRush refactoring assistance on VS Code.

  • IntelliJ IDEA: IntelliJ IDEA is a popular (IDE) for Java and Kotlin development. It offers a lot of automatic refactoring capabilities like renaming, extracting, and deleting to improve code maintainability and readability.

  • Visual Studio Code: Visual Studio Code (VS Code) is a popular open-source code editor that supports refactoring methods like the extraction method. VS Code also supports a range of extensions including those for code refactoring.
    An example of this is the built-in support for TypeScript and JavaScript refactoring through the TypeScript language service. Also in VS Code, Code Actions (the light bulb icon indicating possible fixes to a code) can provide both refactoring and Quick Fixes for detected issues

  • ReSharper: ReSharper is a Visual Studio IDE Extension for .NET developers. It offers refactoring features, code analysis, code generation, and code navigation tools.
    Using the 'Refactor This' action provides you with a shortcut of all the refactoring available in the current context of your code.

Code Refactoring Techniques

1. Extraction Method

This involves breaking down a large piece of code that performs a specific function into smaller, simpler portions by creating a new method. This improves the code structure, readability, and reuse.

Example: Assume you have a component whose function is to add items to a shopping cart and then calculate the total price of items in the cart.
If the calculation part is getting bulky and taking up a lot of space in the component, you can decide to extract that calculation into its separate function called calculateTotalPrice. This way, the main function becomes simpler and easier to understand.

2. Abstraction Method

This involves reorganizing code into a high-level concept to focus on the main idea. This reduces code redundancy, eliminates duplication, and improves code structure.

Example: Assume you have a program that checks and calculates the area of different shapes like triangles, squares, etc. Each of these shapes has its method for calculating area. You can decide to separately implement these methods for each shape but this will lead to code duplication and affect the code maintainability.

Instead of doing this, you can use the abstraction method to refactor this code by creating a higher-level function called calculateArea. This function will then take the shape as input and calculate its area based on the type.

Code snippet:

import React from 'react';

// Functional component for Shape
function Shape({ type, calculateArea }) {
    return (
        <div>
            <h2>{type}</h2>
            <p>Area: {calculateArea()}</p>
        </div>
    );
}

// Component for Circle
function Circle({ radius }) {
    const calculateArea = () => Math.PI * radius ** 2;
    return <Shape type="Circle" calculateArea={calculateArea} />;
}

// Component for Square
function Square({ sideLength }) {
    const calculateArea = () => sideLength ** 2;
    return <Shape type="Square" calculateArea={calculateArea} />;
}

// Component for Triangle
function Triangle({ base, height }) {
    const calculateArea = () => 0.5 * base * height;
    return <Shape type="Triangle" calculateArea={calculateArea} />;
}

// Main Component
function App() {
    return (
        <div>
            <Circle radius={6} />
            <Square sideLength={4} />
            <Triangle base={3} height={6} />
        </div>
    );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

3. Code Simplification

This involves identifying and simplifying complex functions or unnecessary code and this increases code readability.

Example: Let's say you have a function that filters out even numbers from an array and returns a new array containing only the even numbers. Initially, the function might include redundant code and be unnecessarily complex. We can simplify it by removing redundancy and simplifying the logic.

Initial code version:

function filterEvenNumbers(arr) {
  let evenNumbers = [];

  for (let i = 0; i < arr.length; i++) {
    if (arr[i] % 2 === 0) {
      evenNumbers.push(arr[i]);
    }
  }

  return evenNumbers;
}
Enter fullscreen mode Exit fullscreen mode

Refactored code:

function filterEvenNumbers(arr) {
  return arr.filter(num => num % 2 === 0);
}
Enter fullscreen mode Exit fullscreen mode

Here, the filter method is used to create a new array containing only the elements that satisfy the condition specified in the callback function. This eliminates the need for a separate loop and the creation of a new array to store the even numbers.

4. Red-Green-Refactor

In test-driven development (TDD), this method involves you writing a test that fails (Red), writing the minimum amount of code to make the test pass (Green), and then improving the code's structure and readability without changing its behavior (Refactor).

Steps in Refactoring a Codebase

Following these steps when refactoring a codebase can help you enhance the quality of your code:

1. Understand the codebase
Before refactoring a codebase especially one you are not familiar with, it's important to take out time to understand the existing codebase thoroughly.

It includes familiarizing yourself with the design patterns, coding conventions, architecture, and existing documentation.

2. Identify code smells
As explained in a previous section, code smells are indicators of potential issues in the code that may warrant refactoring. Common code smells include complex conditional logic, large classes, duplication, and too many dependencies.

Use code analysis tools (SonarQube, Coverity, DeepSource, Codacy, etc) or manual code reviews to identify these code smells.

3. Prioritize refactoring targets
In every codebase, there are high-priority target areas that are either frequently changed, prone to bugs or are very crucial for performance. Some areas may not require immediate attention.

Consider prioritizing refactoring targets based on factors like how it changes frequently, its impact on the whole software, and the seriousness of the code smell.

4. Choose the right refactoring technique
There are lots of techniques available for code refactoring like the ones mentioned in the previous section. Choose the best technique that is suitable for addressing the identified code smells in the codebase. Choose techniques that improve code readability and maintainability.

5. Plan refactoring tasks
Before you start refactoring the codebase, think about the specific improvements to be made and break down the tasks into smaller units so they can be easily managed.
Write down what needs to be done and how these changes might affect other parts of the code.

6. Execute refactoring safely
When you're making changes to the code, do it little by little and test each change to make sure it doesn't break anything or have any bugs. Use version control systems, like GitHub to keep track of the changes you're making, so if something goes wrong, you can easily track it and go back to the previous version.

Also, follow good practices like writing unit tests, having others review your code, and using automated testing tools (JUnit for Java and pytest for Python for writing unit tests).

7. Feedback, monitor, and iterate
After making changes to the code, pay attention to how they affect the code quality, its performance, and the effect on developer productivity. Ask for feedback from your team and anyone else involved. Use this feedback to make further improvements.

Step-by-step guide for using the VS Code Extension - Glean to refactor a React codebase

VS Code Glean is a VS Code extension that provides refactoring features for your React codebase such as extracting JSX into a new component, converts class components to functional components, converting functional components to class components, wrapping with Hooks and more.

Here's a step-by-step guide on how to use Glean to refactor a complex codebase directly in your VS Code editor:

  • Step 1: Open your Next.js or React project in VS Code
    Launch Visual Studio Code and open your project.

  • Step 2: Install Glean extension
    Navigate to the VS Code extensions marketplace or press (Ctrl+Shift+X) and type 'glean' in the search box. This will bring up the Glean extension code refactoring tools. Select the first option that comes up and click on the "Install" button.

  • Step 3: Use Glean refactoring features
    Once you've identified areas for refactoring, use Glean's refactorings to make the necessary changes. Here are some examples of refactorings you can perform with Glean:

    • A. Extract Component: You can extract a portion of JSX code into a new React component. Select the block of code, click on the light bulb icon, and select the "Extract Component to File" option. A dropdown with a list of the directories you can extract your component to will appear.

You can either select a component you want it extracted to or you can input a new file you want the component to be extracted to, e.g., '/Form.tsx'. Hit enter. Doing this will extract that JSX code to a new component called Form.tsx while providing it with all the props it needs.

Extracting a component

  • B. Conversion: You can seamlessly convert class components to functional components and vice versa. All you need to do is highlight everything in the component file. Next, click the light bulb icon. Select the "Convert Function to Class component" or the "Convert Class to Function component" option depending on whether it's a functional or state component you're converting from.

Converting function to class component

  • C. Rename: Use the "Rename" refactoring to quickly rename any state variables, functions, or components throughout your codebase. Highlight the variable you intend to rename, click on the light bulb icon, and then choose the "Rename State" option. This will prompt you to enter the name you want, hit enter and it will rename the state variable for you.

Renaming a state variable

  • Step 4: Review, test, and commit After refactoring, review your changes to ensure they haven't introduced any bugs. Once you're satisfied with the refactored code, commit your changes to your version control system if you need to.

By following these steps and using Glean's refactoring features, you can effectively refactor a more complex codebase, improving its maintainability and readability.

NB: For more VS Code refactoring extensions, navigate to the extensions marketplace view in your VS Code or press (Ctrl+Shift+X) and type 'refactor' in the search box. This will bring up some options for code refactoring tools.

Refactoring extensions from VS code marketplace

Some are specifically for different programming languages. You can then sort these results by popularity, category, recently published, and so on.

Always conduct proper research on the particular refactoring tool you intend to use. Check for the number of downloads, ratings, and support that the tool has.

When You Should Consider Refactoring Your Code

Here are some common scenarios when you should consider refactoring your code:

  • When introducing new features that change an existing code, consider refactoring.

  • During code reviews, a team member can highlight areas of code that are complex, have code smells, or violate coding standards. This is an ideal time to refactor your code.

  • If a code is extremely complex or adds to the problem, refactor it to address bugs and errors that will arise during development.

A point to note is that code refactoring is NOT fixing bugs in your code. It is NOT adding new features. Code refactoring is deeper than all these and it’s done to improve the codebase's internal structure.

Refactoring a full codebase

We've learned what it means to refactor a codebase and how it can be done using different tools and techniques. I also provided examples and small code snippets illustrating how to refactor some parts of a component/file. To make sure we understand this well, I'll demonstrate all we've learned by refactoring a full codebase.

The project codebase we'll be refactoring is an invoicing application I recently built using Nextjs, TypeScript, Tailwindcss for styling, and Strapi as the headless CMS.

Here's the GitHub page for the project. Study the codebase, you'll notice that originally only 3 files were used to build the functionalities.

We'll focus on breaking down the code into more manageable pieces, improving readability, and maintaining functionality. Here's how you can approach this:

Refactor the 'InvoiceForm' component

First, we'll refactor the code components for the 'InvoiceForm' component.

We'll be using three techniques to refactor this codebase.

1. Extraction Method
Extract repetitive UI elements into reusable components:

  • Create a new component for input fields (InputField.tsx):
// components/InputField.tsx
import React, { ChangeEvent } from 'react';

interface InputFieldProps {
  label: string;
  id: string;
  name: string;
  type: string;
  placeholder: string;
  value: string | number;
  onChange: (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
  required?: boolean;
}

const InputField: React.FC<InputFieldProps> = ({ label, id, name, type, placeholder, value, onChange, required = false }) => (
  <div className="flex flex-col w-[45%] mb-4">
    <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor={id}>
      {label}
    </label>
    <input
      className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
      id={id}
      name={name}
      type={type}
      placeholder={placeholder}
      onChange={onChange}
      value={value}
      required={required}
    />
  </div>
);

export default InputField;
Enter fullscreen mode Exit fullscreen mode
  • Create a new component for textarea fields (TextAreaField.tsx):
// components/TextAreaField.tsx
import React, { ChangeEvent } from 'react';

interface TextAreaFieldProps {
  label: string;
  id: string;
  name: string;
  placeholder: string;
  value: string;
  onChange: (e: ChangeEvent<HTMLTextAreaElement>) => void;
  required?: boolean;
}

const TextAreaField: React.FC<TextAreaFieldProps> = ({ label, id, name, placeholder, value, onChange, required = false }) => (
  <div className="flex flex-col w-[45%] mb-4">
    <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor={id}>
      {label}
    </label>
    <textarea
      className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
      id={id}
      name={name}
      placeholder={placeholder}
      onChange={onChange}
      value={value}
      required={required}
    />
  </div>
);

export default TextAreaField;
Enter fullscreen mode Exit fullscreen mode

2. Abstraction Method
Abstract the form state management logic into a custom hook (useInvoiceForm.tsx):

// hooks/useInvoiceForm.ts
import { useReducer, useEffect } from 'react';
import { Invoice } from '../components/InvoiceForm';

const initialState = {
  name: '',
  senderEmail: '',
  recipientEmail: '',
  shippingAddress: '',
  date: '',
  dueDate: '',
  invoiceNote: '',
  description: '',
  qty: 0,
  rate: 0,
  total: 0,
};

function reducer(state = initialState, { field, value }: { field: string, value: any }) {
  return { ...state, [field]: value };
}

const useInvoiceForm = (selectedInvoice: Invoice | null) => {
  const [formFields, dispatch] = useReducer(reducer, initialState);

  useEffect(() => {
    if (selectedInvoice) {
      for (const [key, value] of Object.entries(selectedInvoice?.attributes)) {
        dispatch({ field: key, value });
      }
    } else {
      for (const [key, value] of Object.entries(initialState)) {
        dispatch({ field: key, value });
      }
    }
  }, [selectedInvoice]);

  useEffect(() => {
    const { qty, rate } = formFields;
    const total = qty * rate;
    dispatch({ field: 'total', value: total });
  }, [formFields.qty, formFields.rate]);

  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
    const { name, value } = e.target;
    dispatch({ field: name, value });
  };

  return { formFields, handleInputChange };
};

export default useInvoiceForm;
Enter fullscreen mode Exit fullscreen mode

3. Code Simplification
Simplify the 'InvoiceForm' component using the new reusable components and the custom hook:

// components/InvoiceForm.tsx
'use client';
import React from 'react';
import axios from 'axios';
import InputField from './InputField';
import TextAreaField from './TextAreaField';
import useInvoiceForm from '../hooks/useInvoiceForm';

interface InvoiceFormProps {
  onClose: () => void;
  setInvoices: React.Dispatch<React.SetStateAction<Invoice[]>>;
  selectedInvoice: Invoice | null;
}

interface Invoice {
  id: number;
  name: string;
  attributes: any;
  senderEmail: string;
  recipientEmail: string;
  shippingAddress: string;
  date: string;
  dueDate: string;
  invoiceNote: string;
  description: string;
  qty: number;
  rate: number;
  total: number;
}

const InvoiceForm: React.FC<InvoiceFormProps> = ({ onClose, setInvoices, selectedInvoice }) => {
  const { formFields, handleInputChange } = useInvoiceForm(selectedInvoice);

  const handleSendInvoice = async () => {
    try {
      const { name, senderEmail, recipientEmail, date, dueDate, shippingAddress, invoiceNote, description, qty, rate, total } = formFields;

      if (selectedInvoice) {
        const data = await axios.put(`http://localhost:1337/api/invoices/${selectedInvoice.id}`, {
          data: { name, senderEmail, recipientEmail, shippingAddress, dueDate, date, invoiceNote, description, qty, rate, total },
        });
        setInvoices((prev) => prev.map((inv) => (inv.id === selectedInvoice.id ? { ...inv, ...formFields } : inv)));
        window.location.reload();
      } else {
        const { data } = await axios.post('http://localhost:1337/api/invoices', {
          data: { name, senderEmail, recipientEmail, shippingAddress, dueDate, date, invoiceNote, description, qty, rate, total },
        });
        setInvoices((prev) => [...prev, data.data]);
      }

      onClose();
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <main className="fixed top-0 z-50 left-0 w-screen h-screen flex justify-center items-center bg-black bg-opacity-50">
      <section className="relative lg:px-10 px-6 py-8 lg:mt-8 lg:w-[60%] bg-white shadow-md rounded px-8 pt-2 pb-8 mb-4">
        <form className="pt-4">
          <h2 className="text-lg font-medium mb-4">{selectedInvoice ? 'Edit Invoice' : 'Create Invoice'}</h2>
          <button className="absolute top-2 right-8 font-bold text-black cursor-pointer text-2xl" onClick={onClose}>
            &times;
          </button>
          <div className="mb-4 flex flex-row justify-between">
            <InputField
              label="Your name"
              id="name"
              name="name"
              type="text"
              placeholder="Sender's name"
              value={formFields.name}
              onChange={handleInputChange}
              required
            />
            <InputField
              label="Your email address"
              id="senderEmail"
              name="senderEmail"
              type="email"
              placeholder="Sender's email"
              value={formFields.senderEmail}
              onChange={handleInputChange}
              required
            />
            <InputField
              label="Recipient's Email"
              id="recipientEmail"
              name="recipientEmail"
              type="email"
              placeholder="Client's email address"
              value={formFields.recipientEmail}
              onChange={handleInputChange}
              required
            />
          </div>

          <div className="mb-4 flex flex-row justify-between">
            <InputField
              label="Date"
              id="date"
              name="date"
              type="date"
              value={formFields.date}
              onChange={handleInputChange}
              required placeholder={''}
            />

            <InputField
              label="Due Date"
              id="dueDate"
              name="dueDate"
              type="date"
              value={formFields.dueDate}
              onChange={handleInputChange}
              required
              placeholder={''}
            />
          </div>

          <div className="mb-4 flex flex-row justify-between">
            <TextAreaField
              label="Shipping Address"
              id="shippingAddress"
              name="shippingAddress"
              placeholder="Office address of recipient"
              value={formFields.shippingAddress}
              onChange={handleInputChange}
              required
            />
            <TextAreaField
              label="Invoice Note"
              id="invoiceNote"
              name="invoiceNote"
              placeholder="Account details"
              value={formFields.invoiceNote}
              onChange={handleInputChange}
              required
            />
          </div>

          <div className="flex justify-center items-center">
            <InputField
              label="Invoice Item"
              id="description"
              name="description"
              type="text"
              placeholder="Description of Item"
              value={formFields.description}
              onChange={handleInputChange}
              required
            />
            <InputField
              label="Qty"
              id="qty"
              name="qty"
              type="number"
              placeholder="Quantity of item"
              value={formFields.qty}
              onChange={handleInputChange}
              required
            />
            <InputField
              label="Rate"
              id="rate"
              name="rate"
              type="number"
              placeholder="Price of item"
              value={formFields.rate}
              onChange={handleInputChange}
              required
            />

            <div className="block text-gray-700 text-sm font-bold mb-2 w-full mr-3">
              <label>Total</label>
              <div className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight">
                {formFields.total}
              </div>
            </div>
          </div>

          <hr className="mt-5 border-1" />

          <div className="mt-4 flex justify-center">
            <button
              type="button"
              className="py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
              onClick={handleSendInvoice}
            >
              {selectedInvoice ? 'Update Invoice' : 'Send Invoice'}
            </button>
          </div>
        </form>
      </section>
    </main>
  );
};

export default InvoiceForm;
Enter fullscreen mode Exit fullscreen mode

Explanation

1. Reusable Components:
The InputField and TextAreaField components were created for reusability and cleaner code. These components simplify the form by reducing repetitive code for the inputs.

2. Custom Hook:
The useInvoiceForm custom hook handles the form state and logic, making the component leaner. It handles initial state setup, modal visibility, and updates based on selected invoice and field changes.

3. Refactored InvoiceForm component:
The InvoiceForm component then uses InputField and TextareaField for the form fields. This component then uses the useInvoiceForm hook for state management, making the component more readable and maintainable.

Refactor the 'Invoices' component

Now let's refactor the Invoices component and break it down.

1. Extraction Method
Extract the invoice header and invoice page layout into separate components.

  • Create the InvoiceHeader.tsx component.
// components/InvoiceHeader.tsx
import React from 'react';

interface InvoiceHeaderProps {
  onOpen: () => void;
}

const InvoiceHeader: React.FC<InvoiceHeaderProps> = ({ onOpen }) => (
  <div className="w-[65%] flex flex-row justify-between items-center mb-6">
    <h1 className="text-2xl font-bold">Invoices</h1>

    <button
      className="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded"
      onClick={onOpen}>
      Create Invoice
    </button>
  </div>
);

export default InvoiceHeader;
Enter fullscreen mode Exit fullscreen mode
  • Create the InvoiceLayout.tsx component.
// components/InvoiceLayout.tsx
import React from 'react';
import { Invoice } from '../types';
import useInvoiceDownload from '@/hooks/useInvoiceDownload';

interface InvoiceLayoutProps {
    invoices: Invoice[];
    onEdit: (invoice: Invoice) => void;
    onDelete: (id: number) => void;
    onDownload: (invoice: Invoice) => void;
}

const InvoiceLayout: React.FC<InvoiceLayoutProps> = ({ invoices, onEdit, onDelete, onDownload }) => (
    <div className="w-[70%]">
        <div className="px-5 py-5 mx-auto">
          {invoices.map((invoice) => (
                <>
                    <div className="flex flex-wrap border-t-2 border-b-2 border-gray-200 border-opacity-60" key={invoice.id}>
                        <div className="lg:w-1/3 md:w-full px-8 py-6 border-opacity-60">
                            <div>
                                <h2 className="text-base text-gray-900 font-medium mb-1">Issued:</h2>
                                <p className="leading-relaxed text-sm mb-4">{invoice.attributes.date}</p>
                            </div>
                            <div className="mt-12">
                                <h2 className="text-base text-gray-900 font-medium">Due:</h2>
                                <p className="leading-relaxed text-sm mb-4">{invoice.attributes.dueDate}</p>
                            </div>
                        </div>

                        <div className="lg:w-1/3 md:w-full px-8 py-6 border-l-2 border-gray-200 border-opacity-60">
                            <h2 className="text-base text-gray-900 font-medium mb-2">Billed To:</h2>
                            <div className="">
                                <h2 className=" text-gray-900 text-sm mb-1 font-medium">Recipient's Email</h2>
                                <p className="leading-relaxed text-sm mb-5">{invoice.attributes.recipientEmail}</p>
                            </div>

                            <div>
                                <h2 className=" text-gray-900 text-sm mb-1 font-medium">Shipping Address</h2>
                                <p className="leading-relaxed text-sm mb-4">{invoice.attributes.shippingAddress}</p>
                            </div>
                        </div>

                        <div className="lg:w-1/3 md:w-full px-8 py-6 border-l-2 border-gray-200 border-opacity-60">
                            <h2 className="text-base text-gray-900 font-medium mb-2">From:</h2>
                            <div className="">
                                <h2 className=" text-gray-900 text-sm mb-1 font-medium">Sender's Name</h2>
                                <p className="leading-relaxed text-sm mb-5">{invoice.attributes.name}</p>
                            </div>

                            <div>
                                <h2 className=" text-gray-900 text-sm mb-1 font-medium">Sender's Email</h2>
                                <p className="leading-relaxed text-sm mb-4">{invoice.attributes.senderEmail}</p>
                            </div>
                        </div>
                    </div>

                    <div className="w-full px-5 py-12 mx-auto">
                        <div className="flex flex-row justify-between border-b-2 border-gray-300">
                            <div>
                                <h2 className="text-lg font-medium text-gray-700 mb-2">Invoice Item</h2>
                            </div>

                            <div className="flex flex-row mb-2">
                                <p className="ml-2 text-lg font-medium text-gray-800">Qty</p>
                                <p className="ml-[6rem] text-lg font-medium text-gray-800">Rate</p>
                                <p className="ml-[6rem] text-lg font-medium text-gray-800">Total</p>
                            </div>
                        </div>

                        <div className="flex flex-row justify-between mt-4">
                            <div>
                                <h2 className="text-base text-gray-700 mb-4">{invoice.attributes.description}</h2>
                            </div>

                            <div className="flex flex-row mb-4">
                                <p className="ml-2 text-base text-gray-800">{invoice.attributes.qty}</p>
                                <p className="ml-[6rem] text-base text-gray-800">${invoice.attributes.rate}</p>
                                <p className="ml-[6rem] text-base text-gray-800">${invoice.attributes.total}</p>
                            </div>
                        </div>

                        <div className="grid justify-end pt-[2.5rem]">
                            <div className="flex flex-row justify-between">
                                <div>
                                    <h2 className="text-lg font-medium text-gray-700 mb-4">Tax (0%)</h2>
                                </div>

                                <div className="flex flex-row">
                                    <p className="ml-[10rem] text-base text-gray-800">0.00</p>
                                </div>
                            </div>

                            <div className="flex flex-row justify-between border-y-2 border-green-400">
                                <div className="pt-4">
                                    <h2 className="text-lg font-medium text-gray-700 mb-4">Amount due:</h2>
                                </div>

                                <div className="flex flex-row pt-4">
                                    <p className="ml-[10rem] text-lg font-medium text-gray-800">${invoice.attributes.total}.00</p>
                                </div>
                            </div>
                        </div>
                    </div>

                    <div className="flex flex-row justify-between w-full mt-1">
                        <div>
                            <button className="bg-blue-500 px-2 py-2 rounded text-white hover:bg-blue-600"
                                onClick={() => onDownload(invoice)}>
                                Download invoice
                            </button>

                            <button className="bg-green-500 px-2 py-2 rounded text-white hover:bg-green-600 ml-4"
                                onClick={() => onEdit(invoice)}>
                                Edit invoice
                            </button>
                        </div>

                        <div className="flex justify-end bg-red-400 px-2 py-2 rounded text-white hover:bg-red-500">
                            <button 
                                onClick={() => onDelete(invoice.id)}>
                                Delete invoice
                            </button>
                        </div>
                    </div>
                </>
            ))}
        </div>
    </div>
);

export default InvoiceLayout;
Enter fullscreen mode Exit fullscreen mode

2. Abstraction Method
Abstract the form state management logic into a custom hook (useInvoices.tsx).

// hooks/useInvoices.ts
import { useState, useEffect } from 'react';
import axios from 'axios';
import { Invoice } from '../types';

const useInvoices = () => {
  const [invoices, setInvoices] = useState<Invoice[]>([]);
  const [selectedInvoice, setSelectedInvoice] = useState<Invoice | null>(null);
  const [isModalOpen, setIsModalOpen] = useState(false);

  useEffect(() => {
    const fetchInvoices = async () => {
      try {
        const { data } = await axios.get('http://localhost:1337/api/invoices');
        setInvoices(data.data);
      } catch (error) {
        console.error(error);
      }
    };

    fetchInvoices();
  }, []);

  const handleDelete = async (id: number) => {
    try {
      alert("Are you sure you want to delete this invoice?")
      await axios.delete(`http://localhost:1337/api/invoices/${id}`);
      setInvoices(invoices.filter((invoice) => invoice.id !== id));
    } catch (error) {
      console.error(error);
    }
  };

  return {
    invoices,
    setInvoices,
    selectedInvoice,
    setSelectedInvoice,
    isModalOpen,
    setIsModalOpen,
    handleDelete,
  };
};

export default useInvoices;
Enter fullscreen mode Exit fullscreen mode
  • Create a custom hook (useInvoiceDownload.tsx) for the download functionality:
// hooks/useInvoiceDownload.ts
import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable';
import { Invoice } from '../types';

const useInvoiceDownload = () => {
  const downloadInvoice = (invoice: Invoice) => {
    const doc = new jsPDF();

    doc.setFontSize(18);
    doc.text('Invoice', 14, 22);

    autoTable(doc, {
      startY: 30,
      head: [['Name', 'Sender Email', 'Recipient Email', 'Date', 'Due Date', 'Total']],
      body: [
        [
          invoice.attributes.name,
          invoice.attributes.senderEmail,
          invoice.attributes.recipientEmail,
          invoice.attributes.date,
          invoice.attributes.dueDate,
          invoice.attributes.total,
        ],
      ],
    });

    doc.save(`invoice_${invoice.id}.pdf`);
  };

  return { downloadInvoice };
};

export default useInvoiceDownload;
Enter fullscreen mode Exit fullscreen mode
  • Create a types file for common types.
// types.tsx
export interface Invoice {
  id: number;
  attributes: {
    name: string;
    senderEmail: string;
    recipientEmail: string;
    shippingAddress: string;
    date: string;
    dueDate: string;
    invoiceNote: string;
    description: string;
    qty: number;
    rate: number;
    total: number;
  };
}
Enter fullscreen mode Exit fullscreen mode

3. Code Simplification
Refactor the main Invoice component by refactoring the Invoices component using the new reusable components and the custom hook:

// components/Invoices.tsx
'use client';
import React from 'react';
import InvoiceHeader from './InvoiceHeader';
import InvoiceLayout from './InvoiceLayout';
import InvoiceForm from './InvoiceForm';
import useInvoices from '../hooks/useInvoices';
import useInvoiceDownload from '../hooks/useInvoiceDownload';

const Invoices: React.FC = () => {
  const {
    invoices,
    setInvoices,
    selectedInvoice,
    setSelectedInvoice,
    isModalOpen,
    setIsModalOpen,
    handleDelete,
  } = useInvoices();

  const handleEdit = (invoice: Invoice) => {
    setSelectedInvoice(invoice);
    setIsModalOpen(true);
  };

  const { downloadInvoice } = useInvoiceDownload();

  return (
    <>
      <div className="flex flex-col items-center justify-center">
        <InvoiceHeader onOpen={() => setIsModalOpen(true)} />

        {isModalOpen && (
          <InvoiceForm
            onClose={() => setIsModalOpen(false)}
            setInvoices={setInvoices}
            selectedInvoice={selectedInvoice}
          />
        )}

        {invoices.length === 0 ? (
          <p>No invoice yet.</p>
        ) : (
          <InvoiceLayout invoices={invoices} onEdit={handleEdit} onDelete={handleDelete} onDownload={downloadInvoice} />
        )}
      </div>
    </>
  );
};

export default Invoices;
Enter fullscreen mode Exit fullscreen mode

We were able to refactor the full codebase of this web application using the refactoring techniques explained, making it reusable, readable, and maintainable without changing its functionalities.

Compare this with the original codebase of the project to see the differences. Test it out on your browser to see that the functionality doesn't change but the codebase did. The application components are now more reusable and can be easily maintained by anybody.

Conclusion

This article provides insights into the art of code refactoring, the identification of code smells, the benefits of code refactoring, code refactoring tools, and techniques, steps in refactoring a codebase, when you should consider refactoring a codebase, and what doesn’t count as code refactoring.

Code refactoring is an essential quality of a good programmer. So don’t hesitate to improve your programming skills by learning how to refactor your code to build readable and maintainable software applications.

Top comments (0)