Fork me on GitHub
#re-frame
<
2022-02-15
>
fadrian14:02:47

I'm trying to get a small re-frame example SPA running (based loosely on the tutorial example from the re-frame docs). Here's the code:

(ns atlas.main
  (:require [react :as rct]
            [react-dom :as rctd]
            [reagent.core :as r]
            [reagent.dom :as rd]
            [re-frame.db :as db]
            [re-frame.core :as rf]))

;; Events
(rf/reg-event-db
 :initialize
 (fn [_ _]
   {:name "world"
    :color "red"}))

(rf/reg-event-db
 :on-color-change
 (fn [db [_ new-color]]
   (assoc db :color new-color)))

(rf/reg-event-db
 :on-name-change
 (fn [db [_ new-name]]
   (assoc db :name new-name)))

;; Subscriptions
(rf/reg-sub
 :name
 (fn [db _]
   (:name db)))

(rf/reg-sub
 :color
 (fn [db _]
   (:color db)))

;; Rendering
(defn greeting-string [nm]
  (str "Hello, " nm "!"))

(defn greeting
  []
  [:div.greet
   {:style {:color @(rf/subscribe [:color])}}
    (greeting-string @(rf/subscribe [:name]))])

(defn color
  []
  [:div.color-input
   "Message color: "
   [:input {:type "text"
            :value @(rf/subscribe [:color])
            :on-change #(rf/dispatch [:on-color-change (-> % .-target .-value)])}]])

(defn the-name
  []
  [:div.name-input
   "Message name: "
   [:input {:type "text"
            :value @(rf/subscribe [:name])
            :on-change #(rf/dispatch [:on-name-change (-> % .-target .-value)])}]])

(defn display
    []
    [:div
     [greeting]
     [color]
     [the-name]])

;; Initialize
(defn ^:export main! []
  (rf/dispatch-sync [:initialize])
  (rd/render [display]
             (js/document.getElementById "root")))
Everything seems to compile correctly, but when I try to run main! to kick off the app, I get the following error:
(main!)
:repl/exception!
; 
; Execution error (TypeError) at (<cljs repl>:1).
Cannot read properties of undefined (reading 'cljs$core$IFn$_invoke$arity$2')
Also, when I use the browser to try to view the page, it simply returns an empty page. I checked the page using Chrome dev-tools and it seems to be returning the initial HTML correctly. Since I'm relatively new to this, I really have no idea how to debug this further (although, I did trace the error to the 2-arity apply function in cljs.core, which would seem to point to something wrong with a function having two arguments - either one of the event or subscription functions). Can anyone see why I'd be getting this error and why the DOM isn't being changed?

p-himik14:02:39

Are you sure that REPL session is connected to the JS runtime that's on that very web page? Why are you kicking off the app from the REPL anyway? The most common way is to either call the exported symbol from a <script> tag in HTML or via your build tool - at least, shadow-cljs has a support for it in its :modules.

fadrian14:02:50

I've got the following in my shadow-cljs.edn file:

:builds
 {:app {:output-dir "public/js"
        :asset-path "."
        :target :browser
        :modules {:main {:init-fn atlas.main/main!}}}}
which should tell it to kick the main! function initially, but that doesn't seem to work either. That's why I'm trying to kick it off manually. I am using calva for development and I'm pretty sure that I'm connected to a js runtime - at least according to the error message I'm getting (Whether or not it is the js runtime is another issue. As I said, I'm new at this.).

p-himik14:02:26

That config above should be everything that's needed - don't start the app in the REPL. When you load your page and it's blank, the first step should always be to check the browser's JS console - you'll probably see that error in there as well. As to the error itself - in your browser's DevTools you can set it to break on all uncaught exceptions. It should help with figuring out what's wrong. Also the JS console should have a much better stacktrace than the REPL - which should also point you in the right direction.

fadrian14:02:06

Thanks. I'll give it a try.

fadrian15:02:31

It's dying in the initial attempt to render the components. I'm getting the error from this line of code:

return reagent.dom.render.cljs$core$IFn$_invoke$arity$2(new cljs.core.PersistentVector(null, 1, 5, cljs.core.PersistentVector.EMPTY_NODE, [atlas.main.display], null),document.getElementById("root"));
});
This would seem to indicate that the issue is a problem with the document.getElementById call, as this is object that might be undefined, but I have a div with the id "root" in my HTML, which should be found by this call:

<html>
  <head>
    <meta charset="utf-8" />
    <title>atlas frontend</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="/js/main.js"></script>
  </body>
So that probably isn't it. The other possibility is that re-frame is passing something odd to the first argument of the reagent-dom/render function. According to the reagent documentation, the first parameter either takes a React element or a vector containing hiccup that's supposed to be rendered. From my reading of the PersistentVector call in core.cljs, this should be returning a vector with the contents atlas.main.display, which is one of my rendering functions:
(defn display
    []
    [:div
     [greeting]
     [color]
     [the-name]])
So again, everything should be going swimmingly, but the cljs$core$IFn$_invoke$arity$2 call still fails when attempting to apply reagent.dom.render.

p-himik15:02:11

The original error: Cannot read properties of undefined (reading 'cljs$core$IFn$_invoke$arity$2') The place: return reagent.dom.render.cljs$core$IFn$_invoke$arity$2 [...] That means that reagent.dom.render is undefined. Meaning, the function render isn't there in the reagent.dom namespace. Does shadow-cljs not give you any warnings about that when you build? Do you have anything in shadow-cljs.edn regarding warnings - maybe you put a flag in there to ignore all warnings?

fadrian15:02:56

No errors. I am getting some warnings from clj-kondo about namespaces being declared, but never used, but nothing about the render function. I did have to --force the installation of react, react-dom, and create-react-class into npm, but the error I'm getting is upstream of the React code. The tutorial said supposedly that the only dependencies you needed were re-frame.core and reagent.core, but when I tried that, the compiler complained about reagent.core/render being deprecated and moved to reagent-dom/render. So I required reagent-dom which then complained that it needed react-dom, which I added, which then required the inclusion of react. As such, I now have about six :required packages in my ns declaration. As far as I can tell, everything in re-frame and reagent are hooked up correctly, but as I said above, I had to use that --force when installing React, which may be an issue. I'll check that next.

p-himik15:02:15

You shouldn't have needed --force and create-react-class, unless you're using some old Reagent+React version. If you create a repo with all the necessary code and instructions on how to build and run it, I can take a look.

fadrian18:02:56

I finally got it working: First, I removed the dependencies I had initially added to the project from the package.json file. I then removed the cljsjs declarations of those libraries from the shadow-cljs.edn file. Finally, I removed the versions of react, react-dom, and create-react-class that I had forced in to the local npm module library, and re-installed only react and react-dom. After that, it's working.

fadrian18:02:46

I am still aggravated each time I have to resolve library dependencies in Clojure namespaces. Clojurescript seems worse, as you have to worry not only about the cljs libraries, but also the npm libraries they depend on. The messaging around these are atrocious, as well, but that's enough of a rant for now. In any case, thanks for your help. It gave me enough info to (a) figure out how to debug a bit more effectively and (b) finally fix the issue.

p-himik18:02:21

Glad you got it working! Regarding dependencies - I can offer some rudimentary advice: • When something doesn't work, don't attempt any fix that you may find online without understanding what it's actually doing and why it is needed to fix your problem. E.g. --force that you tried initially is usually a severe smell that something's wrong (and not just in NPM - that applies to other places as well). Same with create-react-class - if none of your packages depend on it, then it's not clear why it's needed, so first you should get to the bottom of that before installing it • With NPM, it's very common that you'll need to nuke node_modules maybe along with package-lock.json and try npm i again. In fact, when something really weird is going on with JS dependencies being involved, that might easily be the first recommended step • Whenever you use a CLJS library that depends on some JS library, don't just run npm i some-js-library. Instead, check which version of some-js-library that CLJS library needs and install the right one. Use the NPM's -E flag if you're developing a app and not a library to make sure that the version is pinned to the exact and not an approximate one • This is more of a "nice to have" advice. Don't use cljsjs libraries. If something in your dependency tree uses cljsjs, it's better to replace that cljsjs library on your classpath with something that simply makes the corresponding JS package installed via NPM available to CLJS. Shadow-cljs documentation has a section about it

fadrian21:02:40

Thanks again. Those words of advice are very helpful. Luckily, I think I have all of the libraries I need at this point. I'm using re-com and I've gotten my first few widgets up and running. My ultimate goal is to have a declarative set of data that drives the app, but right now I'm settling for hardwired choices in widgets while I figure out how to get the best code. Thanks again for your help. It was invaluable.

👍 1
olive23:02:32

if i dispatch-sync an event that updates the db, and then immediately @(rf/subscribe [....]), are the updates to the db guaranteed to be available and current in the subscription?