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.
(rf/reg-event-fx
:init
(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
(rf/reg-event-db
:page
(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.
:page
(fn [db _]
(:page db)))
(defn get-page [page]
(cond
(= 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 atom
s 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.