Fork me on GitHub
#reitit
<
2023-08-17
>
Dallas Surewood18:08:36

I'm trying to use reitit as a frontend router using controllers per https://github.com/metosin/reitit/blob/master/doc/frontend/controllers.md, but I notice it behaves differently than I'd like if I push state from inside the controller itself. Is anyone not using controllers at all, and if so, how are you managing "on mount/off mount" effects of pages?

p-himik18:08:16

Not sure how helpful it is, but I'm using kee-frame with reitit and it seems to be working just fine.

wevrem19:08:56

I’m using controllers on some routes and not on others. When I am using controllers it’s typically something like this where I fire off a re-frame event to take some action or to initialize some values in re-frame’s app-db (if you’re not using re-frame then it might instead be some globally-defined state atom):

(def routes
  ["/users" {:controllers [{:parameters {:path [:client-key]}
             :start (fn [{{:keys [client-key]} :path}]
                      (rf/dispatch [::fetch-users client-key]))}]}])
On some pages with forms that have their own localized state, I don’t want to clog up app-db with flags and stuff that only belong to the form, so I don’t use controllers, but instead use https://cljdoc.org/d/reagent/reagent/1.1.0/doc/tutorials/creating-reagent-components#appendix-b---with-let-macro`with-let`https://cljdoc.org/d/reagent/reagent/1.1.0/doc/tutorials/creating-reagent-components#appendix-b---with-let-macro, which is just sugar for a Form-2 component with some additional life-cycle stuff (so maybe that makes it sugar for a Form-3?):
(defn page [client-key]
  (r/with-let [state (r/atom {:editing? false})
               toggle-editing (fn [] (swap! state update :editing? not))
               detect-escape (fn [e] ...logic to detect ESC key press...)
               _ (.addEventListener js/document "keydown" detect-escape)]
    (let [editing? (:editing? @state)]
      [:form 
       ...alter view based on `editing?`
       ...button to toggle editing
       ...])
    (finally
      (.removeEventListener js/document "keydown" detect-escape))
You may not care if you are not using re-frame, but if I do have a situation where re-frame events need to do something with the local state atom, I close around it with a function defined inside page and send that function as a parameter to the event (for example, clearing an error).

Dallas Surewood21:08:18

The issue I'm running into is with having a controller that redirects, say, if session is not present

["secret"
     {:name ::secret
      :controllers [{:start #(wrap-restricted
                              (fn []
                                (log-fn "start" "secret controller")
                                (rum/mount (secret-page) app-div)))
                     :stop #(log-fn "stop" "secret controller")}]}]
wrap-restricted will redirect to login if not already logged in, but this causes an issue with the way Reitit recommends keeping track of route matches.
(defn ^:dev/after-load start-router []
  (rfe/start!
   (router)
   (fn [new-match]
     (swap! match (fn [old-match]
                    (let [result (when new-match
                                   (assoc new-match :controllers (rfc/apply-controllers (:controllers old-match) new-match)))]
                      result))))
   {:use-fragment false}))
If you redirect from a controller, you will end up at /login, but the swapping logic here will first swap the match atom to /login and then swap it to where we originall intended to go, /secret. It seems to do it in a weird order, so my match state is completely messed up. So I can't use controllers for redirects, for some reason.

wevrem22:08:03

What exactly is wrap-restricted? I guess it may not matter because, you are right, controllers get called during rfc/apply-controllers and that is not a good place to be then calling rfe/push-state or rfe/replace-state and triggering a new match. Could you have wrap-restricted redirect on a slightly delayed basis? This isn’t an issue with re-frame because events are dispatched on the next event loop and so we avoid the recursion issue you are dealing with.

wevrem22:08:07

Maybe in wrap-restricted you can do (js/setTimeout #(rfe/push-state name)) and that will queue it up to execute after apply-controllers finishes.

Dallas Surewood22:08:24

Yes that would probably work. In the future for myself, how were you aware this was bad practice? It can be a little hard between the 20 specialty libraries to make a functioning web app to know the idiosyncrasies of all of them. I would've thought a push state inside of a controller would've been fine because the first controller would execute and then the second one would execute later

Dallas Surewood22:08:45

Yeah essentially wrap-restricted just checks session and pushes state if it's not there

wevrem01:08:53

There is no one answer to your question about the future. I guess you just learn as you go along. And as things don’t work. 🙂 And as you ask questions here on slack. In this particular case, it’s mostly reading the reitit docs and source code on what apply-controllers and push-state do and learning that push-state calls the function you provided to start! as the on-navigate argument which calls apply-controllers which, if you call push-state from inside one of those controllers, gets you into a strange recursive situation. Report back if using setTimeout worked or not. I remember having another situation where I needed to do that, but since I use re-frame, I mostly don’t have to worry about it because re-frame asynchronously queues up events.