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])
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":

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.