Re-Frame + Reagent CLJS Router Pattern

TIL how to implement a routing component using Re-frame and Reagent in CLJS. This has been one of the joys for me in using CLJS, things that seem like they would be complicated on the surface, end up being just a few lines of code. With this routing component, the key is to have the current page key as a symbol in the Re-frame state (sweet sweet re-frame.db/app-db), then to subscribe to that state in our component that does the routing. Here’s how I did it, step-by-step.

Adding an init event.

Most re-frame applications have an event that initializes the app db with some state, matching the schema that the app-db should have throughout the life of the application. Within my init event, I do just that.

  (fn [{:keys [db] :as cofx} _]
    {:db (assoc db :time (js/Date.)
                   :page :home}))

Pay special attention here to :page :home in my db map that I am creating. I use a set of symbols to represent the different pages that are a part of my application. Before I get into how to register a page, I’ll first show what to do to call the init event and ensure that our DB populates. Re-frame as a concept can be thought of as data flowing through a series of stages. The first stage in Re-frame is to trigger events. Events then will trigger event handlers, shown by our :init handler above. Event handlers will then trigger effect handlers. :init is using the special effect handler :db to initialize the database. The keys in the map you return from your reg-event-fx function will be the effect handlers that are called. Effect handlers will then update the state of the world usually, which will then trigger subscriptions and typically rebuild the components of the app

Switching pages

Later in the app, it’ll be important to be able to navigate to a different page. In :init we set up the initial page, but now it’s important to change that page in the database when necessary. For this we need another event handler

  (fn [db [_ page]]
    (assoc db :page page)))

reg-event-db is short-hand for using reg-event-fx with the :db effect. With reg-event-db, you are saying that you want to update the app-db with some new values. The page value here is going to be a symbol that we will then use to register pages in our router. Simple enough right?

Reacting to page switches

Let’s now put this all together with a final motivating code sample

(rf/reg-sub ;; 1.
  (fn [db _]
    (:page db)))

(defn get-page [page]
    (= page :home) [home]
    (= page :menu) [vum/menu]
    (= page :calibrate) [calibrate/calibrate]))

(defn menu-button [event-key label] ;; 2.
  [:button {:on-click #(rf/dispatch-sync [event-key])} label])

(defn app-root []
  (let [page @(rf/subscribe [:page]) ;; 1.
        menu [:nav
              (menu-button :calibrate "Calibrate")]] ;; 2.
    (-> [:div.content]
        (conj menu)
        (conj (get-page page))))) ;; 3

1. Subscription handler

The final piece of the re-frame data flow (before it happens again), is that you must register subscriptions to then use and react to data within your UI components. This happens through reg-sub and subscribe. reg-sub is setting up a function that can then be keyed with the symbol you specify. In the above code sample, :page allows us to subscribe to the :page value that we set in the database with our event and effect handlers. You’ll see that subscription actively being used in the app-root component in a let binding. Those new to Clojurescript will see the @ sign. reframe.db/app-db is what’s known as an atom, and atoms are how Clojure represents values that might change. With the @ sign you are asking for the value that is within the atom at this point in time.

2. Using our page keys to registe a new page/event.

My menu-button component will create a button on scrreen that has an :on-click handler. This handler will dispatch an event that represents a page switch. You’ll see that the first argument to the function event-key, is our page symbol. :calibrate in this example is a touch more complicated than our existing handlers, because I have to perform some actions to the state first before I actually render the page. The :on-click handler though is an example of how you start the re-frame data loop once more, by dispatching another event that starts the flow of data through the system.

3. Rendering the router component

This is how we actually register the pages, and render the currently selected page on the screen like an SPA. Using the Clojure threading macro ->, we interactively put the component together. menu is the component that we created in our let binding using the hiccup-style syntax. The magic is in get-page. This function is taking the value bound in let to page, and rendering the current page component through get-page. get-page just calls another function based on the key passed to it that will then return the proper component to render. rf/subscribe is where the real magic happens, and you can think of it like setState in React. When updates happen to the central app-db, Re-frame can update and re-draw relevant components based on the returns of their subscriptions being different. So the routable component is bound to a certain page at one point in time, and when you navigate to a new one you bind it to a new page, triggering a re-draw. Pretty neat and pretty functional.

Wrapping up

If you made it this far, thanks for reading! Clojurescript is an incredibly powerful and productive language, and I plan to use it more and more for projects that I have coming up.


I run WindleWare! Feel free to reach out!

Subscribe for Exclusive Updates

* indicates required