Om Next for React devs: Application state

This is a series of articles on developing web applications in ClojureScript with Om Next. It is based on official Om Next tutorial, but targets JavaScript developers. It’s also an introduction into the language.

Important: I’m beginner in ClojureScript. This series is a part of my learning course.

Previous articles:

I’m using project setup from previous articles.

Introducing state

In Om the state of the app is stored in a single atom. Make sure you have read a short article where I’m trying to explain what is an atom in Clojure: “Single atom state tree buzzword explained”.

The changes in the atom state are managed by reconciler. It is responsible for merging changes into the state and updating components when a change was made. You can learn about reconciler here: “Om/Next: The Reconciler”. Let’s create a simple controlled input field and put its value into the atom.

(ns om-react.core
(:require [goog.dom :as gdom]
[om.next :as om :refer-macros [defui]]
[om.dom :as dom]))
(def state (atom {:value "Hello"}))
(defn set-value [e]
(swap! state assoc :value (.. e -target -value)))
(defui Input
Object
(render [this]
(dom/input
#js {:value (:value (om/props this))
:onChange set-value})))
(def reconciler
(om/reconciler {:state state}))
(om/add-root! reconciler
Input
(gdom/getElement "app"))

The reconciler holds the state and passes it into the component via props. Also we don’t render component directly anymore and doesn’t use it as a factory, reconciler will do it automatically. The component is reading the value from props and applies it onto input element. The onChange handler function will put a new value into the state using special swap! operation. It takes a state, a function which mutates state, a key where mutation should be done and a new value. When the value of the atom is changed, reconciler will re-render the component. Very familiar cycle, right? This is how React applications are done usually. The problem with this approach is that the component is coupled to the global state. We could go the Flux way and put all of the state transformations into the separate namespace and communicate with it via something like dispatcher. But Om Next takes a different approach which has its own benefits.

Queries

Om Next decouples components from state management logic using queries which are defined on those components. This idea is already present in Relay in JavaScript world. A query represents a request for some data that we want to be available in the component. The idea that client can specify precisely what data it requires is very important in modern web where we have lots of clients (desktop, mobile, IoT). This way it becomes much easier to build and consume APIs in contrast to traditional REST approach. The client now is the one who defines the demand and there’s no need anymore to modify particular REST endpoint to include additional data when you add another input field. In fact multiple endpoints doesn’t make sense anymore, we will see this later.

In Om there are two types of queries: reads and mutations. All read/write requests are handled by router. The router is actually called a parser, because queries themselves are data written in EDN format. The parser accepts two functions: one for reads and another one to handle writes. These functions takes a query and you decide how to handle different requests. To some extent you can think of queries as actions in Flux. Here’s a very simple read query example:

[:username]

And mutation query with a hash map of parameters:

'[(set-password {:password "123456"})]

A reader function

The function takes three arguments: env hash map with any context data necessary for reads (on front-end it’s automatically populated with application state), a key which we want to read and params map to modify the read. Evaluate the following in Figwheel REPL:

(in-ns 'om-react.core)
(defn read [{:keys [state] :as env} key params]
(let [st @state]
(if-let [[_ v] (find st key)]
{:value v}
{:value :not-found})))

The read function gets application state out of env and checks if there’s a requested key in the state. if-let is a macro which just combines the behaviour of both if and let. If there’s a requested key, it will return a hash map which contains a value behind that key in the state. If no such a key, the value in the hash map will be :not-found. So that’s a simple read request handling. Now create a parser with already defined read function in the REPL.

(def parser (om/parser {:read read}))

Define a state and request a read.

(def state (atom {:username "John Doe"}))
(parser {:state state} [:username :password])

The return value is the following hash map:

{:username "John Doe", :password :not-found}

A mutation function

Mutation function takes the same set of arguments, but returns a different value. We check if the current mutation is the one we need and if so, return a hash map with two fields: :action, contains a function which modifies the state and :value, it’s a map with a vector of keys which we want to re-read after the mutation is done.

(in-ns 'om-react.core)
(defn mutate [{:keys [state] :as env} key params]
(if (= 'set-password key)
{:value {:keys [:password]}
:action #(swap! state assoc :password (:value params))}
{:value :not-found}))

Now redefine the parser and apply mutation which fits the function.

(def parser (om/parser {:read read :mutate mutate}))
(parser {:state state} '[(set-password {:value "123456"})])

If you deref the state in the REPL, you’ll see that it is changed.

{:username "John Doe", :password "123456"}

Mutating state this way, again, might remind you of how it’s usually done in Flux with actions.

function handleAction(state, type, payload) {
if (type === SET_PASSWORD) {
state = Object.assign({}, state,
{ password: payload.password });
}
return state;
}

Queries in Om components

Let’s apply these to the UI. Save the following code and open the page in the browser. You will see there two input fields.

Open browser’s console and put some text into one of the inputs. On every change you’ll see something like this appears in the console:

[ 20.903s] [om.next] transacted '[(om-react.core/set-field {:value "h", :field :username})], #uuid "0cda4821-fbe7-4f0a-9c09-2ef625fa1cab"

In Om Next mutations made through transactions. The info in the console is a description of a such transaction. It’s common to keep a history of changes in transaction-based systems and Om is not an exception. Replace the UUID hash value with the one from your console and evaluate this snippet in the REPL.

(in-ns 'om-react.core)
(om/from-history reconciler
#uuid "0cda4821-fbe7-4f0a-9c09-2ef625fa1cab")

You’ll get the state of the app before this transaction was applied. Having a history of transactions makes it easy to go to any point of time in the application.

Back to the code. We are interested in a couple of new things there. At line 22 we have the following in the component definition:

static om/IQuery
(query [this]
[:username :password])

This is where you declare read query in the component. om/IQuery is a protocol which we implement with query method. The method is static. This way reconciler can collect and resolve a query for the whole application without instantiating components.

Quickly about protocols in Clojure: it’s basically an interface which you want to implement. Read about protocols here. Here’s a short example:

(defprotocol IWebDeveloper
(create-js-framework [this]))
(def developer
(reify
IWebDeveloper
(create-js-framework [_] "PoopJS"));
(def entrepreneur
(reify
IWebDeveloper
(create-js-framework [_] "AbsurdJS"));
(create-js-framework developer)
;; => "PoopJS"
(create-js-framework entrepreneur)
;; => "AbsurdJS"

Ok, the second new thing in the code is in the event handler functions at lines 31 and 36.

#(om/transact! this
`[(set-field {:value ~(.. % -target -value) :field :username})])

om/transact! applies provided mutation, which will be passed into mutation function in the parser and change the state of the app. Usually if something needs to be treated as data you would use quote special form. But in this case we want programatically add a value into the form and still treat it as data. We can use syntax-quote ` (backtick character), because it allows to unquote inside of it with ~ (tilde character). This will evaluate extraction of the value from event object and put it into the quoted form. Syntax-quote behaves a bit different from usual quote, read about this differences here.

Query parameters

Now when we have declarative queries there should be a similar way to parameterise them. Let’s set a goal for our next simple application: given a set of values render only a subset of them. Om Next has another static method which is used to define query parameters.

static om/IQueryParams
(params [this]
{:from 0 :to 5})

To use query parameters in the actual query, we need to modify it.

static om/IQuery
(query [this]
'[(:items {:from ?from :to ?to})])

A query with parameters should be a list of two items: query itself and a hash map of parameters. Notice that the values in the params map are symbols prefixed with ? character and their names are matching the names of the parameters in query params method. This way Om Next knows where params values should be applied in the query.

Here’s the code which solves our goal using query params. Put it into your project and see the result in the browser.

Given a vector of 10 values in the state, we are rendering only first five of them by declaring :from and :to params. These params are used in the read function of the parser to extract a part of the vector from the state. The component then renders these values as HTML list using map function.

Another alien feature of the language we are using here is multimethods. A multimethod system in Clojure is an approach to runtime polymorphism. A multimethod is defined using defmulti macro which is called a dispatching function. Dispatching function should return a value which will be used to match one of the many functions defined on that multimethod. In this case our dispatching function read returns a key, which represents a query. Methods are defined using defmethod macro. A method takes the name of the multimethod, the value on which we want to match and the rest of the function which in our case will perform read operation on the application state. A method with :default dispatching value will be called if there’s no match. Using multimethods allows us to separate a function into multiple functions by signature instead of putting everything into a single function. This gives a better readability and structure to the program. You can read more about multimethods here.

Dynamic queries

Back to previous example: what if we want to let user to specify which part of the list of values to render? It’s possible with om/set-query! function.

(om/set-query!
this
{:params {:from 0 :to 10}
:query '[:username (:items {:from ?from :to ?to})]})

The function takes a map with new params and/or query. When params or query is changed reconciler will update component. Here’s an example which answers our question. Save and run this code, you’ll see the same list of values. But this time there are also two number inputs and a button. Try to change values in the fields and hit the button to see how the list is changing.

set-query! is called from the onClick event handler attached to the button. It takes new values for params from input fields using React refs. A ref is defined as an attribute on the element. The element can be accessed using dom/node function, which takes instance and ref name.

Conclusion

Here we have learned that in Om Next, similar to React, it’s possible to manage application state in two ways: by modifying state directly, which couples everything together and thus doesn’t work well in larger application, and by using queries. Having queries co-located on components allows to get a single query for the whole application for requesting required data in a single network request. This kind of architecture makes it possible to build many clients for a single service with less pain. And since read/write operations are decoupled from components, the UI now doesn’t care where data comes from. Finally transactional mutations decouples components from application state and makes it trivial to take application to previous state.