Managing State in ClojureScript
I’ve noticed a couple prominent forms of data management in ClojureScript projects. First is the “Form 2” structure, where you manage state within a Reagent atom and return a function. Second is managing state in the namespace.
Form 2 State Management
Let’s say we have a counter element that increments a counter when clicked. Using the Form 2 structure, we can implement this like so:
(defn counter-button []
(let [counter (reagent/atom 0)]
(fn [_]
[:p {:on-click #(swap! counter inc)} @counter])))
This function creates a Reagent atom starting at 0, and every time the p
element is clicked, the count increments and the DOM is updated with the
new count.
This state management structure comes with some pros and cons.
Pros:
- Data is only retained as long as it is needed (no hanging on to unused data in memory)
- Nothing else knows about
counter
–we can create 100 different counters displaying 100 different values!
Cons:
- Data is lost after the component is removed from the DOM.
- Nothing else knows about
counter
. If another part of the code needs to know the current value ofcounter
, this structure would make that task difficult.
One way we could address some of these issues is to retain the value of
counter
in a parent element.
(defn counter-button
([] [counter-button (reagent/atom 0)])
([counter]
(let [_ counter]
(fn [_]
[:p {:on-click #(swap! counter inc)} @counter]))))
(defn parent []
(let [counter (reagent/atom 0)]
(fn []
[:div#container
[counter-button counter]
[:h1 (str "The count is " @counter)]])))
While this may work fine over two components, continuing in this structure in more complex applications can end up in a large chain of data being passed down to lower-level components.
Namespace Management
If you don’t like the idea of passing data down a large chain of functions, you may elect to manage your state in atoms owned by the namespace.
(def counter (reagent/atom 0))
(defn counter-button []
[:h1 {:on-click #(swap! counter inc)} @counter])
Using this structure, you just create an atom somewhere within your namespace
and use it in your components. No need for any let
fn
structures.
As with Form 2 components, this also have some pros and cons.
Pros:
- Data persists throughout the lifetime of the application (basically until the page is refreshed)
- The scope of
counter
is not limited to the component–just about anything can reference its value.
Cons:
- Data persists throughout the lifetime of the application
- This can be bad if your atoms are very resource-heavy and end up causing performance issues.
- This can hinder the component’s reusability. Since the component references
a named atom, every counter will render the same value.
- This may be a desirable trait if you are looking for consistency across specific components.
I feel somewhat partial to the “Form 2” style given the reusability of components as well as the functional feel that they have. Although, there also seems to be quite a bit of power in managing state under namespaces. You really just gotta take a quick look at the problem and pick the best tool for the job.