React is a popular JavaScript library for building user interfaces, and it offers a lot of flexibility and power for front-end developers. However, as with any programming language, it is essential to write clean, readable, and maintainable code. One way to achieve this is to use TypeScript, which offers static typing and improved code organization.

Another way to improve code quality that has quickly been gaining traction recently is to use AI tools that can suggest code improvements based on the context of your code. In this blog post, we’ll explore how to use both ChatGPT and Github Copilot X to refactor a React component and improve its readability, modularity, and use TypeScript.

Our starting point is the following component:

import React, { useState, useEffect } from "react";

const SpaghettiComponent = (props) => {
  const [counter, setCounter] = useState(0);
  const [isActive, setIsActive] = useState(false);
  const [message, setMessage] = useState("");
  useEffect(() => {
    if (isActive) {
      document.title = `Counter: ${counter}`;
    } else {
      document.title = "React App";
    }
  }, [counter, isActive]);

  const handleClick = () => {
    setCounter(counter + 1);
  };

  const handleToggle = () => {
    setIsActive(!isActive);
    if (isActive) {
      setMessage("Active");
    } else {
      setMessage("Inactive");
    }
  };

  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        justifyContent: "center",
        height: "100vh",
      }}
    >
      <p>Counter: {counter}</p>
      <button onClick={handleClick}>Increment</button>
      <br />
      <span>Status: {message}</span>
      <button onClick={handleToggle}>
        {isActive ? "Deactivate" : "Activate"}
      </button>
    </div>
  );
};

export default SpaghettiComponent;

This component is a functional component, using hooks and inline styles, which are all valid ways to write React code. However, as the component grows and more functionality is added, it can become more complex and harder to maintain. Additionally, the component mixes the logic of updating the document title and the message status into one useEffect hook, which can make the code less modular and harder to read. Finally, we would like to convert this component to Typescript, which can help us catch errors and ensure code quality.

To address these issues, we decided to use ChatGPT, a language model developed by OpenAI that can suggest code snippets based on the context of your code. We prompted ChatGPT to help us refactor the code with the following message: “Refactor this component to make it more readable, modular, and to use Typescript.” By giving ChatGPT this specific prompt, we were able to focus on the key areas of improvement that we wanted to target. ChatGPT then suggested a refactored version of the component that addressed these issues and made the code more readable and modular, while also adding Typescript types to the component.

This is what ChatGPT suggested:

import React, { useState, useEffect } from "react";

interface SpaghettiComponentProps {}

const SpaghettiComponent: React.FC<SpaghettiComponentProps> = () => {
  const [counter, setCounter] = useState<number>(0);
  const [isActive, setIsActive] = useState<boolean>(false);
  const [message, setMessage] = useState<string>("");

  useEffect(() => {
    updateDocumentTitle();
  }, [counter, isActive]);

  const updateDocumentTitle = () => {
    if (isActive) {
      document.title = `Counter: ${counter}`;
    } else {
      document.title = "React App";
    }
  };

  const handleClick = () => {
    setCounter(counter + 1);
  };

  const handleToggle = () => {
    setIsActive(!isActive);
    updateMessage();
  };

  const updateMessage = () => {
    if (isActive) {
      setMessage("Active");
    } else {
      setMessage("Inactive");
    }
  };

  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        justifyContent: "center",
        height: "100vh",
      }}
    >
      <p>Counter: {counter}</p>
      <button onClick={handleClick}>Increment</button>
      <br />
      <span>Status: {message}</span>
      <button onClick={handleToggle}>
        {isActive ? "Deactivate" : "Activate"}
      </button>
    </div>
  );
};

export default SpaghettiComponent;


// In this refactored version of the component:

// 1. I've defined a TypeScript interface for the component's props.
// 2. I've defined the component as a function that takes no props and returns JSX.
// 3. I've added TypeScript types to the `useState` hooks to enforce proper typing of state values.
// 4. I've split the `useEffect` logic into a separate function, `updateDocumentTitle`, to improve modularity and readability.
// 5. I've split the `handleToggle` logic into a separate function, `updateMessage`, to improve modularity and readability.
// 6. I've added proper indentation and spacing to improve readability.

Here are some of the changes that ChatGPT made:

  • Defining the component as a function that takes no props and returns JSX
  • Adding TypeScript types to the useState hooks to enforce proper typing of state values
  • Splitting the handleToggle logic into a separate function, updateMessage, to improve modularity and readability

Overall, these changes aim to make the code more maintainable, readable, and modular. By separating the logic into smaller, more manageable functions, the component becomes easier to understand and less prone to errors. Additionally, adding TypeScript types helps prevent type errors and makes it easier to work with the component in larger projects.

In addition to the suggestions made by ChatGPT, we can also make our own improvements to the code using Github Copilot X. Copilot X is a more advanced version of Github’s popular code suggestion tool, and it uses machine learning to generate more accurate and context-sensitive code suggestions.

To use Copilot X, we can specify a “brush” to help guide the suggestions. For example, we can use the “more robust” brush to ask Copilot X to generate more robust and fault-tolerant code. As a result, the updating of state is more robust, and the code is less likely to fail due to unexpected errors or edge cases.

We can also use the “custom” brush to ask Copilot X to generate code that makes more use of TypeScript’s implicit typing feature. This feature allows TypeScript to infer the type of a variable from its initial value, rather than requiring explicit typing for every variable. As a result, the code is more concise and easier to read.

However, even with the help of Copilot X, we may still need to make some manual adjustments to the code to ensure that it meets our specific needs and standards. For example, Copilot X may generate code that includes explicit typing for component props, even if we’re not using them. In these cases, we can make the necessary adjustments ourselves to ensure that the code is as clean and efficient as possible.

Here is the finalized component:

import React, { useState, useEffect } from "react";

const SpaghettiComponent = () => {
  const [counter, setCounter] = useState(0);
  const [isActive, setIsActive] = useState(false);
  const [message, setMessage] = useState("");

  useEffect(() =>  updateDocumentTitle(), [counter, isActive]);

  const updateDocumentTitle = () => {
    if (isActive) {
      document.title = `Counter: ${counter}`;
    } else {
      document.title = "React App";
    }
  };

  const updateMessage = () => {
    if (isActive) {
      setMessage("Active");
    } else {
      setMessage("Inactive");
    }
  };

  const handleClick = () => {
    setCounter((prevCounter) => prevCounter + 1);
  };

  const handleToggle = () => {
    setIsActive((prevIsActive) => !prevIsActive);
    updateMessage();
  };

  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        justifyContent: "center",
        height: "100vh",
      }}
    >
      <p>Counter: {counter}</p>
      <button onClick={handleClick}>Increment</button>
      <br />
      <span>Status: {message}</span>
      <button onClick={handleToggle}>
        {isActive ? "Deactivate" : "Activate"}
      </button>
    </div>
  );
};

export default SpaghettiComponent;

// In this refactored version of the component:

// 1. I've removed the explicit typing from the `useState` hooks. TypeScript is smart enough to infer the types from the initial values.
// 2. I've removed the explicit typing from the component's props interface. TypeScript is smart enough to infer the types from the component's props.

// In this refactored version of the component:

// 1. I've defined a TypeScript interface for the component's props.
// 2. I've defined the component as a function that takes no props and returns JSX.
// 3. I've added TypeScript types to the `useState` hooks to enforce proper typing of state values.
// 4. I've split the `useEffect` logic into a separate function, `updateDocumentTitle`, to improve modularity and readability.
// 5. I've split the `handleToggle` logic into a separate function, `updateMessage`, to improve modularity and readability.
// 6. I've added proper indentation and spacing to improve readability.

Bonus: Add Tests!

What if we now want to add tests to this code? There’s bad news and then there’s good news. As far as I can tell, Copilot does not want to automatically write tests for a .tsx file, explaining that it currently “only supports Javascript and Typescript.”

However, we can just go back to ChatGPT. I asked ChatGPT to write at least 5 tests for the component, without specifying what type of tests or which methodology to use. Here’s what it came up with:

import React from "react";
import { render, fireEvent } from "@testing-library/react";
import SpaghettiComponent from "./SpaghettiComponent";

describe("SpaghettiComponent", () => {
  it("renders without throwing any errors", () => {
    expect(() => render(<SpaghettiComponent />)).not.toThrow();
  });

  it("increments counter by 1 when Increment button is clicked", () => {
    const { getByText } = render(<SpaghettiComponent />);
    const incrementButton = getByText("Increment");
    const counterText = getByText("Counter: 0");

    fireEvent.click(incrementButton);
    expect(counterText).toHaveTextContent("Counter: 1");

    fireEvent.click(incrementButton);
    expect(counterText).toHaveTextContent("Counter: 2");

    fireEvent.click(incrementButton);
    expect(counterText).toHaveTextContent("Counter: 3");
  });

  it("toggles isActive state when Activate/Deactivate button is clicked", () => {
    const { getByText } = render(<SpaghettiComponent />);
    const toggleButton = getByText("Activate");
    const statusText = getByText("Status: Inactive");

    fireEvent.click(toggleButton);
    expect(statusText).toHaveTextContent("Status: Active");

    fireEvent.click(toggleButton);
    expect(statusText).toHaveTextContent("Status: Inactive");
  });

  it("updates document title to 'React App' when isActive is false", () => {
    render(<SpaghettiComponent />);
    expect(document.title).toBe("React App");
  });

  it("updates document title to 'Counter: [counter]' when isActive is true", () => {
    const { getByText } = render(<SpaghettiComponent />);
    const toggleButton = getByText("Activate");

    fireEvent.click(toggleButton);
    expect(document.title).toBe("Counter: 0");

    fireEvent.click(toggleButton);
    fireEvent.click(incrementButton);
    expect(document.title).toBe("Counter: 1");

    fireEvent.click(incrementButton);
    fireEvent.click(incrementButton);
    expect(document.title).toBe("Counter: 3");
  });

  it("updates message to 'Active' when isActive is true and 'Inactive' when isActive is false", () => {
    const { getByText } = render(<SpaghettiComponent />);
    const toggleButton = getByText("Activate");
    const statusText = getByText("Status: Inactive");

    fireEvent.click(toggleButton);
    expect(statusText).toHaveTextContent("Status: Active");

    fireEvent.click(toggleButton);
    expect(statusText).toHaveTextContent("Status: Inactive");
  });
});

Not bad!

Conclusion

In summary, we began with a functional React component that used hooks and inline styles, which while functional, could become complex and hard to maintain as it grows. We then used both ChatGPT and Github Copilot X to suggest code improvements and make it more readable, modular, and use TypeScript. We started with ChatGPT to prompt it to help us refactor the code, which led to a refactored version of the component with TypeScript types, more modular functions, and improved readability. We then used Github Copilot X to suggest even more improvements, such as more robust updating of state and implicit typing with TypeScript. We made a few last changes ourselves to come up with a final version of the component that was both clean and efficient. Finally, we asked it to write 5 tests for the component.

It’s essential to remember that while Copilot is a useful tool, it has its limitations and should not be relied on to generate perfect code. Developers should always review and double-check the generated code to ensure that it aligns with their coding standards and preferences. It’s important to use Copilot responsibly and always prioritize good coding practices.

Overall, by using Copilot in combination with other best practices, developers can write cleaner, more modular, and more maintainable code.