Skip to content

Thinking With Links!

Brendon Walsh edited this page Aug 8, 2016 · 20 revisions

Introduction

"There is no Tree, only Graph" — The Ancients

When building larger systems on top of React you are often faced with the problem of structuring your application data so that you can feed the right information into all of your components. This problem is greatly exacerbated if you would prefer to embrace an immutable application model. Om Next is designed out of the box to fix this problem by making links (also called idents) a first class concept.

Setup

We assume you are now familiar with the previous tutorial setups. We will not cover that material other than your project.clj, which should look like the following:

(defproject om-tutorial "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.7.0"]
                 [org.clojure/clojurescript "1.7.170"]
                 [org.omcljs/om "1.0.0-alpha24"]
                 [figwheel-sidecar "0.5.0-SNAPSHOT" :scope "test"]])

The Application State

Your ns form should look like the following and you might want to enable printing for debugging purposes:

(ns om-tutorial.core
  (:require [goog.dom :as gdom]
            [om.next :as om :refer-macros [defui]]
            [om.dom :as dom]))

(enable-console-print!)

Let's look at a very typical problem in UI programs.

Imagine our application data is structured like the following:

(def init-data
  {:current-user {:email "[email protected]"}
   :items [{:id 0 :title "Foo"}
           {:id 1 :title "Bar"}
           {:id 2 :title "Baz"}]})

We hold everything about the current user in a hash map at the top level of the application state. We will be rendering subviews and these subviews will need this piece of global information. This information has no reification in a database, and we don't actually want to pollute our data with component view needs.

What to do?!?

Links to the rescue!

Before we do that let's define our read parsing functions:

(defmulti read om/dispatch)

(defmethod read :items
  [{:keys [query state]} k _]
  (let [st @state]
    {:value (om/db->tree query (get st k) st)}))

If you've been through the earlier tutorials this is pretty boring stuff.

Let's see our first example of embedding links into query expressions.

Embracing Links

(defui Item
  static om/Ident
  (ident [_ {:keys [id]}]
    [:item/by-id id])
  static om/IQuery
  (query [_]
    '[:id :title [:current-user _]]) ;; NEW STUFF
  Object
  (render [this]
    (let [{:keys [title current-user]} (om/props this)]
      (dom/li nil
        (dom/div nil title)
        (dom/div nil (:email current-user))))))

Here we see a new pattern for the very first time. We are already familiar with the 2 element vector representation of idents, but we've never embedded them directly into query expressions.

When resolving a query, parsing will perform lookups at the root of the application every time it encounters an ident. In this case the second part of the ident doesn't matter because it is a unique value in the application state - _ signifies this and a lookup will be performed using just the key :current-user. If instead we had a proper ident with an id component we would do a proper table lookup.

Besides this one novelty everything else should be familiar.

The remainder of the application looks like the following:

(def item (om/factory Item))

(defui SomeList
  static om/IQuery
  (query [_]
    [{:items (om/get-query Item)}])
  Object
  (render [this]
    (dom/div nil
             (dom/h2 nil "A List!")
             (dom/ul nil
                     (map item (-> this om/props :items))))))
(def reconciler
  (om/reconciler
    {:state init-data
     :parser (om/parser {:read read})}))

(om/add-root! reconciler SomeList (gdom/getElement "app"))

Add your own links and play around with the various possibilities. You can use links in joins and subselect properties if you wish.

By embracing links you no longer have to write contorted logic to feed information to children deep in your view hierarchy.

Remoting

Astute readers will wonder if this causes problems for remoting since this ident in this case is a client only concern.

The answer is "No".

Modify your read function to the following:

(defmethod read :items
  [{:keys [query state ast]} k _]
  (let [st @state]
    {:value  (om/db->tree query (get st k) st)
     :remote (update-in ast [:query] #(into [] (remove om.util/ident?) %))}))

At the REPL try the following:

(parser {:state state} (om/get-query SomeList) :remote)
;; => [{:items [:id :title]}]

No problems.

Conclusion

Stop thinking in terms of trees start thinking in terms of graphs and links.

Appendix

The entire source for this tutorial follows:

(ns om-tutorial.core
  (:require [goog.dom :as gdom]
            [om.next :as om :refer-macros [defui]]
            [om.dom :as dom]))

(enable-console-print!)

(def init-data
  {:current-user {:email "[email protected]"}
   :items [{:id 0 :title "Foo"}
           {:id 1 :title "Bar"}
           {:id 2 :title "Baz"}]})

(defmulti read om/dispatch)

(defmethod read :items
  [{:keys [query state]} k _]
  (let [st @state]
    {:value (om/db->tree query (get st k) st)}))

(defui Item
  static om/Ident
  (ident [_ {:keys [id]}]
    [:item/by-id id])
  static om/IQuery
  (query [_]
    '[:id :title [:current-user _]])
  Object
  (render [this]
    (let [{:keys [title current-user]} (om/props this)]
      (dom/li nil
        (dom/div nil title)
        (dom/div nil (:email current-user))))))

(def item (om/factory Item))

(defui SomeList
  static om/IQuery
  (query [_]
    [{:items (om/get-query Item)}])
  Object
  (render [this]
    (dom/div nil
             (dom/h2 nil "A List!")
             (dom/ul nil
                     (map item (-> this om/props :items))))))

(def reconciler
  (om/reconciler
    {:state init-data
     :parser (om/parser {:read read})}))

(om/add-root! reconciler SomeList (gdom/getElement "app"))