Fork me on GitHub
#clojurescript
<
2023-02-25
>
Kari Marttila09:02:43

I used previously react-table version 7 with Clojurescript - it was relatively straightforward when using Javascript data structures interacting with react-table. Now there is a new version https://tanstack.com/table/v8 - and the examples are with Typescript. I'm wondering if it is possible to use this new version 8 with Clojurescript?

Kari Marttila09:02:19

E.g. with Typescript:

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

export type ProductGroupType = {
  pgId: number;
  name: string;
};

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",
  }),
];
...
I'm a bit puzzled how to convert that to Clojurescript.

hifumi12309:02:18

just ignore the types and write it as javascript

hifumi12309:02:53

most sane typescript libraries will distribute themselves as minified javascript — typically ESM (but CJS sometimes exists too)

hifumi12309:02:44

basically, if a TS library can be used from vanilla JS, it can be used from CLJS — and to follow TS examples, you can get away by just ignoring type declarations… Problems arise when they start using TS-specific stuff like abstract classes

Kari Marttila09:02:15

I'm not that fluent with Javascript. I was wondering, how to define e.g. the above type ProductGroupType in Javascript...

Kari Marttila09:02:33

But this is an interesting exercise for me! 🙂

hifumi12309:02:47

I want to say ProductGroupType here exists simply to make the TS compiler happy

hifumi12309:02:21

No type decls -> inferred as any type -> typed functions wanting a specific type like T will cause a TS compiler error since any is not as specific as T

hifumi12309:02:51

that’s why in react typescript codebases you’ll see lots of boilerplate like defining the types of each prop of a component inside an interface then doing sth like function component(props: componentProps) { … }

Kari Marttila09:02:40

Ok. I try, let's see how it works out! 🙂

p-himik09:02:29

Sometimes types are important in JS as well. Interop to create them is less than nice, but if you use shadow-cljs then there's shadow.cljs.modern/defclass.

Kari Marttila09:02:06

Ok. Let's see if I'm able to do this. 🙂

Kari Marttila20:02:12

I almost did it. 🙂 I just couldn't make the hyperlink work in the pgId column. And I just can't figure out why it is not working. In Typescript I had:

const pgColumnHelper = createColumnHelper<ProductGroupType>();

const columns = [
  pgColumnHelper.accessor("pgId", {
    header: "Id",
    cell: (info) => (
      <NavLink to={`/products/` + info.getValue()}>{info.getValue()}</NavLink>
    ),
  }),
...
        <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>
In Clojurescript I thought this would do the trick:
(defn mylink 
  [pg-id]
  [:<>
   [:a {:href (rfe/href ::f-state/products {:pgid pg-id})} pg-id]])
...
        columnHelper (rt/createColumnHelper)
        columns #js [(.accessor columnHelper "pgId" #js {:header "Id" :cell (fn [info] [mylink (.getValue info)])})
...
      [: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))])])]]]))
But the hyperlink just won't be there (just the cell value).

p-himik20:02:38

I imagine the :cell function must return a React element instead of a Hiccup vector. You probably have a bunch of errors in the JS console. If so, you can try wrapping that [mylink ...] in (reagent.core/as-element ...).

Kari Marttila20:02:01

Nope, no errors in the JS console.

Kari Marttila20:02:18

But let's try that...

p-himik20:02:25

Mm, maybe something checks whether it's an element or not deliberately.

Kari Marttila20:02:18

Gosh! Thank you!

Kari Marttila20:02:47

Now I really want to understand this. Why do I need the as-element here?

Kari Marttila20:02:59

Let's read documentation.

Kari Marttila20:02:54

I tried to figure this out some 2-3 hours. I should have asked help here 2 hours ago. @U2FRKM4TW: Thank you so much! ❤️

👍 2
Kari Marttila18:02:44

To give back to the community, I wrote a blog post about this exercise. Hopefully someone can benefit from it: https://www.karimarttila.fi/clojurescript/2023/02/28/clojurescript-javascript-interop-with-react-component.html

Jakub Šťastný14:02:33

Hey guys. Can CLJS export ES modules rather than CJS requires? I only see the default --target and --target nodejs (which is what I'm using at the moment). I have some code in CLJS, but the tests are in JS (so they can work as a reference for other people like the ones who don't know CLJS). I'd like the tests to run on Deno.js. I'm aware there's @borkdude's bebo, but that seems to be geared towards running CLJS in Deno, while I need to consume CLJS from JS (hence compilation is probably necessary).

borkdude14:02:59

@U024VBA4FD5 Check out the latest messages in #nbb: nbb is now able to run in Deno (although this isn't what you asked for)

🙏 2
borkdude14:02:19

You can use #C6N245JGG with :target :esm to output ESM

borkdude14:02:23

Nbb itself is built as an ESM module so you can use it from JS:

import { loadString } from 'nbb'

await loadString(`(+ 1 2 3)`);
So you could look at its shadow-cljs.edn file to see how it's set up

borkdude14:02:39

There's also #C03U8L2NXNC and #C03QZH5PG6M which compile straight to ESM

Jakub Šťastný14:02:54

Really appreciated @borkdude! I think cherry is what I'm after.

Jakub Šťastný14:02:06

How the hell do you have time for all these projects? Seriously...

borkdude14:02:22

Note that both squint and cherry are still experimental, so shadow-cljs + target esm I recommend as the safe option ;)

borkdude14:02:45

Just one issue at a time I guess

👍 2
Jakub Šťastný14:02:50

What I have is pretty basic, just some numeric functions to calculate business projections...

borkdude14:02:04

that should be good then I think

borkdude14:02:29

You can also do this with nbb btw. Just use loadString from the JS test file and from the .cljs file do:

#js {:my-fn my-fn}
so:
const { my-fn } = await loadString("my_file.cljs")

Jakub Šťastný14:02:45

That'd be the best, but I have a file full of (CLJS) fns and I need to import these (so I can test them).

borkdude14:02:00

ah yes, cherry would work best here

borkdude14:02:17

it auto-exports all public vars

James Amberger15:02:29

Does anyone have any guidance for producing a PWA with shadow-cljs (or without it if that’s better)?

👀 2
michaeldrogalis22:02:14

Hey everybody. Very long time since I've been here—great to be back. 🙂 I have a question about how people are using React from ClojureScript these days. I saw https://www.youtube.com/watch?v=3HxVMGaiZbc&amp;ab_channel=ChariotSolutions by @dnolen where he describes a workflow of writing React component functions directly in JavaScript, then using ClojureScript for all the business logic. That sounds ideal, but I'm hung up on what that actually looks like. Can anyone hazard a guess? For instance: is there one top-level component function defined in ClojureScript which instruments the JS components? Is the app state defined in ClojureScript and passed into JavaScript? How does one handle passing and accessing Clojure data to and from JavaScript? Just kind of looking for a sketch to follow if anyone's done this.

michaeldrogalis22:02:22

Ah, searched around this channel and looks like it's been discussed a few times. It sounds like: 1. Top-level is instrumented using Reframe/Reagent 2. App state is obviously Clojure data structures 3. Reagent is handling the JS <-> CLJ data structure conversion, for the most part. Is that basically right?

hifumi12302:02:56

That is one way of using React in CLJS, yeah. You can make functions returning vectors encoding HTML data -- this is often called "Hiccup". Re-frame is in charge of state management. Reagent is in charge of rendering your Hiccup into a React component

michaeldrogalis03:02:16

Thanks. I think Ill try to build a toy version of that and post it as an example. Im mostly just fuzzy on the boundary between CLJS and JS from David's description. The hard thing about not having done this in a while is that it feels like there's a large number of choices about how to layer over React, and it's not obvious what the trade-offs are.

hifumi12305:02:27

There are roughly three choices: • Om/rum/etc. are legacy wrappers and not really in active development anymore • Reagent is the most popular choice, but it is very awkward to use with modern React features (hooks, suspense, etc.) and there is a runtime cost to interpreting Hiccup • Helix is a thin React wrapper designed specifically to play nice with JS interop and modern React features; unlike Reagent, Hiccup is replaced with a macro that attempts outputting as much components as it can at compile-time. This in theory should make Helix outperform Reagent in performance.

hifumi12305:02:53

Considering the new React documentation is written entirely with function components and hooks and class components are explicitly labelled "Legacy" and "not recommended" for modern projects. I think your best choice is to use Helix, since class-based components are effectively deprecated in React

michaeldrogalis03:02:31

@U0479UCF48H Thanks - I had been wondering about the intersection of hooks and Reagent/Reframe. It kind of seems like a problem since it'd be hard to play with the rest of the ecosystem, like react-use.

michaeldrogalis03:02:45

@borkdude 👋 Thanks! Great to see some familiar faces still here.

michaeldrogalis03:02:56

@U0479UCF48H Do you know if there's an opinion in the Reagent/Reframe communities regarding what to do about the challenges with hooks/etc?

hifumi12303:02:58

As far as I’ve been able to tell — the overall approach seems to be “ride out class components until they’re finally ripped out of React”

hifumi12303:02:53

how long that time will come? I’m not sure. But even when it does come, the last major release of React with class components will likely be good for an extra 1-2 years. And most people in the Clojure community are fine with staying on older libraries provided they still work for their intended use case. That is to say, people here seem to prefer stability over novelty

hifumi12303:02:50

e.g. many react libraries out there still support React 16 (the latest version is currently 18)… so it is not the end of the world once a major release of React drops class based components entirely

michaeldrogalis03:02:58

Thanks - very valuable information. It's been hard to gauge how concerned to be about what seems like volatility. That helps.

hifumi12303:02:23

No problem! And do note that reagent does allow emitting function components in case hooks are absolutely necessary and your project has decided to use reagent

michaeldrogalis03:02:41

Ah, that's fantastic. That makes me feel a lot better about Reframe then. Absolutely the right abstraction, hopefully reasonably future proof. 🤞

michaeldrogalis03:02:14

(I guess Im assuming that carries up from Reagent)

hifumi12303:02:23

Yeah. Re-frame depends on reagent, but reagent does allow support for function components and hooks. The caveat being that you must denote them explicitly with :f> like below.

(defn my-component []
  (let [[count set-count] (react/useState 0)]
    [:div
      [:p "Count value: " count]
      [:button {:on-click #(set-count inc)} "Increase count"]]))

(defn my-page []
  [:div
    [:h1 "My function component"]
    [:f> my-component]])

hifumi12303:02:51

(warning: did not test this code, but it is a basic example of how to use hooks and emit function components in reagent)

hifumi12303:02:32

I’m personally keeping a close eye on Helix mostly for two things: (1) performance, and (2) being able to embrace modern React from the ground-up

hifumi12303:02:03

I don’t think state management libraries for Helix are “quite there yet”, however. But refx is super promising since it seems to be API compatible with re-frame (in fact, it is an implementation of re-frame without reagent)

hifumi12303:02:33

So the key takeaway here is: you should be able to use re-frame for many years, just know the situation upstream about class components and function component

michaeldrogalis03:02:02

Whew, you answered heaps of questions in 5 minutes that I've spent all last week googling. Hugely appreciated 🙂

👍 2
🙌 2
Rupert (All Street)08:02:05

Also add to this that you can use #uix (https://github.com/roman01la/uix orhttps://github.com/pitch-io/uix) which are thin wrappers over React for ClojureScript. This let's you use Modern react hooks in clojurescript and also makes it very easy to interoperate with the whole React ecosystem. Also worth noting that server side rendered clojure apps are now extremely powerful using a combo of hiccup, compojure, https://github.com/bigskysoftware/htmx and https://github.com/bigskysoftware/_hyperscript. Allowing you to build fast and interactive webapps entirely in Clojure on the serverside without clojurescript. This has several benefits including unified codebase, no API needs to be built and better SEO.

👍 2
hifumi12308:02:38

Out of curiosity, what does Uix do differently from Helix? (I think I saw Uix mentioned in the refx library docs a few days ago, but I never looked it up)

Rupert (All Street)08:02:31

UIX and Helix have some similarities (especially Helix and UIX V2). UIX V1 heavily used hiccup datastructures so you could define a large part of the UI in config (e.g. EDN) and then have a small amount of code just for loading the UI and injecting hooks. So I think it's a case of checking out both Helix and UIX and seeing which you like better from a design/usability/etc perspective.

dnolen18:02:34

@U050A65BL I don’t think the wrapper is very important here to be honest wrt. idea

dnolen18:02:16

basically we just write components in JS, then we “script” them in Reagent as if they were written in ClojureScript - you don’t really care that much.

dnolen18:02:04

The only real issue is that nested JS components won’t be able to take EDN props, so we use context to thread in a converter which the JS components can use to get at the props.

michaeldrogalis18:02:17

When you say "script", you basically mean you have a mirror set of views in CLJS that just call through to JS, right? > The only real issue is that nested JS components won’t be able to take EDN props Great, that is exactly the piece I was hung up on. Thanks for sharing!

dnolen19:02:54

Reagent has a function for taking any React component and making them usable in hiccup, that’s all we do

dnolen19:02:09

same as if we used a 3rd part JS React Component except we wrote the JS ourselves

dnolen19:02:28

originally the main thing here was Storybook - there is a Storybook narrative for CLJS now

dnolen19:02:45

but for us not a big deal since by writing components in JS it’s easier to onboard JS folks

dnolen19:02:14

and the benefits of ClojureScript when you’re writing purely functional components w/ no business logic is not particularly significant

dnolen19:02:28

and we can lean on existing JS documentation

dnolen19:02:10

wiring up the app is course very custom, and we’re getting data from a Clojure service, so we’re dividing up the effort into the parts where’s there some obvious benefit

Rupert (All Street)19:02:02

ClojureScript + JavaScript + HTML can play really well together. Agree that hybrid frontend codebases can work well and you don't have to turn everything into ClojureScript. I've found that with just a few react hooks (`state`, effect,`callback` and memo + a few others) you can build many types of UI component. So for simple components we often barely interface with react at all. So we use clojurescript + UIX which gives us idiomatic access to React hooks + hiccup which is great for expressing HTML.

michaeldrogalis19:02:08

@dnolen Thanks, very helpful description. It's been a long time since I've worked in this area, so having a bit more color on how exactly it works helps. If I get it working, Ill pay it forward with a little blog post. 🙂

michaeldrogalis19:02:44

@UJVEQPAKS Yeah after I had dabbled around with plain React for a few weeks, it felt like a mistake to ignore hooks. Everything's centered on them now for better or worse.

👍 2
sergey.shvets01:02:32

I would also mention hicada. It's pretty much a macro instead of JSX that rewrites hiccup into react.createElement at compile time. The wrapper is very thin and allows you to use everything react has to offer. I believe it's the best way if you plan to use a lot of 3rd party react components libraries or complex components like editors etc.

michaeldrogalis15:02:54

@U4EFBUCUE Thanks! Ill check it out!