Building a React Text Comparison Tool - From POC to NPM Package

chhakuli_zingare_2a0ad5f8

Chhakuli Zingare

Posted on October 11, 2024

Building a React Text Comparison Tool - From POC to NPM Package

Hey everyone! 👋 Chhakuli here. Today, I want to walk you through an exciting task I received at work: researching and building a text comparison tool. This tool’s job is to compare two pieces of text, highlight the differences, and present them in a visually intuitive way.

The need for this feature can arise in various contexts, such as document versioning, code reviews, or any application requiring a side-by-side comparison of text content. While it may sound straightforward, I quickly learned that building this feature comes with its own set of challenges.

In this post, I’ll share my research, the initial implementation I came up with, and how I transformed it into an NPM package. If you're ever tasked with something similar, I hope this will help you!

Understanding the Requirements

The initial brief seemed simple enough. Here’s what my tech lead outlined as the goals for the tool:

  1. Side-by-side view: Show both original texts side by side (easy peasy, right?)

  2. Combined version with highlights:

  • Common words should be shown in normal text.

  • Words present in the original but missing in the updated text should appear in red (as if removed).

  • Words added in the updated version should be shown in green (as if added).

At first glance, it might seem like a small task. But as I started thinking through the details, I realized there were quite a few considerations. For example:

  • Should we compare word-by-word or character-by-character?

  • How do we handle larger texts, where slight variations might occur throughout paragraphs?

  • Are there any existing packages that could streamline the process, or should we build a custom solution from scratch?

Exploring Available Solutions

My first step was researching what existing libraries or tools are available for text comparison. I came across several options:

  • diff-match-patch: Google’s open-source library for comparing text.

  • jsdiff: A lightweight JavaScript library designed for diffing text and objects.

  • diff2html: A visual diff tool for displaying changes between text or code.

Each of these libraries offers powerful capabilities, but I wanted to take a hands-on approach to better understand the logic behind text comparison. That led me to think: Why not build a custom implementation?

Building a Custom Implementation

If you're like me and love getting into the nitty-gritty, building a custom solution offers more control and a deeper understanding of the problem space. To start, I knew I wanted a word-by-word comparison, which is relatively simple with JavaScript's split() method.

Here's the initial implementation:

const compareTexts = (t1: string, t2: string) => {
  const words1 = t1.split(' ');
  const words2 = t2.split(' ');

  const result: JSX.Element[] = [];
  let i = 0;
  let j = 0;

  while (i < words1.length || j < words2.length) {
    const word1 = words1[i];
    const word2 = words2[j];

    if (word1 === word2) {
      // Both words are the same, add as common (black)
      result.push(<span key={`common-${i}`} className="text-black">{word1} </span>);
      i++;
      j++;
    } else {
      // Word exists in text1 but not in text2 (removed from text2)
      if (i < words1.length && (!words2.includes(word1) || word1 !== words2[j])) {
        result.push(<span key={`removed-${i}`} className="text-red-500">{word1} </span>);
        i++;
      }

      // Word exists in text2 but not in text1 (added in text2)
      if (j < words2.length && (!words1.includes(word2) || word2 !== words1[i])) {
        result.push(<span key={`added-${j}`} className="text-green-500">{word2} </span>);
        j++;
      }
    }
  }

  return result;
};
Enter fullscreen mode Exit fullscreen mode

Let’s take a deeper dive into the logic. The function starts by breaking the two texts into words (using spaces as delimiters) and loops through both word arrays. For each word, the function checks if the word is present in both texts. If a word exists in one text but not the other, it is marked accordingly.

How It Works

  • Text Splitting: Both strings (t1 and t2) are split into arrays of words using .split(' ').

  • While Loop: The loop runs as long as there are words left to compare in either words1 or words2.

  • Word Comparison:

    • If the words at the current index (i, j) match, they are added as common words with a black color.
    • If a word is present in words1 but not in words2, it’s marked as removed (red).
    • If a word is present in words2 but not in words1, it’s marked as added (green).

This function effectively highlights differences between the two pieces of text, which you’re rendering directly in your React component.

The Visual Representation

Once the comparison logic was set, the next task was to integrate it into a presentable UI. Using JSX and Tailwind CSS made this part relatively easy. Here’s how I structured the output:

return (
  <div className="p-4 bg-gray-100 rounded-lg shadow-md w-[60rem] ml-4 mt-4">
    <h2 className="text-xl font-bold mb-4 text-black">Text Difference Comparison</h2>
    <div className="grid grid-cols-2 gap-4">
      <div className="bg-white p-3 rounded">
        <h3 className="font-semibold mb-2 text-black">Output 1:</h3>
        <p className="text-black font-medium">{text1}</p>
      </div>
      <div className="bg-white p-3 rounded">
        <h3 className="font-semibold mb-2 text-black">Output 2:</h3>
        <p className="text-black font-medium">{text2}</p>
      </div>
    </div>
    <div className="mt-4 bg-white p-3 rounded">
      <h3 className="font-semibold mb-2 text-black">Difference:</h3>
      <p className="font-medium">{compareTexts(text1, text2)}</p>
    </div>
  </div>
);
Enter fullscreen mode Exit fullscreen mode

Why Tailwind CSS Rocks

I used Tailwind CSS for styling, which made my life so much easier. It’s a utility-first CSS framework that lets you style elements right in your JSX, without needing to maintain separate style sheets. It’s perfect for quickly putting together components with a clean, modern look.

The Final Output of Initial Implementation

Here is your full component of the initial implementation.

import React from 'react';

const TextDiffComponent = () => {
  const text1 = "A beautiful day starts with a fresh cup of coffee and a clear mind to conquer the world";
  const text2 = "A productive morning begins with a strong cup of coffee and the determination to change the world";

  const compareTexts = (t1: string, t2: string) => {
    const words1 = t1.split(' ');
    const words2 = t2.split(' ');

    const result: JSX.Element[] = [];
    let i = 0;
    let j = 0;

    while (i < words1.length || j < words2.length) {
      const word1 = words1[i];
      const word2 = words2[j];

      if (word1 === word2) {
        // Both words are the same, add as common (black)
        result.push(<span key={`common-${i}`} className="text-black">{word1} </span>);
        i++;
        j++;
      } else {
        // Check if the word is missing from text2 (added in text1)
        if (i < words1.length && (!words2.includes(word1) || word1 !== words2[j])) {
          result.push(<span key={`removed-${i}`} className="text-red-500">{word1} </span>);
          i++;
        }

        // Check if the word is missing from text1 (added in text2)
        if (j < words2.length && (!words1.includes(word2) || word2 !== words1[i])) {
          result.push(<span key={`added-${j}`} className="text-green-500">{word2} </span>);
          j++;
        }
      }
    }

    return result;
  };

  return (
    <div className="p-4 bg-gray-100 rounded-lg shadow-md w-[60rem] ml-4 mt-4">
      <h2 className="text-xl font-bold mb-4 text-black">Text Difference Comparison</h2>
      <div className="grid grid-cols-2 gap-4">
        <div className="bg-white p-3 rounded">
          <h3 className="font-semibold mb-2 text-black">Output 1:</h3>
          <p className="text-black font-medium">{text1}</p>
        </div>
        <div className="bg-white p-3 rounded">
          <h3 className="font-semibold mb-2 text-black">Output 2:</h3>
          <p className="text-black font-medium">{text2}</p>
        </div>
      </div>
      <div className="mt-4 bg-white p-3 rounded">
        <h3 className="font-semibold mb-2 text-black">Difference:</h3>
        <p className="font-medium">{compareTexts(text1, text2)}</p>
      </div>
    </div>
  );
};

export default TextDiffComponent;
Enter fullscreen mode Exit fullscreen mode

Output:

text_diff

text_diff_2

Transforming It into an NPM Package 🛠️

The text-compare package is available as an open source project. You can use it for free and also make meaningful contributions to it. Please find the repository here: https://github.com/CreoWis/text-compare

After the initial implementation, my tech lead suggested packaging this logic as an NPM module so that it could be reused across projects. I took on the task of generalizing the logic, ensuring it could handle various text comparison scenarios, and wrapping it into a package.

The result? The text-compare package is now available for the public! 🚀 It not only highlights the differences between texts but also gives you a matching percentage to quantify how similar two pieces of text are.

Using the NPM Package

To get started with the package in your projects, install it via NPM:

npm install text-compare
Enter fullscreen mode Exit fullscreen mode

Then, in your JavaScript or TypeScript file:

import { compareTexts } from 'text-compare';

const originalText = "A beautiful day starts..."
const updatedText = "A productive morning begins..."

const result = compareTexts(originalText, updatedText);
console.log(result); // Returns the comparison with highlighted differences
Enter fullscreen mode Exit fullscreen mode

Enhancements: Customization and Similarity Calculation

In the initial version of the useTextComparison hook, we compared two text inputs word by word and displayed the differences using a simple color-coding mechanism for common, added, and removed words. In this enhanced version, we've incorporated additional features that offer more flexibility and provide more detailed insights into the text comparison.

Key Improvements:

  1. Customizable Colors: We introduced an optional customColors object, allowing users to specify their own colors for common, removed, and added words. This improves the flexibility of the hook by enabling better visual representation based on different use cases. If no custom colors are provided, the comparison will default to gray for common words, red for removed words, and green for added words.

  2. Handling Edge Cases We added logic to handle null or undefined inputs more gracefully. If either text is empty, the function will return an empty result with a similarity of 0%.

  3. Similarity Percentage Calculation: In this version, the hook also calculates the similarity between the two texts as a percentage. This metric provides an additional layer of insight into how closely the two texts match. The formula considers the number of matching words over the total number of words in the longer text, yielding a straightforward similarity score.

  4. Whitespace-Aware Comparison: The text is split using a regular expression that accounts for multiple spaces, ensuring that words are separated appropriately, even if they are followed by multiple spaces.

Here’s the enhanced code:

interface ComparisonColors {
  commonColor?: string;
  removedColor?: string;
  addedColor?: string;
}

const useTextComparison = (t1: string, t2: string, customColors?: ComparisonColors) => {
  const { commonColor = 'black', removedColor = 'red', addedColor = 'green' } = customColors || {};
  const words1 = t1.split(/\s+/);
  const words2 = t2.split(/\s+/);

  const result: JSX.Element[] = [];
  Let i = 0, j = 0.

  while (i < words1.length || j < words2.length) {
    const word1 = words1[i];
    const word2 = words2[j];

    if (word1 === word2) {
      result.push(<span key={`common-${i}`} style={{ color: commonColor }}>{word1} </span>);
      i++;
      j++;
    } else {
      if (i < words1.length && (!words2.includes(word1) || word1 !== words2[j])) {
        result.push(<span key={`removed-${i}`} style={{ color: removedColor }}>{word1} </span>);
        i++;
      }

      if (j < words2.length && (!words1.includes(word2) || word2 !== words1[i])) {
        result.push(<span key={`added-${j}`} style={{ color: addedColor }}>{word2} </span>);
        j++;
      }
    }
  }

  const similarity = (words1.filter(word => words2.includes(word)).length / Math.max(words1.length, words2.length) * 100;

  return { result, similarity };
};
Enter fullscreen mode Exit fullscreen mode

This version enhances the visual representation while providing a similarity score, giving developers flexibility and insights when comparing texts.

How to Use:

To use this updated hook in your React component, simply pass in the two texts you want to compare along with the optional customColors to modify the appearance:

const options = {
  customColors: {
    commonColor: '#1E90FF',    // DodgerBlue for common words
    removedColor: '#FF6347',   // TomatoRed for removed words
    addedColor: '#32CD32',     // LimeGreen for added words
  }
};

const { comparisonResult, similarity } = useTextComparison(text1, text2, options);
Enter fullscreen mode Exit fullscreen mode

With these new features, the useTextComparison hook provides more robust text comparison capabilities and an enhanced user experience. Whether you're creating a text diff viewer, comparing documents, or simply highlighting changes, this updated logic offers both clarity and flexibility.

Wrapping Up

What started as a "quick POC" turned into a full-brain exercise. But you know what? I love days like this. They remind me why I got into coding in the first place—solving puzzles and making cool stuff.

Remember, the key to becoming a better developer isn’t just about writing code; it’s about understanding the problems you're solving and mastering the tools and technologies you're using to solve them. It's about continuous learning, experimenting, and collaborating. That’s the beauty of being a developer!

Now, here's the exciting part—this component is part of our open-source project! 🎉 You can check out the Creowis Text Compare GitHub repository for the full codebase. Feel free to explore, fork, and contribute. Whether you find bugs, improve the code, or suggest new features, your contributions are highly valued, and we’d love to have you join our open-source journey. Open-source is all about learning together and building better software for everyone.

Feel free to play with this tool, break it, tweak it, and most importantly, make it your own. Coding is all about creativity and collaboration, and I’d love to see what you build with this tool. Drop your GitHub repos, share your projects, or connect with me on social media. Let's keep this conversation going, learn from each other, and grow as a community of developers!

Stay curious, keep experimenting, and most importantly, keep having fun with it. Coding is a journey, and every line you write brings you closer to mastering your craft.

If you found this article helpful, don't forget to share it with your fellow dev friends.

Happy coding, everyone! 💻✨


We at CreoWis believe in sharing knowledge publicly to help the developer community grow. Let’s collaborate, ideate, and craft passion to deliver awe-inspiring product experiences to the world.

Let's connect:

This article is crafted by Chhakuli Zingare, a passionate developer at CreoWis. You can reach out to her on X/Twitter, LinkedIn**, and follow her work on the GitHub.

đź’– đź’Ş đź™… đźš©
chhakuli_zingare_2a0ad5f8
Chhakuli Zingare

Posted on October 11, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related