Fork me on GitHub
#clojurescript
<
2024-01-17
>
Dirk Meulenbelt09:01:20

Hello Clojurians! I have a problem that I've been grinding at for two days now and I still don't understand at all. I have created a webpage in ClojureScript with Reagent, and it has, as its landing page, a grid that displays projects that I retrieve from a backend. The call to the backend works fine and this page renders fine after I navigate to "/" from the navigation bar. However, after correctly rendering my content consistently that way, my main grid disappears upon either recompiling or refreshing the page. I know that this has something to do with the atom that it stores the backend's return in. Oddly enough, when I try to add some debug statements to my call to the backend, I find that it is correctly populated in all situations, compiling, refreshing, navigating. But, only upon navigating does it render my grid, and it does so consistently. What could I try? Here is my code (I have provided a few comments to show what happens):

(ns app.pages.main
  (:require [reagent.core :as r]
            [ajax.core :refer [GET]]
            [app.components.common :refer [handler error-handler]]
            [app.components.common :refer [nav-link]]
            [app.state :as state]))

(defn transform-project [project]
  {:name (get project "projects/name")
   :pageid (get project "projects/pageid")
   :tags (get project "projects/tags")
   :image (get project "frontimage")})

(def project-list (r/atom []))

(defn fetch-and-update-projects []
  (GET ""
    {:handler (fn [response]
                (reset! project-list (map transform-project response))
                ;; Debug code to check whether the projects are really there (they always are):
                (let [projects @project-list]
                (doall
                 (for [project projects]
                   (js/console.log "Project " project))))) ;; This always prints all the projects
     :error-handler error-handler}))

(defn main-grid []
  (fetch-and-update-projects)
  [:div {:class "flex flex-wrap justify-center max-w-md mx-auto"}
   (let [projects @project-list]
     (if (and projects (empty? projects))
       [:div {:class "flex justify-center items-center h-full"}
        [:h2 "No projects found"]] ; We get here each time after refreshing or compiling the page

       ;; This works -consistently- upon navigating to "/" via the navbar
       (doall
        (for [project projects]
          ^{:key (:name project)}
          [:div {:class "w-full md:w-1/3 p-4"}
           (nav-link (str "/" (:pageid project))
                     [:div {:class "rounded-lg shadow relative"}
                      [:img {:class "max-w-full h-auto shadow-lg rounded-lg" :src (:image project)}]
                      [:div {:class "absolute inset-0 flex items-center justify-center"}
                       [:h3 {:class "text-xl font-bold text-center text-black shadow-white"} (:name project)]]])]))))])

p-himik10:01:18

(def project-list (r/atom [])) should use defonce instead of def so that the state is preserved after a hot reload. Of course, that means that if you need to reset the state, you'll have to do so manually via either reloading the page or resetting the atom from the REPL or some other action. And while it doesn't affect anything, that second doall is not needed - you can have that for in there, Reagent will realize the sequence for you.

Dirk Meulenbelt10:01:01

I have changed the def to defonce and that seems to solve the problem of the grid disappearing upon re-compiling, but the error of the entire thing getting lost upon refresh or simply going to localhost:3000 remains. Any ideas? I've removed the doall

phill10:01:26

That fetch-and-update is in a strange place. It's bad luck to do that sort of thing in a render function in Reagent

phill10:01:08

The GET is async, so upon page load the atom will be empty when main-grid resumes after fetch-and-update. But since fetch-and-update was undertaken within the render function, the question of whether the atom changed (upon completion of the GET) might be murky

Dirk Meulenbelt10:01:53

Could you recommend something to do? Perhaps another atom that keeps the loading state?

phill10:01:02

Effects should be triggered by events, not by rendering

p-himik10:01:18

Good point. The ratom will be changed and it will lead to a re-rendering. However, that will also trigger a second fetching right away. An alternative to an event triggering fetching (that can sometimes be cumbersome) would be using a form-2 or form-3 component, or r/with-let where the fetching is initiated in the binding block (you can use _ as a bogus binding name).

p-himik10:01:42

entire thing getting lost upon refresh"Refresh" as in the Refresh button in the browser? Doing that will reset everything, but should also trigger another fetching. If the component is rendered empty and fetching is not triggered, I'd have to see a reproduction case.

Dirk Meulenbelt10:01:26

Yes, my grid disappears when I click refresh in the browser.

Dirk Meulenbelt10:01:38

It does indeed trigger another fetching as when I do that the debug statements in the fetch-and-update does indeed fetch the projects correctly

Dirk Meulenbelt10:01:22

In these cases it gets stuck in the "No projects found"

Dirk Meulenbelt10:01:43

I had tried adding a refetch function in place there but to no avail

p-himik10:01:19

Oh, try replacing map with mapv in that :handler.

Dirk Meulenbelt10:01:33

Done. Does not seem to affect anything

p-himik10:01:23

How is main-grid used?

Dirk Meulenbelt10:01:11

It's the main page. The routing map:

(ns app.routingconfig
  (:require [app.pages.main :refer [main-grid]]
            [app.pages.about :refer [about-page]]
            [app.pages.contact :refer [contact-page]]
            [app.pages.projectpage :refer [project-page]]))

(defn route-component-map [path]
  (cond
    (= path "/") main-grid
    (= path "/about") about-page
    (= path "/contact") contact-page
    :else (project-page {:project-id (last (clojure.string/split path #"/"))})
    ))
That is called here:
(ns app.routing
  (:require [app.state :as state]
            [app.routingconfig :refer [route-component-map]]))

(defn navigate [path]
  (let [new-content (route-component-map path)]
    (reset! state/current-content (new-content))
    (.pushState js/window.history nil "" path)))
And the layout renders it like so:
(ns app.layout
  (:require [app.components.logo :refer [logo]]
            [app.state :as state]
            [app.components.navbar :refer [navbar]]
            [app.routing :as routing])) ;; Make sure routing is required

(defn layout []
  [:div
   [logo]
   [navbar]
  @state/current-content]) ;; Dereference current-content here
Ultimately built with core.cljs:
(ns app.core
  (:require [reagent.core :as r]
            [reagent.dom :as rdom]
            [app.state :as state]
            [app.routing :as routing]
            [app.layout :as layout]))

(defn init-routing []
  (add-watch state/current-route :route-change
               (fn [_ _ _ new-route]
                 (routing/navigate new-route))))

(defn ^:export main []
  (init-routing)

  ;; Re-establish the current route based on the browser's URL
  (let [current-path (.-pathname js/window.location)]
    (reset! state/current-route current-path)

    ;; Set the current content based on the current route
    (routing/navigate current-path))

  ;; Render the app
  (rdom/render [layout/layout] (.getElementById js/document "app"))

  ;; Handle browser navigation events
  (.addEventListener js/window "popstate"
                     (fn [_] (routing/navigate (.-pathname js/window.location)))))
You're welcome to tell me if any of this seems dumb. It's my first clojure(script) project and frankly also my first website

p-himik10:01:46

Do not use Reagent components with (). Always use them in Hiccup vectors, []. All the way up to the call to render.

Dirk Meulenbelt10:01:19

Do you mean this part:

(defn navigate [path]
  (let [new-content (route-component-map path)]
    (reset! state/current-content (new-content))
    (.pushState js/window.history nil "" path)))
?

Dirk Meulenbelt10:01:35

Wow, that actually fixed the problem

Dirk Meulenbelt10:01:54

Thank you kindly sir. Could you explain or point me to where I can read the explanation?