Error using a native JS component in CLJS

Hello everyone!

I’m trying and failing when using the nivo ResponsiveBar component. The nivo Bar component works. So I’m discounting my basic library import etc. as possible problems. For reference, the component definitions are linked below.

My code is quite simple. Here’s my shadow.edn:

{:source-paths ["src/dev" "src/main" "src/test"]
 :dependencies [[reagent/reagent "2.0.0-alpha2"]
                [cljs-http "0.1.49"]]
 :dev-http {8080 {:root "public" :proxy-url "http://localhost:5000" :proxy-predicate rgrr.server/proxy?}}
 :builds {:client {:target :browser
                   :modules {:main {:init-fn rgrr.client.app/init}}}}}

And the Bar/ResponsiveBar component is rendered with the code below. In this code if I change ResponsiveBar to Bar I get the browser showing the expected bar chart.

(defn render-histogram []
  (rdc/render @root
              [:div {:style {:width "100%" :height "500px"}}
               [:> ResponsiveBar
                {:data [{"country" "AD" "hotdogs" 60 "burgers" 80}
                        {"country" "AE" "hotdogs" 30 "burgers" 50}
                        {"country" "AF" "hotdogs" 70 "burgers" 90}]
                 :keys ["hotdogs" "burgers"]
                 :indexBy "country"
                 :height 500
                 :width 400
                 :margin {:top 50 :right 50 :bottom 50 :left 60}
                 :padding 0.3
                 }]]))

But with ResponsiveBar I get this error. I have truncated the stacktrace for the sake of brevity.

react-dom-client.development.js:4261 Uncaught Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
at createFiberFromTypeAndProps (react-dom-client.development.js:4261:28)
at createFiberFromElement (react-dom-client.development.js:4275:14)
at reconcileChildFibersImpl (react-dom-client.development.js:7881:31)
at eval (react-dom-client.development.js:8059:33)
at reconcileChildren (react-dom-client.development.js:8623:13)
at updateFunctionComponent (react-dom-client.development.js:8916:7)
at beginWork (react-dom-client.development.js:10524:18)
at runWithFiberInDEV (react-dom-client.development.js:1521:30)
at performUnitOfWork (react-dom-client.development.js:15134:22)
at renderRootSync (react-dom-client.development.js:14958:41)
createFiberFromTypeAndProps @ react-dom-client.development.js:4261
createFiberFromElement @ react-dom-client.development.js:4275
reconcileChildFibersImpl @ react-dom-client.development.js:7881
eval @ react-dom-client.development.js:8059
reconcileChildren @ react-dom-client.development.js:8623
updateFunctionComponent @ react-dom-client.development.js:8916
beginWork @ react-dom-client.development.js:10524
runWithFiberInDEV @ react-dom-client.development.js:1521
performUnitOfWork @ react-dom-client.development.js:15134
renderRootSync @ react-dom-client.development.js:14958
performWorkOnRoot @ react-dom-client.development.js:14464
performWorkOnRootViaSchedulerTask @ react-dom-client.development.js:16218
performWorkUntilDeadline @ scheduler.development.js:46
<…>

I’m picking up clojure after a several year break, am new to clojurescript and javascript. So I hope I’m not asking something that has a simple and obvious answer. Any thoughts on why I’m getting an error with ResponsiveBar and not Bar?

Sunil

This is the relevant part of the error message. It basically means that ResponsiveBar is not a react component, but likely an object with a property holding that component. So, the relevant bit that is missing from your snippet is where this is coming from. I’m guessing its part of your ns :require structure and just translated incorrectly.

FWIW it is almost never useful to look at the source of JS components, as that often doesn’t have much to do with how its actually used. From the docs I found

import { ResponsiveBar } from '@nivo/bar'

which would translate to

(ns your.thing
  (:require ["@nivo/bar" :refer (ResponsiveBar)]))

;; and then used like you had
[:> ResponsiveBar ...]

You can find many examples of how things are translated in the docs. I’m not exactly sure how reagent translates the :data or :margin (i.e. nested maps). But should be fine.

You said changing it to Bar works, so same question there. Where does that come from? It should work the same way.

Another way to debug this is just quickly firing up a REPL (e.g. npx shadow-cljs browser-repl) and doing a (require ‘[“@nivo/bar” :as x]) and then (js/console.dir x)and looking at the Object in the browser devtools. It should be an object with a ResponsiveBar property (which likely is a function). (js/console.dir (.-ResponsiveBar x)) would let you inspect that thing alone. The REPL output alone unfortunately isn’t all that useful often for these kinds of objects, hence using js/console.dir and looking at it in the console.

1 Like

Appreciate the response and guidance. Here’s the entirety of my application so far. As you can see I’m importing Bar and ResponsiveBar the same way. And Bar and ResponsiveBar are used in the exact same way in code, only one works and the other does not.

(ns rgrr.client.app
  (:require-macros [cljs.core.async.macros :refer [go]])
  (:require [cljs-http.client :as http]
            [cljs.core.async :as async]
            ["@nivo/bar" :refer [Bar ResponsiveBar]]
            [reagent.core :as r]
            [reagent.dom.client :as rdc]))

(def histogram-data (atom {}))

(defn fetch-histogram []
  (go
    (let [resp (async/<! (http/get "/simulations/dummy/histograms"))]
      (reset! histogram-data (:body resp)))))

(defonce root (delay (rdc/create-root (js/document.getElementById "app"))))

(defn render-histogram []
  (rdc/render @root
              [:div {:style {:width "100%" :height "500px"}}
               [:> Bar                  ; ResponsiveBar
                {:data [{"country" "AD" "hotdogs" 60 "burgers" 80}
                        {"country" "AE" "hotdogs" 30 "burgers" 50}
                        {"country" "AF" "hotdogs" 70 "burgers" 90}]
                 :keys ["hotdogs" "burgers"]
                 :indexBy "country"
                 :height 500
                 :width 400
                 :margin {:top 50 :right 50 :bottom 50 :left 60}
                 :padding 0.3
                 ;; :colors {:scheme "nivo"}
                 ;; :axisBottom {:tickRotation 0}
                 }]]))

(add-watch histogram-data :redisplay render-histogram)

(defn ^:export ^:dev/after-load init []
  (fetch-histogram))

I tried the repl as you suggested, and I don’t see any difference between these objects. I guess my next step would be to cut out cljs and see if I can get these components working in JS. Is there a way to look at the JS that’s being generated for the cljs code? I tried using the browser debugger but the sources in there preserve the cljs code.

Sunil

I tried it and identified the issue.

It is one of those fun commonjs → ESM transition issues. It isn’t directly the @nivo/bar dependency but rather it using the react-virtualized-autosizer component. That one ships a commonjs variant and a ESM variant, which are used differently depending on which is referenced. The nivo thing uses it and expecting the ESM variant, but shadow-cljs still defaults to picking the commonjs variant. Hence you get a misleading error where the actual problem is way further down and not related to your code.

You can configure shadow-cljs to prefer the ESM files instead and that seems to fix the issue. Just set

:js-options {:export-conditions ["module" "import" "browser" "require" "default"]} 

in your build config. The default is ["browser" "require" "default" "module" "import"], so this is just basically re-ordering what is picked first and module/import here referring to ESM files generally.

Unfortunately this may lead to problems with other packages, because the opposite scenario where something may expect the commonjs code is also quite common. It may also just work fine forever, all depends on the mix of packages you use.

1 Like

Thanks, that worked. The change specifically had to be made in the :build configuration rather than the top level:

{:source-paths ["src/dev" "src/main" "src/test"]
 :dependencies [[reagent/reagent "2.0.0-alpha2"]
                [cljs-http "0.1.49"]]
 :dev-http {8080 {:root "public" :proxy-url "http://localhost:5000" :proxy-predicate rgrr.server/proxy?}}
 :builds {:client {:target :browser
                   :js-options {:export-conditions ["module" "import" "browser" "require" "default"]}
                   :modules {:main {:init-fn rgrr.client.app/init}}}}}

I don’t plan on building a large complex application, so hopefully I won’t run into the scenario you’ve described. If I understand the problem correctly, there are still many packages that are shipped as CJS, and shadow-cljs prefers CJS. I wonder if there’s a way to make this issue more apparent? I would never have figured this out on my own.

On an unrelated note, :proxy-predicate is not mentioned in the :shadow-cljs user guide at Shadow CLJS User’s Guide This was necessary for working with a non-clojure backend server. I could not work out where the documentation source resides, otherwise I would have contributed a PR.

I generally recommend that people do not use proxy at all. You can just have your server on localhost:5000 serve the static .js files. Unless for whatever reason that server can’t do that of course, then proxy is fine.

The docs are in this repo: GitHub - shadow-cljs/shadow-cljs.github.io: shadow-cljs homepage

If I understand correctly your recommendation then would be to compile the pages to static js using clojurescript, and then use my python server to serve up those js files? That makes sense. I’ll have to work out the specific workflow for that scenario. I might lose the live reloading that I get with shadow.

The :clientbuild stays as it. It outputs a public/js/main.js file and :dev-http is serving that file via localhost:8080. So, instead you just remove :dev-http entirely and either have your python server serve the public directly as static files, or change :output-dir in the build config to something other than ”public/js”.

The workflow really doesn’t change, you just remove one extra server. Live reloading is also not affected at all as :dev-http is not the thing doing it. So it doesn’t matter at all if you use a python server or :dev-http as far as that is concerned.

Thank you so much for all the guidance! I will try this out.

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.