Basic Om Concepts — A Concise Introduction
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.
- A piece of app 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.
- !!!! Is that exactly right?
- Om roots are attached to the app state.
- An Om root is a DOM element that has an Om rendering loop
attached to it.
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:
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
orIRenderState
. See below for details of how to create Om components.
- This takes the following two arguments:
value
— the application state- One of:
- a tree of associative ClojureScript data structures
- an atom wrapping such a tree.
- One of:
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.
- This map may contain any key allowed by
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.
- This is syntactic sugar for creating components that
implement
- 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 toom.core/build
orom.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.
ccf | init-state | will-mount | render | |
---|---|---|---|---|
Page load (or browser refresh) | y | y | y | y |
Update UI | y | y | ||
Re-evaluate om.core/root form | y | y | y | y |
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
andwill-mount
to be called — they are only called when attaching an Om root to a DOM element.
- When updating the UI, the call of the component constructor
function does not cause
- 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 ofreify
. - 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!
.
- 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
om.core/update!
- This sets the app state to a new value without making use of
the existing value. It is analogous to
reset!
.
- This sets the app state to a new value without making use of
the existing value. It is analogous to
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
orget-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
andPersistentVector
. - 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:
- The Om Wiki, which includes the tutorials.
- David Nolen's The Functional Final Frontier talk.
- Christian Johansen's Functional UI programming talk.
- Magomimmo's om-start Leiningen template for creating Om projects compatible with nREPL compliant editors/IDEs.
And here are some things I want to read/re-read or watch/re-watch some time soon:
- Lexically Scoped's A slice of React, Clojurescript and Om blog post.
- David Nolen's The Future of JavaScript MVC Frameworks blog post.
- Anna Pawlicka's EuroClojure 2014 talk:
- Slides at Reactive Data Visualisations with Om slides.
- Code at Reactive Data Visualisations with Om code.