Typescript and Clojurescript Typescript code

Typescript and Clojurescript implementations using react-table v.8.

Introduction

I started a couple of months ago in a new project that uses Typescript to implement a React frontend application. Before I started the project I did a small practice application using the same technologies just for learning purposes. Later on, my corporation asked me to give a presentation and live-coding session regarding Clojure. I thought that it might be a good idea to implement the exact same practice application, this time using Clojure and Clojurescript. I’m about to write a longer blog post comparing those two ecosystems a bit later. But today I realized that there is an opportunity to write a bit shorter blog post first, regarding a more special aspect: the Clojurescript / Javascript interop and how I used the interop to implement the same HTML table using TanStack Table v8 React headless table library.

Both applications are in my Github account:

You might want to compare specifically these two files, the first one using Typescript / React, and the second one using Clojurescript / React:

Technologies

The Typescript frontend app uses the following technologies:

The Clojurescript frontend app uses the following technologies:

Both apps use TanStack Table v8 for demonstration purposes to illustrate how one can use a third-party React library in those two applications.

Clojurescript / Javascript Interop

The Clojurescript / Javascript Interop is just excellent. It is really easy to use any Javascript standard library, Javascript data structures, or any third party Javascript library. Some examples:

(rt/useReactTable #js {:columns columns :data (clj->js data) :getCoreRowModel (rt/getCoreRowModel)})

The #js tagged literal can be used to convert a Clojurescript data structure (a map in this example) into a Javascript native data structure (a JSON object in this example). #js does not convert nested data structures recursively, so you can use clj->js standard library function for that. You can even mix Clojurescript and Javascript data structures.

Calling Javascript functions is as easy as:

(.getContext header)

… compared to the equivalent Typescript function call:

header.getContext()

For Clojurescript this is quite natural. The function is the first element in the list. You just use the dot (.) to mark the function call (getContext in our example) to be Clojurescript / Javascript interop call, for the entity that is the next one in the list (header in our example).

And the same way you access the Javascript object properties:

(.-isPlaceholder header)

… compared to the equivalent Typescript object property access:

header.isPlaceholder

Read more about the Clojurescript / Javascript interop in my previous blog post: Clojurescript Next Level.

Using a Third-party React Library from Clojurescript vs Typescript

Let’s use TanStack Table v8 for demonstration purposes to illustrate how one can use a third-party React library in those two applications.

First the Typescript implementation:

import {
  createColumnHelper,
  flexRender,
  getCoreRowModel,
  useReactTable,
} from "@tanstack/react-table";
...

const pgColumnHelper = createColumnHelper<ProductGroupType>();

const columns = [
  pgColumnHelper.accessor("pgId", {
    header: "Id",
    cell: (info) => (
      <NavLink to={`/products/` + info.getValue()}>{info.getValue()}</NavLink>
    ),
  }),
  pgColumnHelper.accessor("name", {
    cell: (info) => info.getValue(),
    header: () => "Name",
  }),
];

function ProductGroupsTable({
  productGroups,
}: {
  productGroups: ProductGroupType[];
}) {
  // setData is not used, but it is required.
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  const [data, setData] = React.useState(() => productGroups);

  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  });

  return (
    <div className="p-4">
      <table>
        <thead>
          {table.getHeaderGroups().map((headerGroup) => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map((header) => (
                <th key={header.id}>
                  {header.isPlaceholder
                    ? null
                    : flexRender(
                        header.column.columnDef.header,
                        header.getContext(),
                      )}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          {table.getRowModel().rows.map((row) => (
            <tr key={row.id}>
              {row.getVisibleCells().map((cell) => (
                <td key={cell.id}>
                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}
...
// And calling our component:
        <div className="p-4">
          {productGroups && (
            <ProductGroupsTable productGroups={productGroups} />
          )}
        </div>

… and then the equivalent Clojurescript implementation using the exact same react-table with the same generated HTML table:

(ns frontend.routes.product-groups
  (:require [re-frame.core :as re-frame]
            ["@tanstack/react-table" :as rt]
            ["react" :as react :default useMemo]
            [reagent.core :as r]
...            
(defn mylink 
  [pg-id]
  [:<>
   [:a {:href (rfe/href ::f-state/products {:pgid pg-id})} pg-id]])

;; Example of Clojurescript / Javascript interop.
;; Compare to equivalent JSX implementation: 
;; https://github.com/karimarttila/js-node-ts-react/blob/main/frontend/src/routes/product_groups.tsx#L36
(defn product-groups-react-table
  [data]
  (let [_ (f-util/clog "ENTER product-groups-table") 
        columnHelper (rt/createColumnHelper)
        columns #js [(.accessor 
                      columnHelper 
                      "pgId" 
                      #js {:header "Id" :cell (fn [info] (reagent.core/as-element [mylink (.getValue info)]))})
                     (.accessor 
                      columnHelper 
                      "name" 
                      #js {:header "Name" 
                           :cell (fn [info] 
                                   (.getValue info))})]
        table (rt/useReactTable #js {:columns columns :data (clj->js data) :getCoreRowModel (rt/getCoreRowModel)})
        ^js headerGroups (.getHeaderGroups table)]
    [:div.p-4
     [:table
      [:thead
       (for [^js headerGroup headerGroups]
         [:tr {:key (.-id headerGroup) }
          (for [^js header (.-headers headerGroup)]
            [:th {:key (.-id header) }
             (if (.-isPlaceholder header)
               nil
               (rt/flexRender (.. header -column -columnDef -header) (.getContext header)))])])]
      [:tbody
       (for [^js row (.-rows (.getRowModel table))]
         [:tr {:key (.-id row)}
          (for [^js cell (.getVisibleCells row)]
            [:td {:key (.-id cell)}
             (rt/flexRender (.. cell -column -columnDef -cell) (.getContext cell))])])]]]))

;; And calling our component:
          [:div.p-4
           [:f> product-groups-react-table product-groups-data]
           ]]]))))

The two solutions are practically the same even though they look a bit different. But if you read the two implementations side by side you can see that most of the code is exactly or almost exactly the same. Let’s see a bit smaller code snippet to make the comparison easier:

            <tr key={row.id}>
              {row.getVisibleCells().map((cell) => (
                <td key={cell.id}>
                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
                </td>
              ))}
            </tr>

… and Clojurescript:

         [:tr {:key (.-id row)}
          (for [^js cell (.getVisibleCells row)]
            [:td {:key (.-id cell)}
             (rt/flexRender (.. cell -column -columnDef -cell) (.getContext cell))])

In the Typescript implementation we get the visible cells, and then map the cells to generate the td elements for the html table, and finally call the react-table flexRender function for the cell. We do exactly the same thing in the Clojurescript side. The Clojurescript with Hiccup is a bit more concise.

This example introduces a couple of new interop elements. First the type hint: ^js and the second is the double-dots macro: .. which is just syntactic sugar for:

(.-cell (.-columnDef (.-column cell)))

I.e., a bit like the Clojure threading macros.

The main difference is not related to the two languages per se, but e.g., how they use different technologies for providing hyperlinks for the id column:

const columns = [
  pgColumnHelper.accessor("pgId", {
    header: "Id",
    cell: (info) => (
      <NavLink to={`/products/` + info.getValue()}>{info.getValue()}</NavLink>
    ),
  }),
...

which uses React Router Navlink component, and…

(defn mylink 
  [pg-id]
  [:<>
   [:a {:href (rfe/href ::f-state/products {:pgid pg-id})} pg-id]])
...
        columns #js [(.accessor 
                      columnHelper 
                      "pgId" 
                      #js {:header "Id" :cell (fn [info] (reagent.core/as-element [mylink (.getValue info)]))})
...                      

which uses Metosin reitit.

But the react-table configuration itself for setting up the component for the hyperlink is the same in both apps.

Conclusions

Clojure & Clojurescript is a really good combination to implement modern full-stack applications with React in the frontend. The Clojurescript / Javascript interop is excellent and you can use any React third-party library in your Clojurescript frontends, just like you use them in Typescript React frontends.

The writer is working at a major international IT corporation building cloud infrastructures and implementing applications on top of those infrastructures.

Kari Marttila