Stateful Reagent
Manage Component State With Reagent
Currently, Reagent is my favorite front-end UI framework, mostly because it makes my UI code look clean and easy to reason about, one thing that I was missing is a simple way to manage local component state (like .setState()
in react).
Fortunately, after doing some digging around the API docs and source, I found that it’s actually very simple to do
with Reagent! I found 3 ways (not counting r/create-class
) and all of them are based on r/atom
, this is nice because you don’t have to learn new concepts.
I assume that you are familiar with React’s .setState()
method and it’s utility, so I’m gonna skip “why” part and jump right into the “how”, but before we continue, let’s quickly look at 1 obvious way that doesn’t work and why.
this is broken:
(defn broken-counter []
(let [counter (r/atom 0)]
[:div
[:label "current:" @counter]
[:button {:on-click #(swap! counter inc)} "add 1"]]))
The problem with this code is that let
will be evaluated each time this component renders,
and a new atom with the initial value will be created each time! remember how reagent/atom
s work?
Basically when you deref (@
) an atom, reagent will remember the function that deref
ed the atom, later when the atom is changed (click in this case) this function will be called, and in this example the function will create a new atom and deref it.
But we want to retain the atom for the entire lifespan of the component, so how do we fix this?
Method #1: return a function
The let
example above can be written like this:
(defn counter []
(let [counter (r/atom 0)]
(fn render [] ; <-- note here, we return a function
[:div
[:label "current:" @counter]
[:button {:on-click #(swap! counter inc)} "add 1"]])))
Notice that we return a function not a nested vector (hiccup),
reagent smart enough to accept function result, And when you do this the outer part essentially
works like componentWillMount
in React (i,e runs only once, when the component is mounted), and
the inner function will be called each time counter
changes.
Be careful with this!
I personally don’t like this method, I think it’s too error prone, and the worse part is that the the error is really hard to debug. Also there is a horrible pitfall with this approach when a component accepts arguments.
Let me quickly show you:
(defn counter [name]
(let [counter (r/atom 0)]
(fn render [name] ; <-- see how I repeat the argument here?
[:div
[:label name ": " @counter]
[:button {:on-click #(swap! counter inc)} "add 1"]])))
If we don’t repeat the args in the inner function, this code will break without any error, because
remember - the outer function is only invoked 1 time when the component is mounted,
this means that name
is not updated.
But wait, can’t we use a macro to solve this? sure we can :)
Method #2: using with-let
macro
Reagent 0.6.0 and up comes with a macro that will handle this inner function mess for us, it’s
called with-let
and it works almost like let
, except that it’s doing the
right thing for reagent components (and it supports component unmount hook via :finally
).
Here’s how you use it:
(defn counter []
(r/with-let [counter (r/atom 0)]
[:div
[:label "current:" @counter]
[:button {:on-click #(swap! counter inc)} "add 1"]]))
Notice that this example is identical to the let
example. This is much
nicer & simpler because we don’t have to worry about weird edge cases, it just works.
99% for the time I use this method, but for completion, let me show you a 3rd method to use local state.
Method #3: using r/state
It turns out that reagent comes with local state management built-in, ha!
(defn counter []
(let [this (r/current-component)
state (merge {:counter 0} (r/state this))]
[:div
[:label "current:" (:counter state)]
[:button {:on-click #(r/set-state this {:counter (inc (:counter state))})} "add 1"]]))
The only “magical” part here is r/current-component
, note
that this function is context sensitive, so for example, if you invoke it form a callback, bad things will happen!
that’s because Reagent has no way of mapping callbacks to components, without some serious macro voodoo..
this is why it’s very useful to use (let [this ...]
here.
The next 2 functions (r/state
and r/set-state
) will take the component as the first argument.
The best way to think about this is that each reagent component has an r/atom
attached to it (you can actually access this atom with r/state-atom
).
So when this is useful over with-let
?
I think it’s a shortcut for when you have at least 2 state variables (an atom with a map..) and state is independent of the previous state (unlike the counter example).
Here’s an example:
(ns silly.login
(:require [reagent.ore :as r]
[promesa.core :as p]))
(defn login-form [do-login navigate-to]
(let [this (r/current-component)
state (r/state this)]
[login-screen {:loading (state :loading)}
[message (state :error-message)]
[:div.login-box
[:input {:on-change #(r/set-state this {:email (.-val %)})}]
[:input {:on-change #(r/set-state this {:password (.-val %)})}]
[:button {:on-click (fn []
(r/set-state this {:loading true})
(-> (do-login (select-keys state [:email :password]))
(p/then #(navigate-to :home-page))
(p/error #(set-state this {:error-message %}))
(p/finally #(set-state this {:loading false}))))))
Here r/set-state
saves us from repeatedly typing swap! state merge
, I think it’s a nice touch,
but either way, it’s just a shortcut for using an atom.. so it’s cheap, no crazy abstraction tax..
That’s it! let me know if I missed something :)
(load :comments)load