Reagent views and multimethods

TL;DR: no form-2 or form-3 components as multimethods:

;; Don't do:
(defmethod page :hello [args]
  (setup-something! args)
  (fn [_]
    [:div "hello"]))

;; Do instead:
(defn hello-view [args]
  (setup-something! args)
  (fn [_]
    [:div "hello"]))

(defmethod page :hello [args]
  [hello-view args])
TL;DR

When creating multiple pages with the same layout (main navbar, static footer etc), a reasonable approach might be using multimethods to implement the main content of the page. Let's look at a simplified example:

(defonce state (r/atom {:current-view :front-page}))
(def current-view (r/cursor state [:current-view]))

(def pages
  ;; [[page-key page-title] ...]
  [[:front-page "Front page"]
   [:about "About"]
   [:setup "Setup"]])

(defmulti page :current-view)
(defmethod page :default [{:keys [current-view]}]
  [:div "UNIMPLEMENTED:" current-view])

(defn select-view! [view]
  (swap! state assoc :current-view view))

(defn navbar [{:keys [current-view]}]
  [:div.navbar
   (doall
     (for [[view title] pages
           :let [current? (= view current-view)]]
       ^{:key (str "navbar-" view)}
       [:a
        {:style {:padding "5px"
                 :background-color (when current?
                                     "#dddddd")}
         :class-name (when (= view current-view)
                       "active")
         :on-click (r/partial select-view! view)}
        title]))])

(defn main [state]
  [:div
   [navbar {:current-view @current-view}]
   [:div.main-content
    [page {:current-view @current-view}]]])

(defmethod page :front-page [_]
  [:div.content
   [:h1 "Front page"]
   [:div "Welcome etc. [front page content here]"]])

(defmethod page :about [_]
  [:div.content
   [:h1 "About"]
   [:div "[About-page content here]"]])

In some cases we might want to add a view that needs to setup something, use local state etc. i.e. what form-2 components are typically used for. You might try something like this:

(defmethod page :setup [_]
  ;; setup something etc. demonstrated by the print statement:
  (println "run on each open, form-2")
  (fn [_]
    [:div.content
     [:h1 "Do something, form-2"]
     [:div "[content here]"]]))

But when opening this view multiple times, the text is printed only once, and the main-content is stuck with the "Do something, form-2":

Note that "About" tab is selected

Let's see if making it a form-3 component helps, using :component-did-mount to perform the setup instead:

;; remove previous implementation if needed:
(remove-method page :setup)

(defmethod page :setup [_]
  (r/create-class
    {:display-name "setup-component"
     :component-did-mount
     (fn [_]
       (println "run on each open/mount, form-3"))
     :reagent-render
     (fn [_]
       [:div.content
        [:h1 "Do something, form-3"]
        [:div "[content here]"]])}))

Same problem. Seems like multimethods are incompatible with form-2 and form-3 components. Luckily, since form-1 components seem to work fine, there's a simple solution:

(remove-method page :setup)

(defn setup-view [_]
  (println "run on each open, wrapped")
  (fn [_]
    [:div.content
     [:h1 "Do something, wrapped"]
     [:div "[content here]"]]))

(defmethod page :setup [args]
  [setup-view args])

Perform any setup actions inside an ordinary reagent component, and simply use the multimethod to wrap around this component. A gotcha I've run into before.