Fork me on GitHub
#fulcro
<
2021-07-19
>
damian10:07:46

Hello! I'm fighting to get SSR working alongside Dynamic Routing. I've noticed this legacy chapter in the documentation https://book.fulcrologic.com/#_code_splitting_and_server_side_rendering Is there a chance of finding a more up to date examples of such a feature? I currently am working on a freshly generated fulcro template (https://github.com/fulcrologic/fulcro-template). I edited the middleware.clj file to use this code:

(defsc Home [_ {}]
  {:ident (fn [] [:component/id :home])
   :route-segment ["Home"]
   :query []
   :initial-state {}}
  (dom/div
   (dom/h4 "HOME")))

(defsc Shop [_ {}]
  {:ident (fn [] [:component/id :shop])
   :route-segment ["shop"]
   :query []
   :initial-state (fn [_] {})}
  (dom/div
   (dom/h4 "SHOP")))

(defrouter RootRouter [this props]
  {:router-targets [Home Shop]})

(def ui-root-router (comp/factory RootRouter))

(defsc Root
  [this
   {:root/keys [ready? router] :as props}]
  {:query [:root/ready?
           {:root/router (comp/get-query RootRouter)}
           [::uism/asm-id ::RootRouter]
           [::dr/id ::RootRouter]]
   :initial-state (fn [params]
                    {:root/ready? false
                     :root/router (comp/get-initial-state RootRouter)})}
  (dom/div :.app-container
           (ui-root-router router)))

(defonce MYSPA (app/fulcro-app))

(defn index [csrf-token]
  (log/debug "Serving index.html")
  (let [normalized-db (ssr/build-initial-state (comp/get-initial-state Root) Root)
        normalized-db (-> normalized-db
                          (assoc-in (conj [::dr/id :app.server-components.middleware/RootRouter]
                                          ::dr/current-route)
                                    (comp/get-initial-state Shop))
                          (comp/set-query*
                           RootRouter
                           {:query [::dr/id
                                    [::uism/asm-id ::RootRouter]
                                    {::dr/current-route (comp/get-query Shop normalized-db)}]}))
        props (fdn/db->tree (comp/get-query Root)
                            normalized-db
                            normalized-db)
        root-factory (comp/factory Root)
        app-html (binding [comp/*app* MYSPA] (dom/render-to-str (root-factory props)))
        initial-state-script (ssr/initial-state->script-tag normalized-db)]
    (html5
     [:html {:lang "en"}
      [:head {:lang "en"}
       [:title "Application"]
       [:meta {:charset "utf-8"}]
       [:meta {:name "viewport" :content "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"}]
       initial-state-script
       [:link {:href ""
               :rel  "stylesheet"}]
       [:link {:rel "shortcut icon" :href "data:image/x-icon;," :type "image/x-icon"}]
       [:script (str "var fulcro_network_csrf_token = '" csrf-token "';")]]
      [:body
       [:div#app app-html]
       [:script {:src "js/main/main.js"}]]])))
The goal is that I want to have some routes like /shop-item/92334 where I want to take the shop-item id 92334 and expect the application to fetch the correct data for me. For this, I believe, I need to get will-enter to work right. My current expectation is that, once I get the code right and do curl localhost:3000 , I'll see that the HTML returned renders [:h4 Shop] and not [:h4 Home] .

Jakub Holý (HolyJak)16:07:41

I don't think there is better docs. The key problem with this approach to SSR is that it stops working as soon as you have any UI generated by a JS lib (a calendar widget,..). But perhaps one of the folks that experimented with ssr will advise you.

tony.kay16:07:09

So, what you're doing is "reasonable" but probably not scalable for a real app. If you really want sustainable SSR you probably want to make the app actually isomorphic. You want to start it in CLJ, run transactions to put it in the right state. This will also run the state machines and cause will-enter. Of course, this opens more cans of worms since you then probably try to do network I/O, which means you have to add a CLJ remote that resolves things via directly calling your server-side parser (instead of making net requests). Also note this is another reason why doing I/O in componentDidMount and such is not great: those will never trigger on server side. UISM will work properly, though. I have also not (and probably will not anytime soon) implemented a server-side version of React Hooks. So, hooks-based components are simply not going to work right. In response to @U0522TWDA, it is true if you really want very good SSR you either: • Wrap the js-only components in CLJC and give a reasonable "first render" approximation that is implemented by you. Often not too hard. Or: • Only use CLJC components that you write. This is usually not a great path, since it is quite expensive to ignore the library ecosystem.

❤️ 2
Jakub Holý (HolyJak)16:07:46

FYI I did this in a recent Fulcro https://gist.github.com/holyjak/6ead10c0b447e098026f3e24e4f1e519 but it does not start UISM and init routing etc as Tony suggested

joseayudarte9117:07:02

Many thanks for the answers! When you refer to “run transactions to put it in the right state” you mean that we should be able to get those isomorphic CLJC components to use their will-enter properties where we could run those mutations to fetch their data? In that case, how we would be able to use different remotes for each case? Many thanks in advance!

damian17:07:34

Yes @U0522TWDA I'm aware of this gist. It's been a huge help, thanks! What I'm not getting is how can I properly run the transactions in the BE. But thanks for reassuring me that it can be done 😄 Tomorrow I'll give it another, more specialised try.

tony.kay17:07:01

"remotes" are just maps that contain functions that can resolve data requests. There's no reason a network has to be involved, but of course it can be, even in CLJ. Fulcro does not provide an implementation for remotes in CLJ, but they are trivial to implement. So, if you want a Fulcro app whose logic, at least, completely works on the server side one thing you will have to supply in many cases is an implementation of remote(s) that can satisfy data needs. See the implementation of the full-stack book examples...they don't even use a network...they use pathom directly as a remote. Transactions and all of the internal processing that Fulcro provides works fine in CLJ. The problem is the rendering and Reactisms. React component lifecycle and Hooks, as mentioned above, are not implemented in CLJ, meaning SSR requires a lot more work on your part. Your alternative is to run a JS VM inside of the JVM with a fake DOM. I've successfully done that as well; however, it is expensive (in resources), but since you're running the real CLJS code it is easier to get something that more fully works.

joseayudarte9107:07:36

Ok, many thanks for the help!

Ryan Toussaint21:07:48

As a follow-up to this (more of a routing question, less SSR), I want to have a nav bar button to create a new ShopForm (where ShopForm is a defsc not defsc-form ). Basic Outline:

; On the nav bar
(ui-dropdown-item {:onClick (fn [] (rroute/route-to! this ShopForm {:id (str (new-uuid))}))} "New")


; Clicking 'New' above redirects to a ShopForm page with an id
(defsc ShopForm [_ {}]
  {:ident :shop/id
   :route-segment ["shop" :id]
   :query [:shop/id :shop/name]
   :initial-state (fn [_] {})}
  (dom/div
   (dom/h4 "SHOP")))
Would appreciate any context on the logic/transaction needed so that when someone clicks the ‘new’ button, it redirects to the ShopForm with a generated id. Thanks

Michaël Salihi23:07:34

I wrote this adapter https://github.com/prestancedesign/inertia-clojure for using Inertia https://inertiajs.com/ on Clojure back-end. Inertia now support SSR and integration is very smooth with CLJ / CLJS. I put a SSR version of a demo app on this repo's branch https://github.com/prestancedesign/pingcrm-clojure/tree/ssr, if you want to play with it. I didn't update the README, but check the shadow-cljs.edn, there is two builds. After cloning the repo, checkout on the ssr branch, then npm install following to npx shadow-cljs release app ssr . You can now run the app: 1. The JVM backend clj -M:run 2. The Node backend for SSR node out/ssr.js A live version can be access here: https://inertia.prestance-design.com/ Ask me, if you have questions.

Jakub Holý (HolyJak)18:07:36

@U0210DM86FR what you do looks reasonable. I do not have an answer handy but you could look what Fulcro RAD Demo does (even if you don't use RAD, you can easily port it back). Not sure about what is the correct routing function + invocation but there is one :)

Jakub Holý (HolyJak)18:07:35

Ah, I see you are using RAD. Notice it has (form/create! this AccountForm) , which is perhaps what you should use then (which assumes, I believe, that the form is under a top-level router). The function does essentially this:

(rad-routing/route-to! app-ish form-class {:action com.fulcrologic.rad.form/create-action
                                           :id     (str (new-uuid))})

Jakub Holý (HolyJak)22:07:32

Ah, sorry, I forgot it is not a RAD form. Anyway look at what the rad.form form-will-enter does and replicate it - the dr/route-deferred form-ident ... is the key I suppose.

jlmr17:07:11

I’m really struggling with something in Fulcro (I have asked some questions before in this channel, but despite very helpful answers, I’m still not getting it). I’m trying to render (recursive) “widgets” where the type of each widget is known by the server (attribute :widget/type). To render a widget, the client should use the correct Component for it (at runtime). Although each widget has the same attributes, ui-only state differs slightly. Which combination of features should I use for this? Union queries? Dynamic queries? Routers (dynamic or legacy)? Pre-merge? Initial-state? While reading the documentation I get the feeling all these features are somehow related, but I’m having trouble getting the big picture.

jlmr17:07:23

Any help or pointers are welcome!

tony.kay17:07:10

So, fully dynamic UI can be very expensive from a sustainable software architecture viewpoint, but the implementation is something I've talked to others about and have helped clients implement. From a Fulcro perspective the structure I recommend is to have a single Component class with a recursive query, and use multimethod dispatch for the rendering.

(declare ui-node)

(defmulti render-node (fn [props] (:node/type props)))
(defmethod render-node :div [{:node/keys [children class]}]
  (dom/div {:classes [class]}
     (map ui-node children))) ; note the recursive call to ui-node
(defmethod render-node :text [props]
   (:node/value props))
    
(defsc DOMNode [this props]
  {:query [:node/id
           :node/type
           :node/class
           {:node/children ...}]
   :ident :node/id}
  (render-node props))

(defn ui-node (comp/factory DOMNode {:keyfn :node/id})

(defsc Root [this {:root/keys [node]}]
  {:query [{:root/node (comp/get-query DOMNode)}]}
  (ui-node node))
Initialization can be done with initial-state, but that's not really the point, right? In this kind of app, I/O is used to build the initial state, so there'd be a load on start like:
(df/load! app :desired-ui/root DOMNode {:target :root/node})
where that resolver would simply return something like:
{:node/type :div 
 :node/id some-random-uuid
 :node/children [{:node/type :div 
                  :node/id some-random-uuid
                  :node/children [{:node/type text
                                   :node/id some-random-uuid
                                   :node/value "Hello world!"}]}]}

tony.kay17:07:55

These days I'd probably recommend using hooks-based support, instead of lifecycle stuff, since hooks are localized and would not require futher multimethods for things like :componentDidMount, etc.

tony.kay17:07:22

Your alternative is to create a big giant union query mess. The upside of the approach I mention is it is relatively easy to implement. The downside is that the query gets a big fat. Recursive components with union queries is another option, but you still have to have some kind of rendering dispatch, so I find the above approach better in general.

jlmr18:07:12

@tony.kay thanks for this, my query and underlying are already close to your example. Quick followup question, you would use React hooks like :componentDidMount to add (for example) an empty string for ui-only purposes to the client database? Like {ui/value ""} ?

tony.kay18:07:33

The problem with trying to make individual components with different queries is that you run into circular ref problems. This is a Clojure weakness, and not something that is easy to work around.

tony.kay18:07:04

no no...that is lifecycle. I'm talking about useState, useEffect, etc. See the hooks ns in Fulcro. Better support for that is in 3.5

tony.kay18:07:42

In terms of what goes in the database I would NOT make that part of the initial state or component responsibilities at all. Ever.

tony.kay18:07:14

once you're doing dynamic things like this you need to control the initialization of this state from something higher level than the rendering layer

tony.kay18:07:27

always try to treat the rendering layer as a pure expression of data..

tony.kay18:07:39

the exception to that rule is animations and other "marketing fluff"

jlmr18:07:51

Suppose one of the widgets is supposed to render a (controlled) text-input, where would you initialise and store the initial empty string?

tony.kay18:07:31

In the code that generates the UI, which is data. I think you are expecting that the act of rendering the UI initializes the UI...this is a chicken and the egg problem. It cannot do both. The UI should not be the initializer of this. You can use :initial-state as a stand-in for this purpose for the very first rendering frame, but that isn't what you're building here. You're building a dynamic UI, which by definition has no single known initial state.

tony.kay18:07:25

So, your resolver (or whatever) gives the tree of UI data to be rendered should mostly initialize things....What if that part of the UI already HAD a value...as in, it's a saved form? The server really needs to fill in the "current value"

tony.kay18:07:54

That may be a different stage, right? You pull the SHAPE of the UI from one data store, then fill in the state from antoher, and that whole initialized tree is what you send to the client.

jlmr18:07:30

I think I get your point, or at least I hope I do 😉. This particular “widget” it should always start as with a blank string. It’s purely an “entry-only” form, there will never be data on the server (or somewhere else) that’s saved and should be shown on initial load. I guess that’s why I was pretty sure the initial empty string is purely a ui concern.

sheluchin20:07:21

Is there anything wrong with using the tempid generated in the UI as the eventual real ID of the data in your backend storage? It's just a UUID anyhow..

lgessler21:07:31

the server can't accept tempids blindly because an attacker could do bad things (e.g. modify the JS so that existing IDs are used, potentially causing data loss or other problems)

👍 2
lgessler21:07:49

so you either need to make the server verify client-submitted IDs somehow or have the server just generate a new one.. i guess the latter is just easier