Basic Om Concepts — A Concise Introduction



If you are struggling to get to grips with Om concepts, this may help

I've been learning the basics of Om recently, using the Om Basic Tutorial and the Om Intermediate Tutorial. It's been a bit of a struggle. Partly it's because I don't know React (on which Om builds), but it's also because, in my opinion at least, the tutorials don't do a very good job of explaining concepts. I found it necessary to do a fair amount of Googling in order to make sense of the tutorials.

Here's my attempt at explaining the basic Om concepts used in the tutorials. I do this in two stages: first I give a brief overview of some core concepts, then I expand on those core concepts and discuss some other concepts.

The intended audience here is someone who understands that Om is a functional approach to web UIs, and who has at least a rough idea of functional programming and of UI programming in general, and of Clojure and of the DOM in particular.

This article will probably be hard going if read in isolation, but if used as a resource when going through the Om tutorials it will be easier to follow.

Core Concepts

The core concepts of Om are described here, in the form of a glossary and in alphabetical order. This is intended as a brief reference, so don't necessarily expect it to make much sense on a first reading. You will probably find it helpful to refer back to this section when reading the more expansive decriptions that are given later.

  • App state
    • A single atom that represents the state of the application.
    • When the value of the atom changes, Om re-renders any roots that are attached to the atom.
  • Component
    • A thing that represents a piece of UI, and which gets rendered to produce a piece of UI.
  • Cursor
    • A piece of app state.
      • App state can be split into pieces, and different pieces can be passed to different components.
      • This means that components don't need to navigate through the app state to obtain the piece they are interested in, and so they can be more modular/reusable than would otherwise be the case.
    • A root cursor is a cursor on the whole of the application state.
  • Lifecycle methods
    • Methods that implement the render phase.
  • Owner
    • The backing React component for an Om component.
    • Created by Om.
  • Render phase
    • A span of time during which components are produced and during which rendering is done. This work is done by the lifecycle methods. (This is in contrast to the phase during which events are executed.)
    • If you know React, this may be useful: According to Om Issue #80, anything that happens directly because of React is part of the render phase and everything else (eg event handlers, core.async loops) is not part of the render phase.
  • Rendering loop
    • A loop that produces UI stuff. The body of the loop is executed by Om each time a particular part of the UI needs to be updated.
  • Root
    • An Om root is a DOM element that has an Om rendering loop attached to it.
      • !!!! Is that exactly right?
        • I'm struggling to find a precise definition of what an Om root is.
    • Om roots are attached to the app state.

More Detail

This section gives a fuller overview of Om, and concepts are presented in an order that allows later concepts to build on earlier concepts.

Roots, Rendering Loops and Components

An Om root is a DOM element that has an Om rendering loop attached to it.

The body of the rendering loop:

  • is called by Om each time that part of the UI needs to be updated
  • returns an Om component (which is a thing that represents a piece of UI).

An Atom for App State

The app state is held in an atom. When the value of the atom changes, Om re-renders any roots that are attached to the atom.

Everything in the atom should be an associative data structure. So, for example, maps and vectors are allowed but lists and lazy sequences are not.

Creating Roots and Rendering Loops

The function om.core/root creates an Om root. In other words it establishes an Om rendering loop on a specific element in the DOM. It takes three arguments:

  1. f — a component constructor function
    • This takes the following two arguments:
      • the app state, or, more precisely, a root cursor on the application state. See the section on cursors below for more details of what this means.
      • owner — the backing React component for the root. (This is created by Om, and you don't usually need to use it.)
    • The return value is an Om component, which is an instance of IRender or IRenderState. See below for details of how to create Om components.
  2. value — the application state
    • One of:
      • a tree of associative ClojureScript data structures
      • an atom wrapping such a tree.
  3. options — a map
    • This map may contain any key allowed by om.core/build, which is described briefly in the “Creating Om Components” section.
    • The key :target is required. The value for this key should be a DOM element.

Calls of om.core/root are idempotent — if there are multiple calls with the same arguments the effect is the same as if there is a single call. Only one Om render loop is allowed on a particular DOM target.

If om.core/root is called a second time on the same target element, the previous render loop will be replaced.

!!!!

So, strictly speaking, is om.core/root idempotent when there are multiple calls with the same arguments?

Maybe it depends on how you look at it:

  • One point of view:
    • It is idempotent because the old render loop and the new one are kind of equal.
  • Another point of view:
    • It is not idempotent because it's replacing the render loop.

Multiple roots are permitted, so you can set up a skeleton HTML document and attach different roots to different elements within that document.

Creating Om Components

There are several ways to create an Om component:

  • Reify om.core/IRender
  • Reify om.core/IRenderState
  • Call om.core/component
    • This is syntactic sugar for creating components that implement om.core/IRender and don't need to access the owner argument.
  • Call om.core/build

om.core/build-all creates a sequence of Om components.

Kinds of State

The Om tutorials make use of two kinds of state.

Firstly, there is app state, which has already been discussed.

Secondly, there is component-local state. This can be used to store:

  • UI state
  • core.async channels, to allow components to communicate with each other
  • !!!! Are there other uses of component-local state?

The following things deal with component-local state:

  • The protocol om.core/IInitState
    • Used to set up a component's initial state.
  • The :init-state option in the map passed to om.core/build or om.core/build-all
    • This allows a component to pass state to subcomponents.
  • The function om.core/get-state
    • Gets state from a component.
  • The protocol om.core/IRenderState
    • State is passed in to this and can be used when rendering.
  • (There are other things that deal with component-local state, but they are not used in the tutorials.)

There are other kinds of state. See this Stackoverflow question about different kinds of state in Om.

The Control Flow Provided by Om

A top-level view of Om's control flow is that the UI is updated whenever the app state changes and whenever component-local state changes. This section probes some of the details within that.

Consider the component constructor function ccf below:

(defn ccf [app owner]
  (reify
    om/IInitState
    (init-state [_] ...)

    om/IWillMount
    (will-mount [_] ...)

    om/IRender
    (render [_] ...)))

The table below shows when the various functions are called.

ccfinit-statewill-mountrender
Page load (or browser refresh)yyyy
Update UIyy
Re-evaluate om.core/root formyyyy

The table shows that:

  • When the browser is refreshed and on page load, all four functions are called.
  • When the UI is updated, the component constructor functions and render functions are called.
    • When updating the UI, the call of the component constructor function does not cause init-state and will-mount to be called — they are only called when attaching an Om root to a DOM element.
  • If the om.core/root form is re-evaluated, all four functions are called.

The tutorial points out the following:

  • Component constructor functions are called multiple times.
  • So be careful where you create component state.
  • Don't do it in let blocks that wrap the uses of reify.
  • Good places to do this are:
    • om.core/IInitState
    • om.core/IWillMount.

Changing App State

App state can be changed using the following functions:

  • om.core/transact!
    • This is the primary way to transition app state. It takes the existing value as input and produces a new value. It is analogous to clojure.core/swap!.
  • om.core/update!
    • This sets the app state to a new value without making use of the existing value. It is analogous to reset!.

Om and DOM Elements

Om has support for creating and accessing DOM elements. The om.dom namespace provides the support for this.

For creating DOM elements there are functions such as om.dom/ul and om.dom/li. The first argument to these functions is the collection of DOM attributes for the element, which is either nil or a piece of JavaScript data. The #js reader macro is used to create JavaScript objects.

The function om/get-node can be used to access DOM elements. The lookup is done on the :ref attribute, which should be set when the node is created.

Cursors

From the tutorials and the documentation, I got the following:

  • In Om, all the application state is held in a single atom, but most components are only interested in a particular piece of state. Cursors are used to manage this.
  • Cursors split one big atom into smaller sub-atoms that remain in sync with the state held in the root atom. You can modify the value of the sub-atoms.
  • Cursors are a reference type provided by Om.
  • There is a root cursor, which contains the whole of the application state. This is passed to the root's component constructor function.
  • You can create sub-cursors by calling get or get-in on cursors.
  • In the render phase you can treat a cursor as a value. The operations available on the cursor are the same as on the contained value. Cursors support all the interfaces that are supported by PersistentMap and PersistentVector.
  • Outside of the render phase you cannot treat a cursor as a value. You can dereference to get the value.
  • Cursors are only consistent during the render phase.

In fact, cursors don't have to be pieces of the app state — we can arbitrarily process the app state. For example with maps we can just assoc in the appropriate keys. The basic tutorial does this in the people function, which replaces the :classes part of the app state. (FWIW, I think this is probably a bad thing to do, and I think the example in the tutorial could be refactored to avoid the need for this.)

For some details on cursors, going into implementation details, I found David Nolen's The Functional Final Frontier talk useful, between 13:40 minutes and 16:20.

Multiple Components Showing the Same Piece of State

One of the notable things about functional UIs is that you can have multiple components showing the same piece of state. There is zero coordination effort.

This is fantastic. It is so much easier than trying to coordinate things yourself.

The tutorial demonstrates this in the professors and students example.

References

Here are some of the resources I've found useful while learning Om:

And here are some things I want to read/re-read or watch/re-watch some time soon: