Skip to content

Thinking With Links!

dnolen edited this page Nov 25, 2015 · 20 revisions

Introduction

"There is 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 the 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 components 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 _]])
  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 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]
    (println "Render")
    (dom/div
      (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.

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]
    (println "Render")
    (dom/div
      (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"))