Adding a real feature to a project 🧛
Tags: hacktoberfest, react, opensource, hacktoberfest23
Published: 2023-10-25 04:42:59

While looking for an issue for my **3rd** hacktoberfest pull request, I stumbled accross a feature request, that looked fun and interesting to work on. [The project](https://github.com/eduardconstantin/azure-fundamentals) was a classic quiz application, simulating practice exams for **azure fundamentals**.

![Home Page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sf4la56tvnm3ix34ubiw.png)

If you visit the link and try to click on `Exam Mode`, you'll get a placeholder screen as the all those changes still need to be shipped to production. 

![Placeholder Page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/towa3z3p1lp8jfp9d69v.png)

The maintainer is only adding features to `dev` branch first, which is why I would be using `localhost` on dev to demonstrate in the blog.

## Table of Contents
&nbsp;1. [The Issue](#the-issue)
&nbsp;2. [Implementing the Feature 🛠️](#implementing-the-feature)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 2.1. [Looking for a queue implementation](#looking-for-a-queue-implementation)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 2.2. [Queue Implementation 🚶🚶🚶🚶](#queue-implementation)
&nbsp;3. [The Pull Request 🧲](#the-pull-request)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 3.3. [Additional Commits](#additional-commits)
&nbsp;4. [Conclusion 🎊](#conclusion)

## The Issue

Before talking about the issue, lets take a quick look at how the application worked at the time.


![Begin Exam](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6kx7v6d0gt8be1utge4c.png)

On clicking **Begin Exam**, a timer started and you could either click **Next** or **Skip** button after selecting a response.

![Exam Question](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cakavnz6piux4eqr086m.png)

In its the then state, the **Skip** button behaved same as the **Next** button. The [issue](https://github.com/eduardconstantin/azure-fundamentals/issues/81) opened by maintainer suggested that it was a bug that needed to be fixed.

However, I would say that was more of a brand-new feature request as not a single part of the logic required was implemented yet.

Here is the expected behavior in the owner's words.

![Feature Specs](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4pko7426o493gb3rs2i3.png)

Now it was time to get to work!

## Implementing the Feature 🛠️

The very first step towards achieving this goal was to figure out what kind of queue I would use. I started looking for a reliable package that implemented the **queue** data structure for me.

### Looking for a queue implementation

The very first one I came across was a package called [**useHooks**](https://usehooks.com/). It looked like a really cool package with tonnes of hooks to use, and [**useQueue**](https://usehooks.com/usequeue) was one of them.

I quickly installed and tested the hook in my local environment, and it worked like a charm. However, adding such a heavy package just for using one single hook didn't make sense to me.

**There had to be a better way!**

I spent the next hour searching for an alternative package that specifically focussed on that data structure, and I was surprised to see there is neither a built-in queue, nor a de-facto standard library/package for this purpose in js.

I found a few packages like [queue](https://www.npmjs.com/package/queue), but they are more like `message queues`, to collect and execute functions/actions in order.

There were some packages like [queue-fifo](https://www.npmjs.com/package/queue-fifo) which solved my purpose, but had low popularity (weekly downloads), and last published 4 years ago. We cannot possibly trust such a package, as it would go against the principles of **Software Supply Chain Security**.

### Queue Implementation 🚶🚶🚶🚶

After all this research on various platforms, I decided I'll have to **roll my own** queue. So I created a new directory called `utils` and started writing my own implementation in a `Queue.ts` file. I tried making a generic class so the user could specify the type they want to store in the queue.

Here's what my implementation looked like:
```ts
/**
 * A data structure implementing FIFO (first in, first out) behavior
 */
export class Queue<T> {
    private collection: T[];

    constructor(initialCollection: T[] = []) {
        this.collection = initialCollection;
    }

    /**
     * Adds an element to the back of the queue
     * 
     * @param {T} element The element to be added
     */
    enqueue(element: T) {
        this.collection.push(element);
    }

    /**
     * Removes and returns the element from the front of the queue
     * 
     * @returns The removed element
     */
    dequeue(): T | undefined {
        return this.collection.shift();
    }

    /**
     * 
     * @returns The element at the front of the queue if present, else null
     */
    front(): T | null {
        return this.collection[0] ?? null;
    }

    /**
     * 
     * @returns Total number of elements in the queue
     */
    size(): number {
        return this.collection.length;
    }

    /**
     * 
     * @returns True if the queue is empty
     */
    isEmpty(): boolean {
        return this.size() === 0;
    }

    /**
     * Removes all the elements from the queue
     */
    clear(): void {
        while (!this.isEmpty()) {
            this.dequeue();
        }
    }

    /**
     * 
     * @returns Returns an array with all the elements in the queue
     */
    queue(): T[] {
        return [...this.collection];
    }
}
```

Once I was satisfied that my queue behaved as expected, I started working on the feature itself.

This is the **workflow** I decided:

- Store the skipped question indexes in a `Queue` using component state.
```ts
const [skippedQuestions, setSkippedQuestions] = useState<Queue<number>>(
    new Queue<number>(),
  );
```

- Whenever **Skip** button gets clicked, add the question indexes to the queue, delete any selected response from the `responseSet`, and invoke the `handleNextQuestion` function.
```ts
const handleSkipQuestion = (questionNo: number) => {
    skippedQuestions.enqueue(questionNo);

    // Unset any selected answer
    setAnswers((prevState) => {
      const updatedAnswers = { ...prevState };

      // Reset the response if an answer was selected
      if (updatedAnswers.hasOwnProperty(questionNo)) {
        delete updatedAnswers[questionNo];
      }

      // Replace with the updated state
      return updatedAnswers;
    });

    handleNextQuestion(questionNo + 1);
  };
```

- Update the `handleNextQuestion` function to move to next question if all questions haven't been touched yet. If all questions have been touched, move to the next skipped question in the queue, if any, else invoke the `checkPass` function to end the test and display results.
```ts
const [allQuestionsTouched, setAllQuestionsTouched] =
    useState<boolean>(false);

const handleNextQuestion = (questionNo: number) => {
    if (questionNo <= numberOfQuestions) {
      if (!allQuestionsTouched) {
        setCurrentQuestionIndex(questionNo);
      } else if (!skippedQuestions.isEmpty()) {
        setCurrentQuestionIndex(
          skippedQuestions.dequeue() ?? numberOfQuestions,
        );
      } else {
        setCurrentQuestionIndex(numberOfQuestions);
        checkPassed();
      }
    } else {
      setAllQuestionsTouched(true);

      if (!skippedQuestions.isEmpty()) {
        setCurrentQuestionIndex(
          skippedQuestions.dequeue() ?? numberOfQuestions,
        );
      } else {
        setCurrentQuestionIndex(numberOfQuestions);
        checkPassed();
      }
    }
  };
```

And added a fix to keep the **Next** button disabled until a response was selected:

![Disable next button](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/78jmhyryu2rz0q8nzqfs.png)

After I finished my logic and everything worked as expected, it was time to open the Pull Request.

## The Pull Request 🧲


![PR Summary](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ai6n6tnchx2pbhaybmz0.png)


I was expecting that this would be a smooth merge as well, but to my surprise, this was the longest conversation I ever had in an open source contribution, **29 conversations long**.

You can take a look yourself:
[https://github.com/eduardconstantin/azure-fundamentals/pull/82](https://github.com/eduardconstantin/azure-fundamentals/pull/82)

### Additional Commits

The very first changes maintainer requested were to convert the Queue class to a function (probably because the project was using functional approach), and reported a bug where the skipped question, had the option selected that user might have clicked before skipping. To be honest, I felt like that was more like a feature than a bug, but it wasn't my project so...

The maintainer gave me an option to either fix the bug, or open another issue for it, but I decided to take it on.

The first thing I did was update my **Queue** class to a factory function. After lots of research and revision of the syntax, this was the end result:

```ts
export interface IQueue<T> {
    enqueue(element: T): void;
    dequeue(): T | undefined;
    front(): T | null;
    size(): number;
    isEmpty(): boolean;
    clear(): void;
    queue(): T[];
}

/**
 * A data structure implementing FIFO (first in, first out) behavior
 */
export function Queue<T>(initialCollection: T[] = []): IQueue<T> {
    // Private collection
    let collection: T[];
    
    function initQueue(): IQueue<T> {
        collection = initialCollection;

        return {
            enqueue,
            dequeue,
            clear,
            front,
            queue,
            isEmpty,
            size
        }
    }

    initQueue();

    /**
     * Adds an element to the back of the queue
     * 
     * @param {T} element The element to be added
     */
    function enqueue(element: T) {
        collection.push(element);
    }

    /**
     * Removes and returns the element from the front of the queue
     * 
     * @returns The removed element
     */
    function dequeue(): T | undefined {
        return collection.shift();
    }

    /**
     * 
     * @returns The element at the front of the queue if present, else null
     */
    function front(): T | null {
        return collection[0] ?? null;
    }

    /**
     * 
     * @returns Total number of elements in the queue
     */
    function size(): number {
        return collection.length;
    }

    /**
     * 
     * @returns True if the queue is empty
     */
    function isEmpty(): boolean {
        return size() === 0;
    }

    /**
     * Removes all the elements from the queue
     */
    function clear(): void {
        while (!isEmpty()) {
            dequeue();
        }
    }

    /**
     * 
     * @returns Returns an array with all the elements in the queue
     */
    function queue(): T[] {
        return [...collection];
    }

    return initQueue();
}
```

I know `IQueue` is a bad naming practive for an interface, but I couldn't name it `Queue` due to naming conflicts. Please let me know what would be a better name for it if you've made it till here 😉.

These were the new commits in response to the requested changes.

![New Commits](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/log9pi90cp76t2e9zh0j.png)

You'll notice I also had to fix **merge conflicts** as there was already progress on the `dev` branch.

Now that I thought everything was good to be merged, another fellow contributor joined the PR as a reviewer and suggested a couple of changes.

First, I wasn't updating the `answers` state properly.

![Change1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7cd0hoafd1egec55b77z.png)

Second, I forgot to setup husky to format my code through commits.

![Change2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1vp2t9ung5njsauxl9g0.png)

I quickly fixed those problems and pushed a new commit.

![More commits](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ocuhzbdlkahdk9czc8n8.png)

You'll notice there's a force push. That was because I **amended** my commit message.

But that was not it, the converstation went on and on. It was a bug discussion, some requirements were changed, some issues were found and everything was addressed, and the Pull Request was finally mereged .

Its better to follow the actual converstation [here](https://github.com/eduardconstantin/azure-fundamentals/pull/82).

## Conclusion 🎊

This was, by far, the largest open source contribution I had ever made. 

![Files Changed](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hwzoibeyxbxsqufwq9lr.png)

There was a comprehensive discussion, with more than 2 people involved, and about 10 commits until the PR was merged.

Lots of learnings, and even though I got frustrated many times, all that work payed off in the end. In other words, this PR taught me the importance of **patience** while contributing to open source projects.