React Expense Manager - Redoing Part 1

I decided to restart the React front end for the expense manager project. Why? Well, I wanted to write it in TypeScript. That’s basically all. This post will contain the setup for a TS React project, usage of Tailwind, the main transaction table and a test for it.

So how do you start a TypeScript React project?

yarn create react-app expenses_frontend --template typescript

Let’s start with our TransactionTable component. It will receive a props object that contains a key named “transactions”. Since I like types, let’s start by defining the props type, starting with the Transaction type.

export interface TransactionType {
  [index: string]: number | string | boolean;
  id: number;
  amount: number;
  name: string;
  retailer: string;
  category: string;
  date: string;
  currency: string;
  type: string;
  recurring: boolean;
  user: string;
}

I am implementing it as an Interface. I include all the keys and their types but I also have a [index: string]: number | string | boolean;. What does that do? It’s an Index Signature and it denotes that the interface will be indexed with a string key and return a number/string/boolean value. I am adding this because later on I will be iterating over a string of keys that will be present in that Transaction and accessing them programmatically.

Our props then have the following type:

type TransactionTableProps = {
  transactions: TransactionType[];
};

Now that we have that, let’s look at our TransactionTable. Keep in mind that I am also using TailwindCSS for the styling and using examples form Tailwind Elements.

const TransactionTable = ({ transactions }: TransactionTableProps) => {
  const sortingMap = new Map([
    ["name", "Description"],
    ["amount", "Amount"],
    ["currency", "Currency"],
    ["retailer", "Retailer"],
    ["category", "Category"],
    ["date", "Date"],
    ["type", "Type"],
  ]);

  return (
    <div className="flex flex-col">
      <div className="overflow-x-auto sm:-mx-6 lg:-mx-8">
        <div className="py-2 inline-block min-w-full sm:px-6 lg:px-8">
          <div className="overflow-x-auto">
            <table className="min-w-full">
              <thead className="border-b">
                <tr>
                  {Array.from(sortingMap).map(([keyName, columnName]) => (
                    <th
                      scope="col"
                      className="text-sm font-medium text-gray-900 px-6 py-4 text-left"
                      key={keyName}
                    >
                      {columnName}
                    </th>
                  ))}
                </tr>
              </thead>
              <tbody>
                {transactions.map((transaction) => (
                  <tr
                    key={transaction.id}
                    className="border-b transition duration-300 ease-in-out hover:bg-gray-100"
                  >
                    {Array.from(sortingMap).map(([keyName, columnName]) => (
                      <td
                        key={keyName}
                        className="text-sm text-gray-900 font-light px-6 py-4 whitespace-nowrap"
                      >
                        {`${transaction[keyName]}`}
                      </td>
                    ))}
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
      </div>
    </div>
  );
};

Ok let’s explain this one. We start with a so called sortingMap where the object keys will be converted to column titles in the table. Then I use the hoverable table from Tailwind Elements. The important part is iterating over the sortingMap and creating the column names in the header:

<tr>
  {Array.from(sortingMap).map(([keyName, columnName]) => (
    <th
      scope="col"
      className="text-sm font-medium text-gray-900 px-6 py-4 text-left"
      key={keyName}
    >
      {columnName}
    </th>
  ))}
</tr>

followed by iterating over each of the transactions, where we also iterate over each of the keys (that are present in the sortingMap).

<tbody>
  {transactions.map((transaction) => (
    <tr
      key={transaction.id}
      className="border-b transition duration-300 ease-in-out hover:bg-gray-100"
    >
      {Array.from(sortingMap).map(([keyName, columnName]) => (
        <td
          key={keyName}
          className="text-sm text-gray-900 font-light px-6 py-4 whitespace-nowrap"
        >
          {`${transaction[keyName]}`}
        </td>
      ))}
    </tr>
  ))}
</tbody>

This is not enough. I want my table to be more interactive, and if I click on the column header names, I want the transactions to get sorted. The sorted transactions (and whether they are sorted ascending or descending) will be passed down via props. So what would we need?

  1. A callable to do the sorting (some handleSort)
  2. A boolean that lets us know whether the rows are sorted in an ascending or descending manner.

For now, these will get implemented in App.tsx but let’s have a look at the other bits and pieces of TransactionTable where these will be handled.

The props of TransactioNTable are now:

type TransactionTableProps = {
  transactions: TransactionType[];
  setSort: (sortKey: string) => void;
  reversed: boolean;
};

setSort is a function that takes in a string and doesn’t return anything. The string will be the string that gets passed on to the parent in order to sort the transactions using that key. Another thing that would be nice to have is some indicator of which column is used for sorting and whether it’s in ascending or descending order (later we will also add a default). We will start with the id column being the active one by default.


const TransactionTable = ({
  transactions,
  setSort,
  reversed,
}: TransactionTableProps) => {
    ...
    const [activeColumn, setActiveColumn] = useState<string>("id");

    const getStyle = (columnName: string): string =>
        columnName === activeColumn
        ? "text-sm font-medium text-gray-900 px-6 py-4 text-left bg-gray-300"
        : "text-sm font-medium text-gray-900 px-6 py-4 text-left";

    const getColumnText = (columnName: string): string => {
        if (!(columnName === activeColumn)) {
        return columnName;
        }
        if (reversed) {
        return columnName + " ↑";
        }

        return columnName + " ↓";
    };
    ...
                  <tr>
                  {Array.from(sortingMap).map(([keyName, columnName]) => (
                    <th
                      scope="col"
                      key={keyName}
                      className={getStyle(columnName)}
                    >
                      <button
                        type="button"
                        onClick={() => {
                          setActiveColumn(columnName);
                          setSort(keyName);
                        }}
                      >
                        {getColumnText(columnName)}
                      </button>
                    </th>
                  ))}
                </tr>
    ...
}

What do we have that’s new? Well we use React’s useState hook in order to keep the activeColumn in state. We also have a getStyle function that will set the style on a column header depending on whether it’s active or not. We also have a getColumnText function, doing what?

  1. It checks whether the column that was clicked on is the active column. If it’s not (meaning we are sorting by a new key), it just returns that column name.
  2. If reversed is true, it adds an up arrow to the name.
  3. If reversed is false, it adds a down arrow to the name.

Then, in the header our columnName become buttons that on click, call the setActiveColumn function as well as the setSort function (which sorts the transactions). This is all we have in the TransactionTable component. You want tests? I’ll give you tests. There are two tests:

  1. To test that the header names are rendered (with no transactions)
  2. To test that when you click on a column it emits an event that calls the setSort function.

In our tests/components/ folder, we have a transactionTable.test.tsx. We are importing a few functions from the React Testing Library, our TransactionTable component and some sampleData we have.

import { render, screen, fireEvent } from "@testing-library/react";
import TransactionTable from "../../components/transactionTable";
import initialTransactions from "../../sampleData";

In our first test, we render the component with no transactions, an empty function for setSort and false for reversed.

test("Tests that the TransactionTable is rendered with no rows", () => {
  render(
    <TransactionTable transactions={[]} setSort={() => null} reversed={false} />
  );
  [
    "Description",
    "Amount",
    "Currency",
    "Retailer",
    "Category",
    "Date",
    "Type",
  ].forEach((colName) => {
    const columnElement = screen.getByText(colName);
    expect(columnElement).toBeInTheDocument();
  });
});

In the second test, we test taht if we click on a column name, it calls the setSort function. In this case, the setSort function we pass to the TransactionTable is a jest mock function.

test("Tests that clicking on a column header emits an event that calls the setSort function and changes state internally", () => {
  const handleSort = jest.fn();
  render(
    <TransactionTable
      transactions={initialTransactions}
      setSort={handleSort}
      reversed={false}
    />
  );
  const currencyColumnButton = screen.getByText("Currency");
  fireEvent.click(currencyColumnButton);
  expect(handleSort).toHaveBeenCalledTimes(1);
  expect(handleSort).toHaveBeenCalledWith("currency");
  // since we're not testing implementation, but rather behaviour, let's test that the column button now includes a down arrow
  expect(currencyColumnButton).toHaveTextContent("Currency ↓");
  // let's click it again
  fireEvent.click(currencyColumnButton);
  expect(currencyColumnButton).toHaveTextContent("Currency ↓");
});

Handling sorting in the parent component

We said that we pass a sorting function to the table component, let’s look at it. We hold our transactions in state, and also use a useEffect hook to set those transactions (we’ll make this an asynchronous call later like it was in the previous posts).

Our sort settings will have the following type:

interface ISort {
  sortKey: string;
  reversed: boolean;
}

and they will be set as:

const [transactions, setTransactions] = useState<TransactionType[]>([]);

useEffect(() => {
  setTransactions(initialTransactions);
}, []);

const [sortedSettings, setSortedKey] = useState<ISort>({
  sortKey: "id",
  reversed: false,
});

Our sortedSettings state is an object with a sortKey string and a reverse boolean. The actual sorting will be done by Lodash’s sortBy:

import { sortBy } from "lodash";

...
  let sortedTransactions: TransactionType[] = sortBy(transactions, [
    sortedSettings.sortKey,
  ]);
  sortedTransactions = sortedSettings.reversed
    ? sortedTransactions.reverse()
    : sortedTransactions;
...

And that’s it for this post. A bit annoying that I decided to rewrite, but I wanted to play more with TypeScript. The commit for this post can be found here.