Skip to content

Instantly share code, notes, and snippets.

@holyjak
Created January 22, 2021 11:30
Show Gist options
  • Save holyjak/6ead10c0b447e098026f3e24e4f1e519 to your computer and use it in GitHub Desktop.
Save holyjak/6ead10c0b447e098026f3e24e4f1e519 to your computer and use it in GitHub Desktop.
Experiments in Fulcro SSR with dynamic routers
(ns ssr-test
"Try server-side rendering in Fulcro where we want to display a non-default
dynamic router target.
*BEWARE*: This is an exploration. I have *no* idea what is the correct way."
(:require
[com.fulcrologic.fulcro.application :as app]
[com.fulcrologic.fulcro.algorithms.denormalize :as denorm]
[com.fulcrologic.fulcro.algorithms.server-render :as ssr]
[com.fulcrologic.fulcro.components :as comp :refer [defsc]]
[com.fulcrologic.fulcro.dom-server :as dom :refer [div label input]]
[com.fulcrologic.fulcro.routing.dynamic-routing :as dr :refer [defrouter]]))
(defsc AlternativeTarget [_ _]
{:query ['* :whatever]
:initial-state {:whatever 123}
:route-segment ["alt"]}
(dom/p "Alternative"))
(defsc DefaultTarget [_ _]
{:query ['* :default]
:initial-state {:default true}
:route-segment ["default"]}
(dom/p "Default"))
(defrouter TopRouter [_ _]
{:router-targets [DefaultTarget AlternativeTarget]})
(defsc Root [this {:keys [router]}]
{:query [{:router (comp/get-query TopRouter)}]
:initial-state {:router {}}}
(dom/div
(dom/h1 "Test App")
((comp/factory TopRouter) router)))
(defn server-render []
(let [; Should I use --v ? In this example the result HTML is the same anyway.
;state (ssr/build-initial-state
; ; {} ; with this <-- the state would be empty so let's ensure some stuff there:
; (comp/get-initial-state Root)
; Root)
state {}
state' (-> state
(assoc-in (conj [::dr/id :ssr-test/TopRouter]
::dr/current-route)
(comp/get-initial-state AlternativeTarget))
(comp/set-query* ; do we need to re-index? But our `state` is likely not `@(:state app)`
TopRouter
{:query [::dr/id
;[::uism/asm-id router-id]
{::dr/current-route (comp/get-query AlternativeTarget state)}]}))
script-tag (ssr/initial-state->script-tag state')
props (denorm/db->tree (comp/get-query Root)
state'
state')
;_ (do (app/set-root! app Root {:initialize-state? true}))
app (app/fulcro-app {:initial-db state'})
html (binding [comp/*app* app]
(dom/render-to-str ((comp/factory Root) props)))]
(str
script-tag
"\n"
html)))
(comment
(server-render)
,)
@holyjak
Copy link
Author

holyjak commented Jul 22, 2021

Tony comments:

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 @holyjak, 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.

"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.

I.e. you want to init routing and UISMs similarly to what the fulcro-rad-demo init does but in clj. Notice that it also triggers the fix-route mutation, which calls routing/route-to! (RAD's thin wrapper of dr/change-route!) to display the desired routers based on the current URL. As Tony explains, if any of those does any df/load! or similar, you also want to register a server-side "remote" with the app so that those would work.

@holyjak
Copy link
Author

holyjak commented Aug 13, 2021

Here are some functions for working with Fulcro dynamic routers that might be useful - most of them work without React/active mount.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment