1. Prologue

1.1. About This Book

This is a stand-alone book for Fulcro developers that can be used by beginners and experienced developers and covers most of the library in detail. Fulcro has a pretty extensive set of resources on the web tailored to fit your learning style. There is this book, YouTube videos, an interactive tutorial, and full-blown sample applications.

A lot of time and energy went into creating these libraries and materials and providing them free of charge. If you find them useful please consider contributing to the project.

This book includes quite a bit of live code. Live code demos with their source look like this:

Show/Hide Source
(ns book.example-1
  (:require [fulcro.client.primitives :as prim :refer [defsc]]
            [fulcro.client.mutations :refer [defmutation]]
    #?(:cljs [fulcro.client.dom :as dom]
       :clj
            [fulcro.client.dom-server :as dom])))

(defmutation bump-number [ignored]
  (action [{:keys [state]}]
    (swap! state update :ui/number inc)))

(defsc Root [this {:keys [ui/number]}]
  {:query         [:ui/number]
   :initial-state {:ui/number 0}}
  (dom/div
    (dom/h4 "This is an example.")
    (dom/button {:onClick #(prim/transact! this `[(bump-number {})])}
      "You've clicked this button " number " times.")))

All of the full stack examples use a mock server embedded in the browser to simulate the interaction, but the source that you’ll read for the application is identical to what you’d write for a real server.

Warning
If you’re viewing this directly from the GitHub Fulcro repository then you won’t see the live code! Use http://book.fulcrologic.com instead.

The mock server has a built-in latency to simulate a moderately slow network so you can observe behaviors over time. You can control the length of this latency in milliseconds using the "Server Controls" in the upper-right corner of this document (if you’re reading the HTML version with live examples).

1.1.1. Common Prefixes and Namespaces

Many of the code examples assume you’ve required the proper namespaces in your code. This book adopts the following general set of requires and aliases:

(ns your-ns
  (:require [fulcro.client.primitives :as prim :refer [defsc defui]]
            [fulcro.client.dom :as dom]
            [fulcro.util :as util]
            [fulcro.client.util :as cutil]
            [fulcro.client.mutations :as m :refer [defmutation]]
            [fulcro.server :as server :refer [defquery-root defquery-entity]]
            [fulcro.client :as fc]])

others will be identified as they are used.

The next chapter, "Getting Started", is an exception. The code you see in that chapter is meant to be added to a template that you create and run on your own machine. A complete running version of it is available on GitHub as described at the end of the chapter.

2. Getting Started

This chapter takes you through a step-by-step guide of how to go from nothing to a full-stack basic application. Concepts are introduced as we go, and given a very cursory definition in the interest of concision. Once you’ve got the general idea you can use other materials to refine your understanding.

The Leiningen template is the very quickest way to get started. It gives you a number of useful things like devcards, production builds, CI integration and more while also giving you the minimal amount of actual code. This can save you hours of setup.

Important
This document assumes you’re working with Fulcro 2.5 and above. The differences are minor, but the DOM factories changed to be more succinct.

2.1. Project setup

You can get a basic app with no prewritten demo code using:

$ lein new fulcro app nodemo

The nodemo option tells the template not to include demonstration full-stack code. It gives you a shell of a project that still contains everything you’d want to set up (cards, testing, a development web server, figwheel, etc.) without much actual code to understand or delete.

You should stop here for a moment and read the README in your generated project to see how that is laid out.

2.1.1. Choosing a React Version

Fulcro supports React 15 and 16 (and may even be fine with 17 with it is released). You can exclude the versions of React and React DOM from Fulcro’s dependency list (using Leiningen exclusions, for example( and include your preferred version.

2.1.2. Figwheel Startup Script

Figwheel is a hot-reload development tool. We recommend using figwheel sidecar so you can easily start the project from the command line or use it from the REPL support built into IntelliJ. The template already has the code for doing this. Part of it is in user.clj, and the other part is a simple script script/figwheel.clj to invoke it:

(require '[user :refer [start-figwheel]])

(start-figwheel)

The README of your project describes how to start the various builds of your cljs code.

2.2. The Code Client Files

A complete Fulcro front-end client can be created in about two lines of code. Hot-load concerns require just a few more lines.

Your project should have a src/main/app/client.cljs file that looks something like this:

(ns app.client
  (:require [fulcro.client :as fc]))

(defonce app (atom (fc/new-fulcro-client)))

This creates a client and stores it in an atom. The client isn’t active until you mount it. In order to do that, you need a UI. The file src/main/ui/root.cljc contains something like this:

(ns app.ui.root
  (:require
    translations.es
    [fulcro.client.dom :as dom]
    [fulcro.client.primitives :as prim :refer [defsc]]))

(defsc Root [this {:keys [ui/react-key]}]
  (dom/div "TODO"))

The actual mount is done by code that figwheel is configured to load/run in src/dev/user.cljs:

(ns cljs.user
  (:require
    [fulcro.client :as fc]
    [app.client :as core]
    [app.ui.root :as root]
    [cljs.pprint :refer [pprint]]
    [fulcro.logging :as log]))

(enable-console-print!)

(log/set-level! :all)

(defn mount []
  (reset! core/app (fc/mount @core/app root/Root "app")))

(mount)

If you look at your project.clj file you’ll see it is configured to re-call mount on every hot load. Mounting an already mounted app is the same as asking for a forced UI refresh.

This is all the real code you need to get started with a hot-code reload capable application! However, the browser needs instructions to load this stuff up, and the target div of the mount needs to exist.

2.2.1. HTML

The most basic HTML file you can start with (and it won’t get much bigger) is:

<!DOCTYPE html>
<html>
    <body>
        <div id="app"></div>
        <script src="js/app.js" type="text/javascript"></script>
    </body>
</html>

Save this in resources/public/index.html.

2.2.2. Running Figwheel

You can now run this project in various ways.

From the command line:

$ lein run -m clojure.main script/figwheel.clj

Within IntelliJ:

  • Run → Edit Configurations…​

  • Press the '+' button, and choose Clojure REPL → Local

    • Give it a name (like dev)

    • Choose "Use clojure.main in normal JVM process" (important: it defaults to nREPL which won’t work right)

    • In JVM Args specify -Ddev. This is a trick of the template’s figwheel script that lets you pick one or more build from your build config easily. This selects just the dev build.

    • In Parameters add script/figwheel.clj

Now you should be able to start it from the Run menu.

For Emacs + Cider:

  • Make sure a piggieback dev-time dependency and repl-option are in project.clj:

  :profiles {:dev {:source-paths ["src/dev" "src/main"]
                   :repl-options {:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}
                   :dependencies [[binaryage/devtools "0.9.9"]
                                  [com.cemerick/piggieback "0.2.1"]
                                  [org.clojure/tools.namespace "0.3.0-alpha4"]
                                  [figwheel-sidecar "0.5.15"]
                                  [org.clojure/tools.nrepl "0.2.13"]]}})
  • With src/dev/user.clj open in a buffer, choose M-x cider-jack-in. In the clojure repl, run (start-figwheel), which will launch a cljs repl.

You should see the application printing "Hello World" at: http://localhost:3449

Now that you have a basic project working, let’s understand how to add some content!

Important
When developing it is a good idea to: Use Chrome (the devtools only work there), have the developer’s console open, and in the developer console settings: "Network, Disable cache (while DevTools is open)", and "Console, Enable custom formatters".

Cached files can, as everywhere else, cause you lots of headaches. Fortunately they only really affect you poorly on the initial load in Fulcro. Hot reloads typically work very well.

2.2.3. Fixing Things

One of the most maddening things that can happen during development is mystery around build errors. Nothing is more frustrating than not understanding what is wrong.

As you work on your code your compiler errors and warnings will show in the browser. DO NOT RELOAD THE PAGE! If you reload the page you’ll lose the warning or error, and that makes it harder to figure out what is wrong!

Instead, edit your code and re-save.

If you are having problems and you’ve lost your way, it is sometimes useful to ask figwheel to clean and recompile everything:

cljs.user=> (reset-autobuild)

will typically get you back on track.

Fixing Stubborn Things

Sometimes stuff just fails for reasons we fail to understand. There are times when you may want to completely kill your REPL, clean the project with lein clean, and start again. Make sure all of the generated Javascript is removed when you clean, or things might not clear up.

It is also true that problems in your project configuration may cause problems that are very difficult to understand. If this happens to you (especially if you’ve never run a project with the current project setup) then it is good to look at things like dependency problems with lein deps :tree and fix those.

In general, if you see a conflict on versions it will work to place the newest version of the conflicted dependency into your own dependency list. This can cause problems as well, but is less likely to fail than using an older version of a library that doesn’t have some needed feature of bug fix.

2.3. Basic UI Components

Fulcro supplies defsc to build React components. This macro emits React components that work as 100% raw React components (i.e. once you compile them to Javascript they could be used from other native React code).

There are also factory functions for generating all standard HTML5 DOM elements in React in the fulcro.client.dom namespace.

2.3.1. The defsc Macro

The basic code to build a simple component has the following form:

(defsc ComponentName
  [this props] ; parameters. Available in body, and in *some* of the options
  ; optional:  { ...options... }
  (dom/div #js {:className "a"}
    (dom/p nil "Hello")))
Note
As of Fulcro 2.5 properties no longer need #js, are optional, and classname keywords exist as a shortcut, so the body of that example could be written (dom/div :.a (dom/p "Hello")) instead.

For our purposes we won’t be saying much about the React lifecycle methods, though they can be added. The basic intention of this macro’s syntax is to declare a component that can render UI and participate in our data-driven story.

This macro emits the equivalent of a React component with a render method.

2.3.2. The render method.

The body of defsc is the render for the component and can do whatever work you need, but it should return a react element (see React Components, Elements, and Instances).

Luckily, there are factory methods for all of HTML5 in fulcro.client.dom. These functions generally take a Javascript map as their first argument (for things like classname and event handlers) and any children. There are two ways to generate the Javascript map: with the reader tag #js or with clj→js.

All versions of Fulcro:

(dom/div #js {:className "a" :id "thing"} "Hi")
(dom/div (clj->js {:className "a" :id "thing"}) "Hi")

Version 2.5 no longer requires the #js, the properties are optional, and they support an optional shorthand keyword for adding CSS class and DOM ids:

Fulcro 2.5+:

(dom/div :.a#thing "Hi") ; keyword can contain any number of classes preceeded by dots, and an id with #
(dom/div :.a#thing {:data-prop 3} "Hi") ; props can still be supplied with the keyword
(dom/div :.a.b.c "Hi") ; Any number of static classes (a b, and c).
(dom/div {:className "a" :data-prop 3} "Hi") ; or it can all be done in props
(dom/div {:classes [(when hidden "hidden") (if tall :.tall :.short)]} ...) ; :classes is nice for expressions (2.5.12+)

The 2.5 versions are macros that obtain the same runtime speed as the older versions in the most cases. Note that supplying props as nil (instead of omitting them) will result in a very slight performance improvement if the element has children.

Important
If you’re writing your UI in CLJC files in 2.5, then you need to make sure you use a conditional reader to pull in the proper server DOM functions for Clojure:
(ns app.ui
  (:require #?(:clj [fulcro.client.dom-server :as dom] :cljs [fulcro.client.dom :as dom]))

... same as before

The reason this is necessary is that CLJS requires macros to be in CLJ files, but in order to get higher-order operation in CLJ the DOM elements must be functions. In CLJS, you can have both a macro and function with the same name, but this is not true in CLJ. Therefore, in order to get the optimal (inlined) client performance two namespaces are required.

2.3.3. Props

React components receive their data through props and state (which is local mutable state on the component). In Fulcro we highly recommend using props for most things. This ensures that various other features work well. The data passed to a component can be accessed (as a cljs map) by calling prim/props on this, or by destructuring in the second argument of defsc.

So, let’s define a Person component to display details about a person. We’ll assume that we’re going to pass in name and age as properties:

(defsc Person [this {:keys [person/name person/age]}]
  (dom/div
    (dom/p "Name: " name)
    (dom/p "Age: " age)))

Now, in order to use this component we need an element factory. An element factory lets us use the component within our React UI tree. Name confusion can become an issue (Person the component vs. person the factory?) we recommend prefixing the factory with ui-:

(def ui-person (prim/factory Person))

Now we can compose people into our root:

(defsc Root [this props]
  (dom/div
    (ui-person {:person/name "Joe" :person/age 22})))

2.3.4. Hot Code Reload

Part of our quick development story is getting hot code reload to update the UI whenever we change the source. Try editing the UI of Person and save. You should see the UI update even though the person’s data didn’t change.

2.3.5. Composing

You should already be getting the picture that your UI is going to be a tree composed from a root element. The method of data passing (via props) should also be giving you the picture that supplying data to your UI (through root) means you need to supply an equivalently structured tree of data. This is true of basic React. However, just to drive the point home let’s make a slightly more complex UI and see it in detail:

Replace your content with this:

(defsc Person [this {:keys [person/name person/age]}]
  (dom/li
    (dom/h5 (str name " (age: " age ")"))))

(def ui-person (prim/factory Person {:keyfn :person/name}))

(defsc PersonList [this {:keys [person-list/label person-list/people]}]
  (dom/div
    (dom/h4 label)
    (dom/ul
      (map ui-person people))))

(def ui-person-list (prim/factory PersonList))

(defsc Root [this {:keys [ui/react-key]}]
  (let [ui-data {:friends {:person-list/label "Friends" :person-list/people
                                              [{:person/name "Sally" :person/age 32}
                                               {:person/name "Joe" :person/age 22}]}
                 :enemies {:person-list/label "Enemies" :person-list/people
                                              [{:person/name "Fred" :person/age 11}
                                               {:person/name "Bobby" :person/age 55}]}}]
    (dom/div
      (ui-person-list (:friends ui-data))
      (ui-person-list (:enemies ui-data)))))

So that the UI graph looks like this:

ui graph

and the data graph matches the same structure, with map keys acting as the graph "edges":

{ :friends           { :person-list/people [PERSON ...]
;  ==to-one list=>      ==to-many people==>
  :enemies           { :person-list/people [PERSON ...] }
data tree

2.4. Feeding the Data Tree

Obviously it isn’t going to be desirable to hand-manage this very well for anything but the most trivial application (which is the crux of the problems with most UI libraries).

At best it does give us a persistent data structure that represents the current "view" of the application (which has many benefits), but at worst it requires us to "think globally" about our application. We want local reasoning. We also want to be able to easily re-compose our UI as needed, and a static data graph like this would have to be updated every time we made a change! Almost equally as bad: if two different parts of our UI want to show the same data then we’d have to find and update a bunch of copies spread all over the data tree.

So, how do we solve this?

2.4.1. Why not have components just "grab" their data (sideband)?

This is certainly a possibility; however, it leads to other complications. What is the data model? How do you interact with remotes to fill your data needs? Fulcro has a very nice cohesive story for these questions, while other systems end up with complications like event handler middleware, coeffect accretion, and signal graphs…​not to mention that the sideband solution says nothing definitive about how you actually accomplish the server interactions with said data model.

Fulcro has a model for all of this, and it is surprising how simple it makes your application once you put your appliation together. Let’s look at the steps and parts:

2.4.2. Step 1 — The Initial State

All applications have some starting initial state. Since our UI is a tree, our starting state needs to somehow establish what goes to the initial nodes.

In Fulcro, there is a way to construct the initial tree of data in a way that allows for local reasoning and easy refactoring: co-locate the initial desired part of the tree with the component that uses it. This allows you to compose the state tree in exactly the same way as the UI tree.

The defsc macro makes short work of this with the initial-state option. Simply give it a lambda that gets parameters (optionally from the parent) and returns a map representing the state of the component. You can retrieve this data using (prim/get-initial-state Component).

It looks like this:

(ns app.ui.root
  (:require
    #?(:clj [fulcro.client.dom-server :as dom] :cljs [fulcro.client.dom :as dom])
    [fulcro.client.primitives :as prim :refer [defsc]]))

(defsc Person [this {:keys [person/name person/age]}]
  { :initial-state (fn [{:keys [name age] :as params}] {:person/name name :person/age age}) }
  (dom/li
    (dom/h5 (str name "(age: " age ")"))))

(def ui-person (prim/factory Person {:keyfn :person/name}))

(defsc PersonList [this {:keys [person-list/label person-list/people]}]
  {:initial-state
   (fn [{:keys [label]}]
     {:person-list/label  label
      :person-list/people (if (= label "Friends")
                            [(prim/get-initial-state Person {:name "Sally" :age 32})
                             (prim/get-initial-state Person {:name "Joe" :age 22})]
                            [(prim/get-initial-state Person {:name "Fred" :age 11})
                             (prim/get-initial-state Person {:name "Bobby" :age 55})])})}
   (dom/div
     (dom/h4 label)
     (dom/ul
       (map ui-person people))))

(def ui-person-list (prim/factory PersonList))

; Root's initial state becomes the entire app's initial state!
(defsc Root [this {:keys [friends enemies]}]
  {:initial-state (fn [params] {:friends (prim/get-initial-state PersonList {:label "Friends"})
                                :enemies (prim/get-initial-state PersonList {:label "Enemies"})}) }
  (dom/div
    (ui-person-list friends)
    (ui-person-list enemies)))
Note
You must reload your browser for this to show up. Fulcro pulls this data into the database when the application first mounts, not on hot code reload (because that would change your app state, and hot code reload is more useful without state changes).

Now a lot of the specific data here is just for demonstration purposes. Data like this (people) would almost certainly come from a server, but it serves to illustrate that we can localize the initial data needs of a component to the component, and then compose that into the parent in an abstract way (by calling get-initial-state against that child).

There are several benefits of this so far:

  1. It generates the exact tree of data needed to feed the initial UI.

  2. That initial state becomes your initial application database.

  3. It restores local reasoning (and easy refactoring). Moving a component just means local reasoning about the component being moved and the component it is being moved from/to: You remove the get-initial-state from one parent and add it to a different one.

You can see that there is no magic if you just pull the initial tree at the REPL:

dev:cljs.user=> (fulcro.client.primitives/get-initial-state app.ui.root/Root {})
{:friends
 {:person-list/label "Friends",
  :person-list/people
  [{:person/name "Sally", :person/age 32}
   {:person/name "Joe", :person/age 22}]},
 :enemies
 {:person-list/label "Enemies",
  :person-list/people
  [{:person/name "Fred", :person/age 11}
   {:person/name "Bobby", :person/age 55}]}}

It’s nothing more than function composition. The initial state option on defsc encodes your initial state into a function that can be accessed via get-initial-state on a class.

So behind the scenes Fulcro detects the initial state on the first mount and automatically uses it to initialize your application state.

By default, the entire initial state database is passed into your root node on render, so it is available for destructuring in Root’s props.

If you even want to see your current application state, you can do so through the atom that is holding your mounted application:

dev:cljs.user=> @(fulcro.client.primitives/app-state (get @app.client/app :reconciler))

Let’s see how we program our UI to access the data in the application state!

2.4.3. Step 2 — Establishing a Query

Fulcro unifies the data access story using a co-located query on each component. This sets up data access for both the client and server, and also continues our story of local reasoning and composition.

Queries go on a component in the same way as initial state: as static implementations of a protocol.

The query notation is relatively light, and we’ll just concentrate on two bits of query syntax: props and joins.

Queries form a tree just like the UI and data. Obtaining a value at the current node in the tree traversal is done using the keyword for that value. Walking down the graph (a join) is represented as a map with a single entry whose key is the keyword for that nested bit of state.

So, a data tree like this:

{:friends
 {:person-list/label "Friends",
  :person-list/people
  [{:person/name "Sally", :person/age 32}
   {:person/name "Joe", :person/age 22}]},
 :enemies
 {:person-list/label "Enemies",
  :person-list/people
  [{:person/name "Fred", :person/age 11}
   {:person/name "Bobby", :person/age 55}]}}

would have a query that looks like this:

[{:friends  ; JOIN
    [ :person-list/label
      {:person-list/people ; JOIN
         [:person/name :person/age]}]}]

This query reads "At the root you’ll find :friends, which joins to a nested entity that has a label and people, which in turn has nested properties name and age.

  • A vector always means "get this stuff at the current node"

  • :friends is a key in a map, so at the root of the application state the query engine would expect to find that key, and would expect the value to be nested state (because maps mean joins on the tree)

  • The value in the :friends join must be a vector, because we have to indicate what we want out of the nested data.

Joins are automatically to-one if the data found in the state is a map, and to-many if the data found is a vector. In the example above the :friends field from root pointed to a single PersonList, whereas the PersonList field :person-list/people pointed to a vector of Person. Be care that you don’t confuse yourself with naming (e.g. friends is plural, but points to a single list).

The namespacing of keywords in your data (and therefore your query) is highly encouraged, as it makes it clear to the reader what kind of entity you’re working against (it also ensures that over-rendering doesn’t happen on refreshes later).

You can try this query stuff out in your REPL. Let’s say you just want the friends list label. The function db→tree can take an application database (which we can generate from initial state) and run a query against it:

dev:cljs.user=> (fulcro.client.primitives/db->tree [{:friends [:person-list/label]}] (fulcro.client.primitives/get-initial-state app.ui.root/Root {}) {})
{:friends {:person-list/label "Friends"}}

HINT: The mirror of initial state with query is a great way to error-check your work (and defsc does some of that for you): For each scalar property in initial state, there should be an identical simple property in your query. For each join of initial state to a child via get-initial-state there should be a query join via get-query to that same child.

Adding Queries to Our Example

We want our queries to have the same nice local-reasoning as our initial data tree. The get-query function works just like the get-initial-state function, and can pull the query from a component. In this case, you should not ever call query directly. The get-query function augments the subqueries with metadata that is important at a later stage.

So, the Person component queries for just the properties it needs:

(defsc Person [this {:keys [person/name person/age]}]
  {:query         [:person/name :person/age]
   :initial-state (fn [{:keys [name age] :as params}] {:person/name name :person/age age})}
  (dom/li
    (dom/h5 (str name "(age: " age ")"))))

Notice that the entire rest of the component did not change.

Next up the chain, we compose the Person query into PersonList (notice how the composition of state and query are mirrored):

(defsc PersonList [this {:keys [person-list/label person-list/people]}]
  {:query [:person-list/label {:person-list/people (prim/get-query Person)}]
   :initial-state
          (fn [{:keys [label]}]
            {:person-list/label  label
             :person-list/people (if (= label "Friends")
                                   [(prim/get-initial-state Person {:name "Sally" :age 32})
                                    (prim/get-initial-state Person {:name "Joe" :age 22})]
                                   [(prim/get-initial-state Person {:name "Fred" :age 11})
                                    (prim/get-initial-state Person {:name "Bobby" :age 55})])})}
  (dom/div
    (dom/h4 label)
    (dom/ul
      (map ui-person people))))

again, nothing else changes.

2.4.4. Step 3 — Receive the Data Feed as Props in Root

Finally, we compose to Root:

(defsc Root [this {:keys [friends enemies]}]
  {:query         [{:friends (prim/get-query PersonList)}
                   {:enemies (prim/get-query PersonList)}]
   :initial-state (fn [params] {:friends (prim/get-initial-state PersonList {:label "Friends"})
                                :enemies (prim/get-initial-state PersonList {:label "Enemies"})})}
  (dom/div
    (ui-person-list friends)
    (ui-person-list enemies)))

This all looks like a minor (and useless) change. The operation is the same; however, we’re getting close to the magic, so stick with us. The major difference in this code is that even though the database starts out with the initial state, there is nothing to say we have to query for everything that is in there, or that the state has to start out with everything we might query for in the future. We’re getting close to having a dynamic data-driven application.

Notice that everything we’ve done so far has global client database implications, but that each component codes only the portion it is concerned with. Local reasoning is maintained. All software evolution in this model preserves this critical aspect.

Also, you now have application state that can evolve (the query is running against the active application database stored in an atom)!

Important
You should always think of the query as "running from root". You’ll notice that Root still expects to receive the entire data tree for the UI (even though it doesn’t have to know much about what is in it, other than the names of direct children), and it still picks out those sub-trees of data and passes them on. In this way an arbitrary component in the UI tree is not querying for it’s data directly in a side-band sort of way, but is instead being composed in from parent to parent all the way to the root. Later, we’ll learn how Fulcro can optimize this and pull the data from the database for a specific component, but the reasoning will remain the same.

2.5. Passing Callbacks and Other Parent-computed Data

The queries on component describe what data the component wants from the database; however, you’re not allowed to put code in the database, and sometimes a parent might compute something it needs to pass to a child like a callback function.

It turns out that we can optimize away the refresh of components (if their data has not changed). This means that we can use a component’s query to directly re-supply data for refresh; however, since doing so skips the rendering of the parent, if we are not careful this can lead to "losing" these extra bits of computationally generated data passed from the parent, like callbacks.

Let’s say we want to render a delete button on our individual people in our UI. This button will mean "remove the person from this list"…​but the person itself has no idea which list it is in. Thus, the parent will need to pass in a function that the child can call to affect the delete properly:

2.5.1. The Incorrect Way:

(defsc Person [this {:keys [person/name person/age onDelete]}] ; (3)
  {:query         (fn [] [:person/name :person/age])
   :initial-state (fn [{:keys [name age] :as params}] {:person/name name :person/age age})}
  (dom/li
    (dom/h5 (str name " (age: " age ")") (dom/button {:onClick #(onDelete name)} "X")))) ; (4)

(def ui-person (prim/factory Person {:keyfn :person/name}))

(defsc PersonList [this {:keys [person-list/label person-list/people]}]
  {:query [:person-list/label {:person-list/people (prim/get-query Person)}]
   :initial-state
          (fn [{:keys [label]}]
            {:person-list/label  label
             :person-list/people (if (= label "Friends")
                                   [(prim/get-initial-state Person {:name "Sally" :age 32})
                                    (prim/get-initial-state Person {:name "Joe" :age 22})]
                                   [(prim/get-initial-state Person {:name "Fred" :age 11})
                                    (prim/get-initial-state Person {:name "Bobby" :age 55})])})}
  (let [delete-person (fn [name] (println label "asked to delete" name))]  ; (1)
    (dom/div
      (dom/h4 label)
      (dom/ul
        (map (fn [p] (ui-person (assoc p :onDelete delete-person))) people))))) (2)
  1. A function acting in as a stand-in for our real delete

  2. Adding the callback into the props (WRONG)

  3. Pulling the onDelete from the passed props (WRONG). The query has to be changed to a lambda to turn off error checking to even try this method.

  4. Invoking the callback when delete is pressed.

This method of passing a callback will work initially, but not consistently. The problem is that we can optimize away a re-render of a parent when it can figure out how to pull just the data of the child on a refresh, and in that case the callback will get lost because only the database data will get supplied to the child! Your delete button will work on the initial render (from root), but may stop working at a later time after a UI refresh.

2.5.2. The Correct Way:

There is a special helper function that can record the computed data like callbacks onto the child that receives them such that an optimized refresh will still know them. There is also an additional (optional) component parameter to defsc that you can use to deconstruct them:

(defsc Person [this {:keys [person/name person/age]} {:keys [onDelete]}]
  {:query         [:person/name :person/age]
   :initial-state (fn [{:keys [name age] :as params}] {:person/name name :person/age age})}
  (dom/li
    (dom/h5 (str name " (age: " age ")") (dom/button {:onClick #(onDelete name)} "X")))) ; (4)

(def ui-person (prim/factory Person {:keyfn :person/name}))

(defsc PersonList [this {:keys [person-list/label person-list/people]}] ; (2)
  {:query [:person-list/label {:person-list/people (prim/get-query Person)}]
   :initial-state
          (fn [{:keys [label]}]
            {:person-list/label  label
             :person-list/people (if (= label "Friends")
                                   [(prim/get-initial-state Person {:name "Sally" :age 32})
                                    (prim/get-initial-state Person {:name "Joe" :age 22})]
                                   [(prim/get-initial-state Person {:name "Fred" :age 11})
                                    (prim/get-initial-state Person {:name "Bobby" :age 55})])})}
  (let [delete-person (fn [name] (println label "asked to delete" name))] ; (1)
    (dom/div
      (dom/h4 label)
      (dom/ul
        (map (fn [p] (ui-person (prim/computed p {:onDelete delete-person}))) people))))) ; (1)
  1. The prim/computed function is used to add the computed data to the props being passed.

  2. The child adds an additional parameter, and pulls the computed data from there. You can also use (prim/get-computed this) to pull all of the computed props in the body.

Now you can be sure that your callbacks (or other parent-computed data) won’t be lost to render optimizations.

2.6. Updating the Data Tree

Now the real fun begins: Making things dynamic.

In general you don’t have to think about how the UI updates, because most changes are run within the context that needs refreshed. But for general knowledge UI Refresh is triggered in two ways:

  • Running a data modification transaction on a component (which will re-render the subtree of that component), and refresh only the DOM for those bits that had actual changes.

  • Telling Fulcro that some specific data changed (e.g. :person/name).

The former is most common, but the latter is often needed when a change executed in one part of the application modifies data that some UI component elsewhere in the tree needs to respond to.

So, if we run the code that affects changes from the component that will need to refresh (a very common case) we’re covered. If a child needs to make a change that will affect a parent (as in our earlier example), then the modification should run from the parent via a callback so that refresh will not require further interaction. Later we’ll show you how to deal with refreshes that could be in far-flung parts of the UI. First, let’s get some data changing.

2.6.1. Transactions

Every change to the application database must go through a transaction processing system. This has two goals:

  • Abstract the operation (like a function)

  • Treat the operation like data (which allows us to generalize it to remote interactions)

The operations are written as quoted data structures. Specifically as a vector of mutation invocations. The entire transaction is just data. It is not something run in the UI, but instead passed into the underlying system for processing.

You essentially just "make up" names for the operations you’d like to do to your database, just like function names. Namespacing is encouraged, and of course syntax quoting honors namespace aliases.

(prim/transact! this `[(ops/delete-person {:list-name "Friends" :person "Fred"})])

is asking the underlying system to run the mutation ops/delete-person (where ops can be an alias established in the ns). Of course, you’ll typically use unquote to embed data from local variables:

(prim/transact! this `[(ops/delete-person {:list-name ~name :person ~person})])

2.6.2. Handling Mutations

When a transaction runs in Fulcro it passes things off to a multimethod. The multi-method is described in more detail in the section on the mutation multimethod, but Fulcro provides a macro that makes building (and using) mutations easier: defmutation.

The template application comes with a pre-built namespace for these src/main/app/api/mutations.cljs, but you can put them anywhere as long as the namespace in question is required by your application at runtime. Note there is also a mutations.clj, which is for the server-side handling of these same mutations.

A mutation looks a bit like a method. It can have a docstring, and the argument list will always receive a single argument (params) that will be a map (which then allows destructuring).

The body looks a bit like a letfn, but the names we use for these methods are pre-established. The one we’re interested in at the moment is action, which is what to do locally. The action method will be passed the application database’s app-state atom, and it should change the data in that atom to reflect the new "state of the world" indicated by the mutation.

For example, delete-person must find the list of people on the list in question, and filter out the one that we’re deleting:

(ns app.api.mutations
  (:require [fulcro.client.mutations :as m :refer [defmutation]]))

(defmutation delete-person
  "Mutation: Delete the person with name from the list with list-name"
  [{:keys [list-name name]}] ; (1)
  (action [{:keys [state]}] ; (2)
    (let [path     (if (= "Friends" list-name)
                     [:friends :person-list/people]
                     [:enemies :person-list/people])
          old-list (get-in @state path)
          new-list (vec (filter #(not= (:person/name %) name) old-list))]
      (swap! state assoc-in path new-list))))
  1. The argument list for the mutation itself

  2. The thing to do, which receives the app-state atom as an argument.

Then all that remains is to change basic-ui in the following ways:

  1. Add a require and alias for app.operations to the ns

  2. Change the callback to run the transaction

(ns app.basic-ui
  (:require [fulcro.client :as fc]
            [fulcro.client.dom :as dom]
            ; ADD THIS:
            [app.api.mutations :as api] ; (1)
            [fulcro.client.primitives :as prim :refer [defui defsc]]))

...

(defsc PersonList [this {:keys [person-list/label person-list/people]}]
  ...
  (let [delete-person (fn [name] (prim/transact! this `[(api/delete-person {:list-name ~label :name ~name})]))] ; (2)
  ...
  1. The require ensures that the mutations are loaded, and also gives us an alias to the namespace of the mutation’s symbol.

  2. Running the transaction in the callback.

Note that our mutation’s symbol is actually app.api.mutations/delete-person, but the syntax quoting will fix it. Also realize that the mutation is not running in the UI, it is instead being handled "behind the scenes". This allows a snapshot of the state history to be kept, and also a more seamless integration to full-stack operation over a network to a server (in fact, the UI code here is already full-stack capable without any changes!).

This is where the power starts to show: all of the minutiae above is leading us to some grand unifications when it comes to writing full-stack applications.

2.6.3. Hold on – This Sucks!

But first, we should address a problem that many of you may have already noticed: The mutation code is tied to the shape of the UI tree!!!

This breaks our lovely model in several ways:

  1. We can’t refactor our UI without also rewriting the mutations (since the data tree would change shape)

  2. We can’t locally reason about any data. Our mutations have to understand things globally!

  3. Our mutations could get rather large and ugly as our UI gets big

  4. If a fact appears in more than one place in the UI and data tree, then we’ll have to update all of them in order for things to be correct. Data duplication is never your friend.

2.7. The Secret Sauce – Normalizing the Database

Fortunately, we have a very good solution to the mutation problem above, and it is one that has been around for decades: database normalization!

Here’s what we’re going to do:

Each UI component represents some conceptual entity with data (assuming it has state and a query). In a fully normalized database, each such concept would have its own table, and related things would refer to it through some kind of foreign key. In SQL land this looks like:

sql norm

In a graph database (like Datomic) a reference can have a to-many arity, so the direction can be more natural:

datomic norm

Since we’re storing things in a map, we can represent "tables" as an entry in the map where the key is the table name, and the value is a map from ID to entity value. So, the last diagram could be represented as:

{ :PersonList { 1  { :label "Friends"
                     :people #{1, 2} }}
  :Person { 1 {:id 1 :name "Joe" }
            2 {:id 2 :name "Sally"}}}

This is close, but not quite good enough. The set in :person-list/people is a problem. There is no schema, so there is no way to know what kind of thing "1" and "2" are!

The solution is rather easy: code the foreign reference to include the name of the table (is a single such "pointer", and to-many relations store many such "pointers" in a vector (so you end up with a doubly-nested vector)):

{ :PersonList { 1  { :label "Friends"
                     :people [ [:Person 1] [:Person 2] ] }}
  :Person { 1 {:id 1 :name "Joe" }
            2 {:id 2 :name "Sally"}}}

A foreign key as a vector pair of [TABLE ID] is known as an Ident.

So, now that we have the concept and implementation, let’s talk about conventions:

  1. Properties are usually namespaced (as shown in earlier examples)

  2. Table names are usually namespaced with the entity type, and given a name that indicates how it is indexed. For example: :person/by-id, :person-list/by-name, etc. If you use Clojure spec, you may choose to alter this a bit for convenience in namespace-aliasing keywords (e.g. ::my-db-schema/person-by-id).

2.7.1. Automatic Normalization

Fortunately, you don’t have to hand-normalize your data. The components have almost everything they need to do it for you, other than the actual value of the Ident. So, we’ll add one more option to your components (and we’ll add IDs to the data at this point, for easier implementation):

The program will now look like this:

(ns app.ui.root
  (:require
    translations.es
    [fulcro.client.dom :as dom]
    [app.api.mutations :as api]
    [fulcro.client.primitives :as prim :refer [defsc]]))

(defsc Person [this {:keys [db/id person/name person/age]} {:keys [onDelete]}]
  {:query         [:db/id :person/name :person/age] ; (2)
   :ident         [:person/by-id :db/id] ; (1)
   :initial-state (fn [{:keys [id name age]}] {:db/id id :person/name name :person/age age})} ; (3)
  (dom/li
    (dom/h5 (str name " (age: " age ")") (dom/button {:onClick #(onDelete id)} "X")))) ; (4)

(def ui-person (prim/factory Person {:keyfn :person/name}))

(defsc PersonList [this {:keys [db/id person-list/label person-list/people]}]
  {:query [:db/id :person-list/label {:person-list/people (prim/get-query Person)}]
   :ident [:person-list/by-id :db/id] ; (5)
   :initial-state
          (fn [{:keys [id label]}]
            {:db/id              id
             :person-list/label  label
             :person-list/people (if (= label "Friends")
                                   [(prim/get-initial-state Person {:id 1 :name "Sally" :age 32})
                                    (prim/get-initial-state Person {:id 2 :name "Joe" :age 22})]
                                   [(prim/get-initial-state Person {:id 3 :name "Fred" :age 11})
                                    (prim/get-initial-state Person {:id 4 :name "Bobby" :age 55})])})}
  (let [delete-person (fn [person-id] (prim/transact! this `[(api/delete-person {:list-id ~id :person-id ~person-id})]))] ; (4)
    (dom/div
      (dom/h4 label)
      (dom/ul
        (map (fn [p] (ui-person (prim/computed p {:onDelete delete-person}))) people)))))

(def ui-person-list (prim/factory PersonList))

(defsc Root [this {:keys [ui/react-key friends enemies]}]
  {:query         [:ui/react-key {:friends (prim/get-query PersonList)}
                   {:enemies (prim/get-query PersonList)}]
   :initial-state (fn [params] {:friends (prim/get-initial-state PersonList {:id :friends :label "Friends"})
                                :enemies (prim/get-initial-state PersonList {:id :enemies :label "Enemies"})})}
  (dom/div
    (ui-person-list friends)
    (ui-person-list enemies)))
  1. Adding an ident allows Fulcro to know how to build a FK reference to a person (given its props). The first element is the table name, the second is the name of the property that contains the ID of the entity.

  2. We will be using IDs now, so we need to add :db/id to the query (and props destructuring). This is just a convention for the ID attribute

  3. The state of the entity will also need the ID

  4. The callback can now delete people by their ID, which is more reliable.

  5. The list will have an ID, and an Ident as well

If you reload the web page (needed to reinitialize the database state), then you can look at the newly normalized database at the REPL:

dev:cljs.user=> @(fulcro.client.primitives/app-state (-> app.client/app deref :reconciler))
{:friends [:person-list/by-id :friends],
 :enemies [:person-list/by-id :enemies],
 :person/by-id
 {1 {:db/id 1, :person/name "Sally", :person/age 32},
  2 {:db/id 2, :person/name "Joe", :person/age 22},
  3 {:db/id 3, :person/name "Fred", :person/age 11},
  4 {:db/id 4, :person/name "Bobby", :person/age 55}},
 :person-list/by-id
 {:friends
  {:db/id :friends,
   :person-list/label "Friends",
   :person-list/people [[:person/by-id 1] [:person/by-id 2]]},
  :enemies
  {:db/id :enemies,
   :person-list/label "Enemies",
   :person-list/people [[:person/by-id 3] [:person/by-id 4]]}}}

Note that db→tree understands this normalized form, and can convert it (via a query) to the proper data tree. db→tree (for legacy reasons) requires a way to resolve references (idents) and the database. In Fulcro these are the same. So, try this at the REPL:

dev:cljs.user=> (def current-db @(fulcro.client.primitives/app-state (-> app.client/app deref :reconciler)))
dev:cljs.user=> (def root-query (fulcro.client.primitives/get-query app.ui.root/Root))
#'cljs.user/current-db
dev:cljs.user=> (fulcro.client.primitives/db->tree root-query current-db current-db)
{:friends
 {:db/id :friends,
  :person-list/label "Friends",
  :person-list/people
  [{:db/id 1, :person/name "Sally", :person/age 32}
   {:db/id 2, :person/name "Joe", :person/age 22}]},
 :enemies
 {:db/id :enemies,
  :person-list/label "Enemies",
  :person-list/people
  [{:db/id 3, :person/name "Fred", :person/age 11}
   {:db/id 4, :person/name "Bobby", :person/age 55}]}}

2.7.2. Mutations on a Normalized Database

We have now made it possible to fix the problems with our mutation. Now, instead of removing a person from a tree, we can remove a FK from a TABLE entry!

This is not only much easier to code, but it is completely independent of the shape of the UI tree:

(ns app.api.mutations
  (:require [fulcro.client.mutations :as m :refer [defmutation]]))

(defmutation delete-person
  "Mutation: Delete the person with name from the list with list-name"
  [{:keys [list-id person-id]}]
  (action [{:keys [state]}]
    (let [ident-to-remove [:person/by-id person-id] ; (1)
          strip-fk (fn [old-fks]
                     (vec (filter #(not= ident-to-remove %) old-fks)))] ; (2)
      (swap! state update-in [:person-list/by-id list-id :person-list/people] strip-fk)))) ; (3)
  1. References are always idents, meaning we know the value to remove from the FK list

  2. By defining a function that can filter the ident from (1), we can use update-in on the person list table’s people.

  3. This is a very typical operation in a mutation: swap on the application state, and update a particular thing in a table (in this case the people to-many ref in a specific person list).

If we were to now wrap the person list in any amount of additional UI (e.g. a nav bar, sub-pane, modal dialog, etc) this mutation will still work perfectly, since the list itself will only have one place it ever lives in the database.

2.7.3. How Automatic Normalization Works (optional)

It is good to know how an arbitrary tree of data (the one in InitialAppState) can be converted to the normalized form. Understanding how this is accomplished can help you avoid some mistakes later.

When you compose your query (via prim/get-query), the get-query function adds metadata to the query fragment that names which component that query fragment came from.

For example, try this at the REPL:

dev:cljs.user=> (meta (fulcro.client.primitives/get-query app.basic-ui/PersonList))
{:component app.basic-ui/PersonList}

The get-query function adds the component itself to the metadata for that query fragment. We already know that we can call the static methods on a component (in this case we’re interested in ident).

So, Fulcro includes a function called tree→db that can simultaneously walk a data tree (in this case initial-state) and a component-annotated query. When it reaches a data node whose query metadata names a component with an Ident, it places that data into the approprite table (by calling your ident function on it to obtain the table/id), and replaces the data in the tree with its FK ident.

Once you realize that the query and the ident work together to do normalization, you can more easily figure out what mistakes you might make that could cause auto-normalization to fail (e.g. stealing a query from one component and placing it on another, writing the query of a sub-component by-hand instead of pulling it with get-query, etc.).

2.8. Review So Far

  • An Initial app state sets up a tree of data for startup to match the UI tree

  • Component query and ident are used to normalize this initial data into the database

  • The query is used to pull data from the normalized db into the props of the active Root UI

  • Transactions invoke abstract mutations

    • Mutations modify the (normalized) db

    • The transaction’s subtree of components re-renders

2.9. Using Better Tools

So far we’ve been hacking things in place and using the REPL to watch what we’re doing. There are better ways to work on Fulcro applications, and now that we’ve got one basically working, let’s take a look at them both.

2.9.1. Fulcro Inspect

A relatively recent (late 2017) addition to the ecosystem is Fucro Inspect. A set of tools you can load into your environment during development. In fact, the template already has them (for the dev build)! On OSX or Linux, simply hit CTRL-F. See Fulcro Inspect’s documentation for how to set the keyboard shortcut in Windows.

The DB tab of this tool shows you your application’s database and has a time slider to see the history of states! It also has tabs for showing you transactions that have run, and network interactions. See the tool’s documentation for more information. In fact, by the time you read this it will probably have even more exciting features!

2.9.2. Dev Cards

There is a build in the template project called cards. This starts up a development environment where you can code entire applications (or portions of them) in an environment that can show you live state and is quite handy, particularly for working with small parts of your program (remember, we can actually split off chunks of the application because they are all relative to their parent).

You can start this build just as we did near the start of this guide, and load it via http://localhost:3449/cards.html.

In fact, you don’t even have to start a new REPL! You can run switch-to-build:

dev:cljs.user=> (switch-to-build "cards" "dev")
Figwheel: Watching build - cards
Figwheel: Cleaning build - cards
Compiling "resources/public/js/cards.js" from ["src/main" "src/cards"]...

Then you can embed a full-funcional Fulcro application into a card environment with very little code. Replace the content of src/cards/app/intro.cljs with:

(ns app.intro
  (:require [fulcro.client.cards :refer [defcard-fulcro]]
            [app.ui.root :as root]))

(defcard-fulcro sample-app
  root/Root
  {}
  {:inspect-data true})

save and go to http://localhost:3449/cards.html#!/app.intro. You should see your app running in a card, and you should be able to see the live database (which will change as you interact)!.

2.10. Going Remote!

OK, back to the main story!

Believe it or not, there’s not much to add/change on the client to get it talking to a server, and there is also a relatively painless way to get a server up and running.

Your template already has one :)

2.10.1. Setting up a Server

Warning
Starting in Fulcro 2.5 the prebuilt servers for Fulcro require that you add some dependencies to your project. These namespaces dynamically resolve these so that you won’t end up with extra dependencies in your product unless you need them:
[http-kit "2.2.0"]
[ring/ring-core "1.6.3"]]
[bk/ring-gzip "0.2.1"]
[bidi "2.1.3"]

You can always hand-build a server in Fulcro, but the fulcro.easy-server is a great option for getting started.

The template generates the easy one for you in src/main/app/server.clj.

Using the Easy Server

The easy server is based upon the component system. It is set up so that it can be stopped, code refreshed, and restarted very quickly. The management functions are already written in src/dev/user.clj underneath the Figwheel startup code.

The server code itself is very light:

(ns app.server
  (:require
    [fulcro.easy-server :refer [make-fulcro-server]]
    ; MUST require these, or you won't get them installed.
    [app.api.read]
    [app.api.mutations]))

(defn build-server
  [{:keys [config] :or {config "config/dev.edn"}}]
  (make-fulcro-server
    :parser-injections #{:config}
    :config-path config))

The make-fulcro-server function needs to know where to find the server config file. You can tell it a number of other things, including which components you’d like to be available when parsing the incoming client requests. In the template, the only component available is the one that reads the application config (which contains the port on which to run the web server).

The configuration is meant for production environments, and requires a default file that spells out defaults in case the main config does not have values for them, and a primary config file that can override any defaults.

Your template already has these in src/main/config (the config component looks for defaults.edn on the CLASSPATH at relative location config/):

defaults.edn:

{:port 3000}

dev.edn:

{}

The first file is always looked for by the server, and should contain all of the default settings you think you want independent of where the server is started.

The server (for safety reasons in production) will not start if there isn’t a user-specified file containing potential overrides.

Basically, it will deep-merge the two and have the latter override things in the former. This makes mistakes in production harder to make. If you read the source of the go function in the user.clj file you’ll see that we supply this development config file as an argument. In production systems you’ll typically want this file to be on the filesystem when an admin can tweak it.

Starting the Server

If you now start a local Clojure REPL (with no special options), it should start in the user namespace. You can kick off your own application’s easy web server with:

user=> (go)

The console should tell you the URL, and if you browse there you should see your index.html file.

Server Refresh

When you add/change code on the server you will want to see those changes in the live server without having to restart your REPL.

user=> (restart)

will do this.

If there are compiler errors, then the user namespace might not reload properly. In that case, you should be able to recover using:

user=> (tools-ns/refresh)
user=> (go)
Warning
Don’t call refresh while the server is running. It will refresh the code, but it will lose the reference to the running server, meaning you won’t be able to stop it and free up the network port. If you do this, you’ll have to restart your REPL.
Serving your App

Figwheel comes with a server that we’ve been using to serve our client. When you want to build a full-stack app you must serve your client from your own server. Thus, if you load your page with the figwheel server (which is still available on an alternate port) you’ll see your app, but the server interactions won’t succeed.

One might ask: "If I don’t use figwheel’s server, do I lose hot code reload on the client?"

The answer is no. When figwheel compiles your application it embeds it’s own websocket code in your application for hot code reload. When you load that compiled code (in any way) it will try to connect to the figwheel websocket.

So your network topology was:

client network topo

where both the HTML/CSS/JS resources and the hot code were coming from different connections to the same server.

The networking picture during full-stack development just splits these like this:

network topo

Fulcro’s client will automatically route requests to the /api URI of the source URL that was used to load the page, and Fulcro’s server is built to watch for communications at this endpoint.

2.10.2. Setup for Playing with Loads

It is very handy to be able to look at your application’s state to see what might be wrong. We’ve been manually dumping application state at the REPL using a rather long expression. So, at this point make sure you are either running your application in a devcard, or you know how to look at things with Fulcro Inspect. The output in the devcards is typically easier for beginners to read.

2.10.3. Loading Data

Now we will start to see more of the payoff of our UI co-located queries and auto-normalization. Our application so far is quite unrealistic: the people we’re showing should be coming from a server-side database, they should not be embedded in the code of the client. Let’s remedy that.

Fulcro provides a few mechanisms for loading data, but every possible load scenario can be done using the fulcro.client.data-fetch/load function.

It is very important to remember that our application database is completely normalized, so anything we’d want to put in that application state will be at most 3 levels deep (the table name, the ID of the thing in the table, and the field within that thing). We’ve also seen that Fulcro can also auto-normalize complete trees of data, and has graph queries that can be used to ask for those trees.

Thus, there really are not very many scenarios!

The three basic scenarios are:

  • Load something into the root of the application state

  • Load something into a particular field of an existing thing

  • Load some pile of data, and shape it into the database (e.g. load all of the people, and then separate them into a list of friends and enemies).

Let’s try out these different scenarios with our application.

First, let’s correct our application’s initial state so that no people are there:

(defsc PersonList [this {:keys [db/id person-list/label person-list/people]}]
  {:query [:db/id :person-list/label {:person-list/people (prim/get-query Person)}]
   :ident [:person-list/by-id :db/id]
   :initial-state
          (fn [{:keys [id label]}]
            {:db/id              id
             :person-list/label  label
             :person-list/people []})} ; REMOVE THE INITIAL PEOPLE
  ...

If you now reload your page you should see two empty lists.

Normalization

When you load something you will use a query from something on your UI (it is rare to load something you don’t want to show). Since those components (should) have a query and ident, the result of a load can be sent from the server as a tree, and the client can auto-normalize that tree just like it did for our initial state!

Loading something into the DB root

This case is less common, but it is a simple starting point. It is typically used to obtain something that you’d want to access globally (e.g. the user info about the current session). Let’s assume that our Person component represents the same kind of data as the "logged in" user. Let’s write a load that can ask the server for the "current user" and store that in the root of our database under the key :current-user.

Loads, of course, can be triggered at any time (startup, event, timeout). Loading is just a function call.

For this example, let’s trigger the load just after the application has started.

Triggering the Load

To do this, we can add an option to our client. In app.client change app:

(ns app.client
  (:require [fulcro.client :as fc]
            [fulcro.client.data-fetch :as df] ; (1)
            [app.ui.root :as root]))

(defonce app (atom (fc/new-fulcro-client
                     :started-callback
                     (fn [app]  ; (2)
                       (df/load app :current-user root/Person)))))
  1. Require the data-fetch namespace

  2. Issue the load in the application’s started-callback

Note
If you are using devcards you will need to place the option for the application in the devcard’s options under the :fulcro key:
(ns app.intro
  (:require [fulcro.client.cards :refer [defcard-fulcro]]
            [app.ui.root :as root]
            [fulcro.client.data-fetch :as df]))

(defcard-fulcro sample-app
  root/Root
  {}
  {:inspect-data true
   :fulcro       {:started-callback
                  (fn [app] (df/load app :current-user root/Person))}})

Of course hot code reload does not restart the app (it just hot patches the code), so to see this load trigger we must reload the browser page.

If you do that at the moment, you should see an error in the various consoles related to the failure of the load.

Important
Make sure your application (or dev card) is running from your server (port 3000) and not the figwheel one!

Technically, load is just writing a query for you (in this case [{:current-user (prim/get-query Person)}]) and sending it to the server. The server will receive exactly that query as a CLJ data structure.

Implementing the Server Handler

You now need to converting the raw CLJ query into a response. You can read more about the gory details of that in the developer’s guide; however, Fulcro’s has some helpers that make our job much easier.

The template has a spot to put your query handlers in src/main/app/api/read.clj. Since we’re on the server and we’re going to be supplying and manipulating people, we’ll just make a single atom-based in-memory database. This could easily be stored in a database of any kind. To handle the incoming "current user" request, we can use a macro to write the handler for us. Change the file to look like this:

(ns app.api.read
  (:require
    [fulcro.server :refer [defquery-root defquery-entity defmutation]]))

(def people-db (atom {1  {:db/id 1 :person/name "Bert" :person/age 55 :person/relation :friend}
                      2  {:db/id 2 :person/name "Sally" :person/age 22 :person/relation :friend}
                      3  {:db/id 3 :person/name "Allie" :person/age 76 :person/relation :enemy}
                      4  {:db/id 4 :person/name "Zoe" :person/age 32 :person/relation :friend}
                      99 {:db/id 99 :person/name "Me" :person/role "admin"}}))

(defquery-root :current-user
  "Queries for the current user and returns it to the client"
  (value [env params]
    (get @people-db 99)))

This actually augments a multimethod, which means we need to make sure this namespace is loaded by our server. The user namespace already does this. So, you should be able to simply restart/refresh the server at the SERVER REPL:

user=> (restart)

If you’ve done everything correctly, then reloading your application should successfully load your current user. You can verify this by examining the network data, but it will be even more convincing if you look at your client database via the dev card visualization on Fulcro Inspect. It should look something like this:

{:current-user         [:person/by-id 99]
 :person/by-id         {99 {:db/id 99 :person/name "Me" :person/role "admin"}}
 ...}

Notice that the top-level key is a normalized FK reference to the person, which has been placed into the correct database table.

Using Data from Root

Of course, the question is now "how do I use that in some arbitrary component?" We won’t completely explore that right now, but the answer is easy: The query syntax has a notation for "query something at the root". It looks like this: [ {[:current-user '_] (prim/get-query Person)} ]. You should recognize this as a query join, but on something that looks like an ident without an ID (implying there is only one, at root).

We’ll just use it on the Root UI node, where we don’t need to "jump to the top":

(defsc Root [this {:keys [ui/react-key friends enemies current-user]}] ; (2)
  {:query         [:ui/react-key
                   {:current-user (prim/get-query Person)} ; (1)
                   {:friends (prim/get-query PersonList)}
                   {:enemies (prim/get-query PersonList)}]
   :initial-state (fn [params] {:friends (prim/get-initial-state PersonList {:id :friends :label "Friends"})
                                :enemies (prim/get-initial-state PersonList {:id :enemies :label "Enemies"})})}
  (dom/div
    (dom/h4 (str "Current User: " (:person/name current-user))) ; (3)
    (ui-person-list friends)
    (ui-person-list enemies)))
  1. Add the current user to the query

  2. Pull of from the props

  3. Show something about it in the UI

Loading something that gets "added in" to an existing entity

The next common scenario is loading something into some other existing entity in your database. Remember that since the database is normalized this will cover all of the other loading cases (except for the one where you want to convert what the server tells you into a different shape (e.g. paginate, sort, etc.)).

Fulcro’s load method accomplishes this by loading the data into the root of the database, normalizing it, then (optionally) allowing you to re-target the top-level FK to different location(s) in the database.

Targeting the Load

The load looks very much like what we just did, but with one addition:

(df/load app :my-friends Person {:target [:person-list/by-id :friends :person-list/people]})

The :target option indicates that once the data is loaded and normalized (which will leave the FK reference at the root as we saw in the last section) this top-level reference (or vector of references) will be moved into the key-path provided. Since our database is normalized, this means a 3-tuple (table, id, target field).

Warning
It is important to choose a keyword for this load that won’t stomp on real data in your database’s root. We already have the top-level keys :friends and :enemies as part of our UI graph from root. So, we’re making up :my-friends as the load key. One could also namespace the keyword with something like :server/friends.

Since friend and enemies are the same kind of query, let’s add both into the startup code (in the card/client):

...
     :started-callback
     (fn [app]
       (df/load app :current-user root/Person)
       (df/load app :my-enemies root/Person {:target [:person-list/by-id :enemies :person-list/people]})
       (df/load app :my-friends root/Person {:target [:person-list/by-id :friends :person-list/people]}))
...
Handling the Load Request on the Server

The server query processing is what you would expect from the last example (in read.clj):

(def people-db ...) ; as before

(defn get-people [kind keys]
  (->> @people-db
    vals
    (filter #(= kind (:person/relation %)))
    vec))

(defquery-root :my-friends
  "Queries for friends and returns them to the client"
  (value [{:keys [query]} params]
    (get-people :friend query)))

(defquery-root :my-enemies
  "Queries for enemies and returns them to the client"
  (value [{:keys [query]} params]
    (get-people :enemy query)))

A refresh of the server and reload of the page should now populate your lists from the server!

user=> (restart)
Morphing the Loaded Data

It is somewhat common for a server to return data that isn’t quite what we want in our UI. So far we’ve just been placing the data returned from the server directly in our UI. Fulcro’s load mechanism allows a post mutation of the loaded data once it arrives, allowing you to re-shape it into whatever form you might desire.

For example, you may want the people in your lists to be sorted by name. You’ve already seen how to write client mutations that modify the database, and that is really all you need. The client mutation for sorting the people in the friends list could be (in mutations.cljs):

(defn sort-friends-by*
  "Sort the idents in the friends person list by the indicated field. Returns the new app-state."
  [state-map field]
  (let [friend-idents  (get-in state-map [:person-list/by-id :friends :person-list/people] [])
        friends        (map (fn [friend-ident] (get-in state-map friend-ident)) friend-idents)
        sorted-friends (sort-by field friends)
        new-idents     (mapv (fn [friend] [:person/by-id (:db/id friend)]) sorted-friends)]
    (assoc-in state-map [:person-list/by-id :friends :person-list/people] new-idents)))

(defmutation sort-friends [no-params]
  (action [{:keys [state]}]
    (swap! state sort-friends-by* :person/name)))

Of course this mutation could be triggered anywhere you could run a transact!, but since we’re interested in morphing just-loaded data, we’ll add it there. Our dev card would now look like this:

(ns app.intro
  (:require [fulcro.client.cards :refer [defcard-fulcro]]
            [app.ui.root :as root]
            [fulcro.client.data-fetch :as df]
            [app.api.mutations :as api]))

(defcard-fulcro sample-app
  root/Root
  {}
  {:inspect-data true
   :fulcro       {:started-callback
                  (fn [app] (df/load app :current-user root/Person)
                    (df/load app :my-friends root/Person {:target        [:person-list/by-id :friends :person-list/people]
                                                          :post-mutation `api/sort-friends})
                    (df/load app :my-enemies root/Person {:target [:person-list/by-id :enemies :person-list/people]}))}})

Notice the syntax quoting. The post mutation has to be the symbol of the mutation. Remember that our require has app.api.mutations aliased to api, and syntax quoting will expand that for us.

If you reload your UI you should now see the people sorted by name. Hopefully you can see how easy it is to change this sort order to something like "by age". Try it!

Loading a specific entity and it’s subgraph (by ident)

Once things are loaded from the server they are immediately growing stale (unless you’re pushing updates with websockets). It is very common to want to re-load a particular thing in your database. Of course, you can trigger a load just like we’ve been doing, but in that case we reloading a whole bunch of things. What if we just wanted to refresh a particular person (e.g. in preparation for editing it).

The load function can be used for that as well. Just replace the keyword with an ident, and you’re there!

Load can take the app or any component’s this as the first argument, so from within the UI we can trigger a load using this:

(df/load this [:person/by-id 3] Person)
Trigger the Load via a User Event

Let’s embed that into our UI at the root:

(defsc Root [this {:keys [ui/react-key friends enemies current-user]}]
  {:query         [:ui/react-key
                   {:current-user (prim/get-query Person)}
                   {:friends (prim/get-query PersonList)}
                   {:enemies (prim/get-query PersonList)}]
   :initial-state (fn [params] {:friends (prim/get-initial-state PersonList {:id :friends :label "Friends"})
                                :enemies (prim/get-initial-state PersonList {:id :enemies :label "Enemies"})})}
  (dom/div
    (dom/h4 (str "Current User: " (:person/name current-user)))
    ; NEW BUTTON HERE:
    (dom/button {:onClick (fn [] (df/load this [:person/by-id 3] Person))} "Refresh Person with ID 3")
    (ui-person-list friends)
    (ui-person-list enemies)))
Handling an Entity Query on the Server

The incoming query will have a slightly different form, so there is an alternate macro for making a handler for entity loading. Let’s add this in our server’s read.clj:

(defquery-entity :person/by-id
  "Server query for allowing the client to pull an individual person from the database"
  (value [env id params]
    ; the update is just so we can see it change in the UI
    (update (get @people-db id) :person/name str " (refreshed)")))

The defquery-entity takes the "table name" as the dispatch key. The value method of the query handler will receive the server environment, the ID of the entity to load, and any parameters passed with the query (see the :params option of load).

In the implementation above we’re augmenting the person’s name with "(refreshed)" so that you can see it happen in the UI.

Remember to (restart) your server to load this code.

Your UI should now have a button, and when you press it you should see one person update!

Refreshing "This"

There is a special case that is somewhat common: you want to trigger a refresh from an event on the item that needs the refresh. The code for that is identical to what we’ve just presented (a load with an ident and component); however, the data-fetch namespace includes a convenience function for it.

So, say we wanted a refresh button on each person. We could leverage df/refresh for that:

(defsc Person [this {:keys [db/id person/name person/age]} {:keys [onDelete]}]
  {:query         [:db/id :person/name :person/age]
   :ident         [:person/by-id :db/id]
   :initial-state (fn [{:keys [id name age]}] {:db/id id :person/name name :person/age age})}
  (dom/li
    (dom/h5 (str name " (age: " age ")")
      (dom/button {:onClick #(onDelete id)} "X")
      (dom/button {:onClick #(df/refresh! this)} "Refresh")))) ; ADD THIS

This should already work with your server, so once the browser hot code reload has happened this button should just work!

Additional Permutations

Fulcro’s load system covers a number of additional bases that bring the story to completion. There are load markers (so you can show network activity), UI refresh add-ons (when you modify data that isn’t auto-detected, e.g. through a post mutation), server query parameters, and error handling. See the Developers Guide, doc strings, or source for more details.

2.10.4. Handling Mutations on The Server

Mutations are handled on the server using the server’s defmutation macro (if you’re using Fulcro’s built-in request parser).

This has the identical syntax to the client version!

Important
You want to place your mutations in the same namespace on the client and server since the defmutation macros namespace the symbol into the current namespace.

So, this is really why we have a duplicated namespace in Clojure called mutations.clj right next to our mutations.cljs.

So, let’s add an implementation for our server-side delete-person. Your mutations.clj should end up looking like this (don’t forget the require to get access to the people db):

(ns app.api.mutations
  (:require
    [taoensso.timbre :as timbre]
    [app.api.read :refer [people-db]]
    [fulcro.server :refer [defmutation]]))

;; Place your server mutations here
(defmutation delete-person
  "Server Mutation: Handles deleting a person on the server"
  [{:keys [person-id]}]
  (action [{:keys [state]}]
    (timbre/info "Server deleting person" person-id)
    (swap! people-db dissoc person-id)))

Refresh the code on your server with (restart) at the REPL. However, don’t expect it to work just yet. We have to tell the client to send the remote request.

Triggering the Remote Mutation from the Client

Mutations are simply optimistic local updates by default. To make them full-stack, you need to add a method-looking section to your defmutation handler:

(defmutation delete-person
  "Mutation: Delete the person with person-id from the list with list-id"
  [{:keys [list-id person-id]}]
  (action [{:keys [state]}]
    (let [ident-to-remove [:person/by-id person-id]
          strip-fk        (fn [old-fks]
                            (vec (filter #(not= ident-to-remove %) old-fks)))]
      (swap! state update-in [:person-list/by-id list-id :person-list/people] strip-fk)))
  (remote [env] true)) ; This one line is it!!!

The syntax for the addition is:

(remote-name [env] boolean-or-ast)

where remote is the name of a remote server (the default is remote). You can have any number of network remotes. The default one talks to the page origin at /api. What is this AST we speak of? It is the abstract syntax tree of the mutation itself (as data). Using a boolean true means "send it just as the client specified". If you wish you can pull the AST from the env, augment it (or completely change it) and return that instead. See the Developers Guide for more details.

Now that you’ve got the UI in place, try deleting a person. It should disappear from the UI as it did before; however, now if you’re watching the network you’ll see a request to the server. If you server is working right, it will handle the delete.

Try reloading your page from the server. That person should still be missing, indicating that it really was removed from the server.

Now that you’ve gotten an overview and have written some code, you can read through the remaining chapters for more detail on each topic.

3. Core Concepts

This chapter covers some detail about the core language features and theory that are important in the Fulcro ecosystem. You need not read this chapter to use Fulcro, but it will aid in your understanding of it quite a bit, especially if you’re relatively new to Clojurescript.

3.1. Immutable Data Structures

Many of the most interesting and compelling features of Fulcro are directly or indirectly enabled (or made simpler) by the use of persistent data structures that are a first-class citizen of the language.

In imperative programming languages like Java and Javascript you have no idea what a function or method might do to your program state:

Person p = new Person();

doSomethingOnAnotherThread(p);

p.fumble();

// did p just change??? Did I just cause a race condition???

This leads to all sorts of subtle bugs and is arguably the source of many of the hardest problems in keeping software sustainable today. What if Person couldn’t change and you instead had to copy instead if you wanted to modify?

Person p = new Person();

doSomethingOnAnotherThread(p);

Person q = p.fumble();

// p is definitely unchanged, but q could be different

Now you can reason about what will happen. The other thread will see p exactly as it was when you (locally) reasoned about it. Furthermore, q cannot be affected because if p is truly "read-only" then I still know what it is when I use it to derive q (the other thread can’t modify it either).

In order to derive these benefits you need to either write objects that enforce this behavior (which is highly inconvenient and hard to make efficient in imperative langauges), or use a programming language that supplies the ability to do so as a first-class feature.

Another benefit is that persistent data structures can do structural sharing. Basically the new version of a map, vector, list, or set can use references to point to any parts of the old version that are still the same in the new version. This means, for example, that adding an element to the head of a list that had 1,000,000 entries (where only one is being changed) is still a constant time operation!

Here are some of the features in Fulcro that trivially result from using persistent data structures:

  1. A Time-travel UI history viewer that consumes little space.

  2. Extremely efficient detection of data changes that affect the UI (can be ref compare instead of data compare)

  3. "Pure Rendering" is possible and convenient without having to resort to hidden variables in the UI.

3.2. Pure Rendering

Fulcro uses Facebook’s React to accomplish updates to the browser DOM. React, in concept, is really simple:

Render is a function you make that generates a data structure known as the VDOM (a lightweight virtual DOM)

  1. On The first "frame", the real DOM is made to match this data structure.

  2. On every subsequent frame, render is used to make a new VDOM. React compares the prior VDOM (which is cached) to the new one, and then applies the changes to the DOM.

The cool realization the creators of React had was that the DOM operations that are slow and heavy, but there are efficient ways to figure out what needs to be changed via the VDOM without you having to write a bunch of controller logic.

Now, because React lives in a mutable space (Javascript), it allows all sorts of things that can embed "rendering logic" within a component. This sounds like a good idea to our OOP brains, but consider this:

What if you could have a complete snapshot of the state of your application, pass that to a function, and have the screen just "look right". Like writing a 2D game: you just redraw the screen based on the new "state of the world". All of the sudden your mind shifts away from "bit twiddling" to thinking more about the representation of your model with minimal data!

That is what we mean by "pure rendering".

rendering

Here’s an example to whet your appetite: Nested check-boxes. In imperative programming each checkbox has it’s own state, and when we want a "check all" we end up writing nightmares of logic to make sure the thing works right because we’re having to store a mutable value into an object that then does the rendering. Then we play with it and find out we forgot to handle that event where some sub-box gets unchecked to fire an event to ensure to uncheck the "select all"…​oh wait, but when I do that it accidentally fires the event from "check all" which unchecks everything and then goes into an infinite loop!

What a mess! Maybe you eventually figure out something that’s tractable, but that extra bit of state in the "check all" is definitely the source of bugs.

Here’s what you do in pure rendering with immutable data:

Each sub-item checkbox is a simple data structure with a :checked? key that has a boolean value. You use that to directly tell the checkbox what it’s state should be (and React enforces that…​making it impossible for the UI to draw it any differently)

(def state {:items [{:id :a :checked? true} {:id :b :checked? false} ...]})

For a "state of the world", these are read-only. (you have to make a "new state of the world" to change one). When you render, the state of the check-all is just the conjunction of it’s children’s :checked?:

(let [all-checked (every? :checked? (get state :items)]
   (dom/input {:checked all-checked}))

The check-all button would have no application state at all, and React will force it to the correct state based on the calculated value. When the sub-items change, a new "state of the world" is generated with the altered item:

(def next-state (assoc-in state [:items 0 :checked?] false))

and the entire UI is re-rendered (React makes this fast using the VDOM diff), the "check all" checkbox will just be right!

If the "check all" button is pressed, then the logic is similarly very simple: change the state for the subitems to checked if any were unchecked, or set them all to unchecked if they were all checked:

(def next-state-2
  (let [all-checked? (every? :checked? (get state :items))
        c            (not all-checked?)
        old-items    (get state :items)
        new-items    (mapv #(assoc % :checked? c) old-items)]
    (assoc state :items new-items)))

and again you get to pretend you’re rendering an entire new frame on the screen!

You’ll be continually surprised at how simple your logic gets in the UI once you adjust to this way of thinking about the problem.

3.3. Data-Driven

Data-driven concepts were pioneered in web development by Facebook’s GraphQL and Netflix’s Falcor. The idea is quite powerful, and eliminates huge amounts of complexity in your network communication and application development.

The basic idea is this: Your UI, which might have various versions (mobile, web, tablet) all have different, but related, data needs. The prevalent way of talking to our servers is to use REST, but REST itself isn’t a very good query 'or' update language. It creates a lot of complexity that we have to deal with in order to do the simplest things. In the small, it is "easy". In the large, it isn’t the best fit.

Data-driven applications basically use a more detailed protocol that allows the client UIs to specify what they need, and also typically includes a "mutation on the wire" notation that allows the client to abstractly say what it needs the server to do.

So, instead of /person/3 you can instead say "I need person 3, but only their name, age, and billing info. But in the billing info, I only need to know their billing zip code".

Notice that this abstract expression (which of course has a syntax we’re not showing you yet) is "walking a graph". This is why Facebook calls their language "GraphQL".

You can imagine that the person and billing info might be stored in two tables of a database, with a to-one relationship, and our query is basically asking to query this little sub-graph:

graph query abstract

Modifications are done in a similar, abstract way. We model them as if they were "function calls on the wire". Like RPC/RMI:

'(change-person {:id 3 :age 44})

but instead of actually 'calling' the function, we encode this list as a data structure (it is a list containing a symbol and a map: the power of Clojure!) and then process that data locally (in the back-end of the UI) and optionally also transmit it 'as data' over the wire for server processing!

3.4. Graph Database

The client-side of Fulcro keeps all relevant data in a simple graph database, which is referenced by a single top-level atom. The database itself is a persistent map.

The database should be thought of as a root-level node (the top-level map itsef), and tables that can hold data relevant to any particular component or entity in your program (component or entity nodes).

dbmodel

The tables are also simple maps, with a naming convention and well-defined structure. The name of the table is typically namespaced with the "kind" of thing you’re storing, and has a name that indicates the way it is indexed:

{ :person/by-id { 4    { :id 4 :person/name "Joe" }}}
;   ^      ^      ^    ^
; kind   indexed  id   entity value itself

3.4.1. Idents

Items are joined together into a graph using a tuple of the table name and the key of an entity. For example, the item above is known as [:person/by-id 4]. Notice that this tuple is also exactly the vector you’d need in an operation that would pull data from that entity or modify it:

(update-in state-db [:person/by-id 4] assoc :person/age 33)
(get-in state-db [:person/by-id 4])

These tuples are known as 'idents'. Idents can be used anywhere one node in the graph needs to point to another. If the idents (which are vectors) 'appear' in a vector, then you are creating a 'to-many' relation:

{ :person/by-id
    {  1  {:id 1 :person/name "Joe"
           :person/spouse [:person/by-id 2]         ; (1)
           :person/children [ [:person/by-id 3]
                              [:person/by-id 4] ] } ; (2)
       2  { :id 2 :person/name "Julie"
            :person/spouse [:person/by-id 1]}       ; (3)
       3  { :id 3 :person/name "Billy" }
       4  { :id 4 :person/name "Heather"}}
  1. A to-one relation to Joe’s spouse (Julie)

  2. A to-many relation to Joe’s kids

  3. A to-relation back to Joe from Julie

Notice in the example above that Joe and Julie point at each other. This creates a 'loop' in the graph. This is perfectly legal. Graphs can contain loops. The table in the example contains 4 nodes.

The client database treats the 'root' node as a special set of non-table properties in the top of the database map. Thus, an entire state database with 'root node' properties might look like this:

The above data structure is now a graph database that looks like this:

dbgraph

This makes for a very compact representation of a graph with an arbitrary number of nodes and edges. All nodes but the special "root node" live in tables. The root node itself is special because it is the storage location for both root properties and for the tables themselves.

Important
Since the root node and the tables containing other nodes are merged together into the same overall map it is important that you use care when storing things so as not to accidentally collide on a name. Larger programs should namespace all keywords.

3.4.2. A Special Note about The Client-Side Database

The graph database on the client is the most central and key concept to understand in Fulcro. Remember that we are doing pure rendering. This means that the UI is simply a function transforming this graph database into the UI.

There are two primary things to write in Fulcro: the UI and the mutations. The UI pulls data from this database and displays it. The mutations evolve this database to a new version. Every interaction that changes the UI should be thought of as a data manipulation. You’re making a new state of the world that your pure renderer turns into DOM.

The graph format of the database means that your data manipulation, the main dynamic thing in the entire application, is simplified down to updating properties/nodes, which themselves live at the top of the state atom or are only 2-3 levels deep:

; change the root list of people, and modify the name and age of person 2
(swap! state (fn [s]
               (-> s
                 (assoc :people [[:people/by-id 1] [:people/by-id 2]])
                 (assoc-in [:people/by-id 2 :person/name] "George")
                 (assoc-in [:people/by-id 2 :person/age] 33))))

For the most part the UI takes care of itself. Clojure has very good functions for manipulating maps and vectors, so even when your data structures get more complex the task is still about as simple as it can be.

3.4.3. Client Database Naming Conventions

To avoid collisions in your database, the following naming conventions are recommended for use in the Fulcro client-side graph database:

UI-only Properties

:ui/name. These are special in that they never end up in server queries derived from components. Can be used on any node to hold UI-only state. Not needed if the node itself is not involved with server interaction.

Tables

:entity-type/index-indicator. Examples: :person/by-id or :graph/by-type

Root properties

:root/prop-name

Targeted Loads

Loads temporarily place their results in root. Targeting relocates them. If you’ve followed the other naming conventions, then these can elide a namespace if that facilitates server interactions.

Node properties

:entity-type/property-name. Examples: :person/name or :graph/data

4. Component Rendering

4.1. HTML5 Element Factories

The core HTML5 elements all have simple factory functions that generate the core elements that stand-in for the real DOM. These stand-ins (commonly referred to as the virtual DOM or VDOM) are ultimately what React uses to generate, diff, and update the real DOM.

So, there are functions for every possible HTML5 element. These are in the fulcro.client.dom namespace.

(ns app.ui
  (:require [fulcro.client.dom :as dom]))

...

(dom/div :.some-class
  (dom/ul {:style {:color :red}}
    (dom/li ...)))
Warning
Fulcro 2.4 and below required you to specify props as a #js map or nil.
Important
If you’re writing your UI in CLJC files in 2.5, then you need to make sure you use a conditional reader to pull in the proper server DOM functions for Clojure: (ns app.ui (:require #?(:clj [fulcro.client.dom-server :as dom] :cljs [fulcro.client.dom :as dom]))

The notation allowed is as follows:

(dom/div "Hello") ; no props
(dom/div nil "Hello") ; nil props (may perform slightly better)
(dom/div #js {:data-x 1} ...) ; js objects are allowed
(dom/div :.cls.cls2#id ...) ; shorthand for specifying static classes and ID
(dom/div :.cls {:data-x 2} "Ho") ; shorthand + props

Fulcro 2.5.12+ also includes a :classes property that can be used to add (typically via expressions) additional classes. It drops nil, and allows classname keywords and strings as well:

(dom/div :.a {:className "other" :classes [(when hidden "hidden") (if tall :.tall :.short)]} ...)

Assuming hidden and (not tall) would yield classes "a other hidden short" on output. Of course you probably don’t need all of these at once, but supporting them all lets you programmatically combine them when generating props. NOTE: This feature does not work on props sent with the legacy #js notation.

Remember that this (nested) call of functions results in a representation (VDOM) of what you’d like to end up on the screen.

The next level of abstraction is simply a function. Combining more complex bits of UI into a function is a great way to group re-usable nested DOM:

(defn my-header []
  (dom/div :.some-class
    (dom/ul
      (dom/li ...))))

4.1.1. Fulcro and React DOM – Notes

Here are some common things you’ll want to know how to do that are different when rendering with Fulcro:

  • dom/p is a function for generating p tags for use in React components. There is one of these for every legal HTML tag.

  • As an example - CSS class names are specified with :className instead of :class.

    • Any time a VDOM includes a collection of elements they should each have a unique :key attribute. This helps the React diff figure out how that collection has changed. You will get warnings in the browser console if you fail to do so.

4.2. The Reconciler

When you start a Fulcro application your :started-callback will get the completed app as a parameter.

Inside of this app is a reconciler under the key :reconciler. The reconciler is a central component in the system that is responsible reconciling the differences between the database and the UI. Therefore it is involved in processing the queries, merging novel data into the database, network interactions, and tracking mounted components that might need refresh.

You will see it mentioned in many places in this book, and we’ll point out where you’ll use it directly.

4.2.1. Useful Reconciler Options

When you create a new client you can pass options directly to the reconciler with :reconciler-options. There are a number of options that are used internally by the higher-level layers of Fulcro and should not really be used directly, but there are a number of options that can be quite useful.

:shared

A map of global (immutable) properties that will be visible to all components. See Shared State.

:shared-fn

A function to compute shared properties from the root props on UI refresh. only recomputed on root-level refresh, such as a call to force-root-render. See Shared State.

:root-render

The root render function. Defaults to ReactDOM.render. Useful for switching to React Native.

:root-unmount

The root unmount function. Defaults to ReactDOM.unmountComponentAtNode. Useful for React Native.

:render-mode

One of :normal, :keyframe, or :brutal. See below.

:lifecycle

A function (fn [component event]) that is called when react components either :mount or :unmount. Useful for debugging tools.

:tx-listen

A function of 2 arguments that will listen to transactions. Called when transact! runs. Gets the transaction and the environment. The environment includes :old-state, :new-state, the state atom, etc.

:instrument

A function that will wrap all rendering. (fn [{:keys [props children class factory}] ) that is called instead of the real factory. You can use this to wrap all UI components for things like performance timing, tooling, etc.

4.2.2. Rendering Optimizations

The reconciler does a number of things to optimize rendering beyond the basics of React.

  • Fulcro provides a built-in shouldComponentUpdate that uses a comparison of the prior props to tell React to skip no-op updates that would lead to a useless VDOM diff.

  • When more than one component needs a refresh Fulcro analyzes the tree and only renders the parent of common children to to prevent multiple-rendering of children.

  • If a component is the target of a refresh and has an ident then Fulcro will run the query for just that component, avoiding quite a bit of database processing. This is the biggest performance win in "flame graph" tests.

    • NOTE: a component without an ident will always trigger a root query/refresh, since there is no way to figure out how to run that component’s query (queries are relative, and idents give you an anchor point for them).

The shouldComponentUpdate optimization reduces the render load by quite a bit, but running the query from root can be somewhat costly depending on how well you optimized your UI query. Thus, idents become a major factor in both normalization and rendering performance since Fulcro relies on them in order to reduce the query overhead of UI refresh. This isn’t something you typically need to remember, since you should be using idents on all components so they are easier to refactor and reuse.

4.2.3. Rendering Modes

Version 2.1+ of Fulcro include the ability to tell the reconciler which rendering mode to use.

:normal

The default mode. Uses all possible optimizations.

:keyframe

Disables the ident-based targeted refresh. Thus every render is considered a key frame of the DOM. This means that every transaction/change will run the root UI query and render from root. The shouldComponentUpdate optimization is in force and prevents quite a bit of work from React. This mode can be plenty fast and has the advantage of not needing you to program with follow-on reads.

:brutal

Disables all optimizations, runs queries from root, runs refresh from root, and forces React to do a full DOM diff. Primarily useful to compare how much benefit optimizations are actually giving you beyond React’s DOM diff.

4.3. The defsc Macro

Note
If you’re new to Fulcro and started with version 2.0, you can safely ignore most comments about defui. The two are rougly equivalent, with defsc being the newer.

Fulcro’s defsc is a front-end to the legacy defui macro. It is sanity-checked for the most common elements: ident (optional), query, render, and initial state (optional). The sanity checking prevents a lot of the most common errors when writing a component, and the concise syntax reduces boilerplate to the essential novelty. The name means "define stateful component" and is intended to be used with components that have queries (though that is not a requirement).

4.3.1. The Argument List

The primary argument list contains the common elements you might need to use in the body:

(defsc [this props <optional-computed> <optional-css-map-if-using-css>]
 { ...options... }
 (dom/div {:onClick (:onClick computed)} (:db/id props)))

The last parameter will let you destructure fulcro-css names, if and only if you’re using the Fulcro CSS library.

Only the first two parameters are required, so you can even write:

(defsc [this props]
 { ...options... }
 (dom/div (:db/id props)))
Argument Destructuring

The parameter list fully supports Clojure destructuring on the props, computed, and css-name-map without having to write a separate let:

(defsc DestructuredExample [this
                            {:keys [db/id] :as props}
                            {:keys [onClick] :as computed :or {onClick identity}}
                            {:keys [my-css-class] :as css-name-map}]
  {:query         [:db/id]
   :initial-state {:db/id 22}
   :css           [[:.my-css-class {:color :black}]]}
  (dom/div {:className my-css-class}
    (str "Component: " id)))

4.3.2. Options – Lambda vs. Template

The core options (:query, :ident, :initial-state, :css, and :css-include) of defsc support both a lambda and a template form. The template form is shorter and enables some sanity checks; however, it is not expressive enough to cover all possible cases. The lambda form is slightly more verbose, but enables full flexibility at the expense of the sanity checks.

IMPORTANT NOTE: In lambda mode use this and props from the defsc argument list. So, for example, (fn [] [:x]) is valid for query (this is added by the macro), and (fn [] [:table/by-id id]) is valid for ident.

4.3.3. Ident Generation

If you include :ident, it can take two forms: a template or lambda.

Template Idents

A template ident is just a vector that patterns what goes in the ident. The first element is always literal, and the second is the name of the property to pull from props to get the ID.

This is the most common case, and if you use the template mechanism you get some added sanity checks: it won’t compile if your ID key isn’t in your query, eliminating some possible frustration.

Lambda Idents

Template idents are great for the common case, but they don’t work if you have a single instance ever (i.e. you want a literal second element), and they won’t work at all for union queries. They also do not support embedded code. Therefore, if you want a more advanced ident, you’ll need to spell out the code.

defsc at least eliminates some of the boilerplate:

(defsc UnionComponent [this {:keys [db/id component/type]}]
  {:ident (fn [] (union-ident type id))} ; id and type are destructured into the method for you.
  ...)

4.3.4. Query

defsc also allows you to specify the query as a template or lambda.

Template Query

The template form is strongly recommended for most cases, because without it many of the sanity checks won’t work.

In template mode, the following sanity checks are enabled:

  • The props destructuring

  • The ident’s id name is in the query (if ident is in template mode)

  • The initial app state only contains things that are queried for (if it is in template mode as well)

Lambda Query

This mode is necessary if you use more complex queries. The template mode currently does not support union queries.

To use this mode, specify your query as (fn [] [:x]). In lambda mode, this comes from the argument list of defsc.

4.3.5. Initial State

As with :query and :ident, :initial-state supports a template and lambda form.

The template form for initial state is a bit magical, because it tries to sanity check your initial state, but also has to support relations through joins. Finally it tries to eliminate typing for you by auto-wrapping nested relation initializations in get-initial-state for you by deriving the correct class to use from the query. This further reduces the chances of error; however, you may find the terse result more difficult to read and instead choose to write it yourself. Both ways are supported:

Lambda mode

This is simple to understand, and all you need to see is a simple example:

(defsc Component [this props]
 {:initial-state (fn [params] ...exactly the state this component should start with...)}
 ...)
Template Mode

In template mode :initial-state converts incoming parameters (which must use simple keywords) into :param/X keys. So,

(defsc Person [this props]
  {:initial-state {:db/id :param/id}}
  ...)

means:

(defsc Person [this props]
  {:initial-state (fn [params] {:db/id (:id params)}})}
  ...)

It is even more powerful than that, because it analyzes your query and can deal with to-one and to-many join initialization as well:

(defsc Person [this props]
  {:query [{:person/job (prim/get-query Job)}]
  :initial-state {:person/job {:job/name "Welder"}}
  ...)

means (in simplified terms):

(defsc Person [this props]
  {:initial-state (fn [params] {:person/job (prim/get-initial-state Job {:job/name "Welder"})})}
  ...)

Notice the magic there. Job was pulled from the query by looking for joins on the initialization keyword (:person/job).

To-many relations are also auto-derived:

(defsc Person [this props]
  {:query [{:person/prior-jobs (prim/get-query Job)}]
  :initial-state {:person/prior-jobs [{:job/name "Welder"} {:job/name "Cashier"]}
  ...)

means (in simplified terms):

(defsc Person [this props]
  {:initial-state (fn [params]
    {:person/prior-jobs [(prim/get-initial-state Job {:job/name "Welder"})
                         (prim/get-initial-state Job {:job/name "Cashier"})]})}
  ...)

The internal steps for processing this template are:

  • Replace all uses of :param/nm with (get params :nm)

  • The query is analyzed for joins on keywords (ident joins are not supported).

    • If a key in the initial state matches up with a join, then the value in initial state must be a map or a vector. In that case (get-initial-state JoinClass p) will be called for each map (to-one) or mapped across the vector (to-many).

REMEMBER: the value that you use in the initial-state for children is the parameter map to use against that child’s initial state function. To-one and to-many relations are implied by what you pass (a map is to-one, a vector is to-many).

Step (1) means that nesting of param-namespaced keywords is supported, but realize that the params come from the declaring component’s initial state parameters, they are substituted before being passed to the child.

4.3.6. CSS Support

Support for using fulcrologic/fulcro-css is built-in. Before 2.4 it was a dynamic dependency so to use needed to include the fulcrologic/fulcro-css library in your project dependencies and require the fulcro-css.css namespace in any file that used this support.

As of version 2.4 it is intregrated with Fulcro, and requires no special dependency.

The keys in the defsc options map to leverage co-located CSS are:

  • :css - The items to put in protocol method css/local-rules. Can be pure garden data, or (fn [] …​)

  • :css-include - The items to put in protocol method css/include-children. Can be a vector of classes, or (fn [] …​)

Both are optional. If you use neither, then your code will not incur a dependency on the fulcro-css library.

See Colocated CSS for more details.

4.3.7. React Lifecycle Methods

The options of defsc allow for React Lifecycle methods to be defined (as lambdas). The this parameter of defsc is in scope for all of them, but not props or computed. You can obtain computed using prim/get-computed. This is because the lifecycle method may receive prior or next props, and using the top parameter list could be confusing.

The signatures are:

(defsc Component [this props]
    ; remember that 'this' is in scope for lifecycle
    :initLocalState            (fn [] ...)
    :shouldComponentUpdate     (fn [next-props next-state] ...)
    :componentWillReceiveProps (fn [next-props] ...)
    :componentWillUpdate       (fn [next-props next-state] ...)
    :componentDidUpdate        (fn [prev-props prev-state] ...)
    :componentWillMount        (fn [] ...)
    :componentDidMount         (fn [] ...)
    :componentWillUnmount      (fn [] ...)

See the React documentation for more details on how these work.

Additional Protocol Support

If you need to include additional protocols (or lifecycle React methods) on the generated class then you can use the :protocols option. It takes a list of forms that have the same shape as the body of a defui, and the static qualifier is supported. If you supply Object methods then they will be properly combined with the generated render:

Here is an example of adding Fulcro CSS using protocols instead of options:

(defsc MyComponent [this props]
   {:protocols (Object
                static css/CSS
                (local-rules [_] [])
                (include-children [_] []))
    ...}
   (dom/div ...))

This gives you the full protocol capabilities of defui, but you only need the extra protocol additions when you use methods and protocols beyond the central ones.

4.3.8. Sanity Checking

The sanity checking mentioned in the earlier sections causes compile errors. The errors are intended to be self-explanatory. They will catch common mistakes (like forgetting to query for data that you’re pulling out of props, or misspelling a property).

Feel free to edit the components in this source file and try out the sanity checking. For example, try:

  • Mismatching the name of a prop in options with a destructured name in props.

  • Destructuring a prop that isn’t in the query

  • Including initial state for a field that is not listed as a prop or child in options.

  • Using a scalar value for the initial value of a child (instead of a map or vector of maps)

  • Forget to query for the ID field of a component that is stored at an ident

In some cases the sanity checking is more aggressive that you might desire. To get around it simply use the lambda style.

4.4. Factories

Factories are how you generate React elements (the virtual DOM nodes) from your React classes defined with defsc. You make a new factory using fulcro.client.primitives/factory:

(def ui-component (prim/factory MyComponent {:keyfn f :validator v :instrument? true}))

There are 3 supported options to a factory:

:keyfn

A function from props to a React key. Should generally be supplied to ensure React rendering can properly diff.

:validator

A function from props to boolean. If it returns false then an assertion will be thrown at runtime.

:instrument?

A boolean. If true, it indicates that instrumentation should be enabled on the component.

Instrumentation is a function you can install on the reconciler that wraps component render allowing you to add measurement and debugging code to your component’s rendering.

In Fulcro documentation we generally adopt the naming convention for UI factories to be prefixed with ui-. This is because you often want to name joins the same thing as a component: e.g. your query might be [{:child (prim/get-query Child)}], and then when you destructure in render: (let [{:keys [child]} (prim/props this) …​ you have local data in the symbol child. If your UI factory was also called child then it would cause annoying name collisions. Prefixing the factories with ui- makes it very clear what is data and what is an element factory.

4.5. Render and Props

Properties are always passed to a component factory as the first argument. The properties can be accessed from within render by calling fulcro.client.primitives/props on the parameter passed to render (typically named this to remind you that it is a reference to the instance itself).

In components with queries there is a strong correlation between the query (which must join the child’s query), props (from which you must extract the child’s props), and calling of the child’s factory (to which you must pass the child’s data).

If you are using components that do not have queries, then you may pass whatever properties you deem useful.

Details about additional aspects of rendering are in the sections that follow.

4.5.1. Derived Values

It is possible that your logic and state will be much simpler if your UI components derive some values at render time. A prime example of this is the state of a "check all" button. The state of such a button is dependent on other components in the UI, and it is not a separate value. Thus, your UI should compute it and not store it else it could easily become out of sync and lead to more complex logic.

(defn item-checked? [item] (:checked? item))

(defsc Checkboxes [this {:keys [items]}]
  {:query [{:items (prim/get-query CheckboxItem)}]}
  (let [all-checked? (every item-checked? items)]
    (dom/div
      "All: " (dom/input {:checked all-checked? ...})
    (dom/ul ...))))
General Guidelines for Derived Values

You should consider computing a derived value when: * The known data from the props already gives you sufficient information to calculate the value. * The computation is relatively light.

Some examples where UI computation are effective, light, or even necessary:

  • Rendering an internationalized value. (e.g. tr)

  • Rendering a check-all button

  • Rendering "row numbering" or other decorations like row highlighting

There are some trade-offs, but most significantly you generally do not want to compute things like the order/pagination of a list of items. The logic and overhead in sorting and pagination often needs caching, and there are clear and easy "events" (user clicking on sort-by-name) that make it clear when to call the mutation to update the database. You still have to store the selected sort order, and you have to have idents pointing to the list of items. It is possible for your "selected sort order" and list to become out of sync, but the trade-offs of sorting in the UI are typically high, particularly when pagination is involved and large amounts of data would have to be fed to the UI.

4.5.2. Computed Props and Callbacks

Many reusable components will need to tell their parent about some event. For example, a list item generally wants to tell the parent when the user has clicked on the "remote" button for that item. The item itself cannot be truly composable if it has to know details of the parent. But a parent must always know the details of a child (it rendered it, didn’t it?). As such, manipulations that affect the content of a parent should be communicated to that parent for processing. The mechanism for this is identical to what you’d do in stock React: callbacks from the child.

The one major difference is how you pass the callback to a component.

The query and data feed mechanisms that supply props to a component are capable of refreshing a child without refreshing a parent. This UI optimization can pull the props directly from the database using the query, and re-feed them to the child.

But this mechanism knows nothing about callbacks, because they are not (and should not be) stored in the client database. Such a targeted refresh of a component cannot pass callbacks through the props because the parent is where that is coded, but the parent may not be involved in the refresh!

So, any value (function or otherwise) that is generated on-the-fly by the parent must be passed via fulcro.client.primitives/computed. This tells the data feed system how to reconstruct the complete data should it do a targeted update.

(defsc Child [this {:keys [y]}]
  {:query [:y]}
  (let [onDelete (prim/get-computed this :onDelete)]
    ...))

(defsc Parent [this {:keys [x child]}]
  {:query  [:x {:child (prim/get-query Child)}]}
  (let [onDelete (fn [id] (prim/transact! ...))
        child-props-with-callbacks (prim/computed child {:onDelete onDelete})]
    (ui-child child-props-with-callbacks)))
Warning
Not understanding this can cause a lot of head scratching: The initial render will always work perfectly, because the parent is involved. All events will be processed, and you’ll think everything is fine; however, if you have passed a callback incorrectly it will mysteriously stop working after a (possibly unnoticeable) refresh. This means you’ll "test it" and say it is OK, only to discover you have a bug that shows up during heavier use.

4.5.3. Children

A very common pattern in React is to define a number of custom components that are intended to work in a nested fashion. So, instead of just passing props to a factory, you might also want to pass other React elements. This is fully supported in Fulcro, but can cause confusion when you first try to mix it with the data-driven aspect of the system.

Working with Children

Fulcro includes a few functions that are helpful when designing React components that are intended to be nested as direct children within a single render:

(prim/children this)

Returns the React children of this

(cutil/react-instance? Component instance)

Returns true if the given element is an instance of the given component class. Otherwise nil.

(cutil/first-node Component child-seq)

Returns the first of a sequence of elements that has the given component class.

So, say you wanted to create the following kind of rendering scheme:

(defsc Panel ...)
(def ui-panel (prim/factory Panel))
(defsc PanelHeader ...)
(def ui-panel-header (prim/factory PanelHeader))
(defsc PanelBody ...)
(def ui-panel-body (prim/factory PanelBody))

(ui-panel {}
  (ui-panel-header {} "Some Heading Text")
  (ui-panel-body {}
     (dom/div "Some sub-DOM")))

The DOM generation for Panel will need to find the header and body children:

(defsc Panel [this props]
  (let [children (prim/children this)
        header (util/first-node PanelHeader children)
        body (util/first-node PanelBody children)]
    (when header
      (dom/h4 header))
    (when body
      (dom/div body))))

Basically, the child or children can simply be dropped into the place where they should be rendered.

React 16 Fragments and Returning Multiple Children

Fulcro 2.6+ with React 16 allows you to return a vector of children or a Fragment from a component (so you no longer are forced to have a single child as a return value). Fulcro DOM namespaces include a fragment function to wrap elements in a fragment:

(defsc X [this props]
  (dom/fragment
    (dom/p "a")
    (dom/p "b")))

or

(defsc X [this props]
  [(dom/p "a") (dom/p "b")])

allows your component to return multiple elements that are spliced into the parent without any "wrapping" elements in the DOM.

(dom/div (ui-x)) => <div><p>a</p><p>b</p></div>

See React documentation for more details.

Mixing Data-Driven Children with UI-Only Concerns

At first this seems a little mind-bending, because you are in fact nesting components in the UI, but the query nesting need only mimic the stateful portion of the UI tree. This means there is ample opportunity to use React children in a way that looks incorrect from what you’ve learned so far. On deeper inspection it turns out it is alignment with the rules, but it takes a minute on first exposure.

Take the Bootstrap collapse component: It needs state of its own in order to know when it is collapsed, and we’d like that to be part of the application database so that the support history viewer can show the correct thing. However, the children of the collapse cannot be known in advance when writing the collapse reusable library component.

The solution is simple once you see it: Query for the collapse component’s state and the child state in the common parent component, then do the UI nesting in that component. Technically the component that is "laying out" the UI (the ultimate parent) is in charge of both obtaining and rendering the data. The fact that the UI child ends up nested in a query sibling is perfectly fine.

The collapse component itself is only concerned with the fact that it is open/closed, and that it has children that should be shown/hidden. The actual DOM elements of those children are immaterial, and can be assembled by the parent:

(defsc CollapseExample [this {:keys [collapse-1 child]}]
  {:initial-state (fn [p] {:collapse-1 (prim/get-initial-state b/Collapse {:id 1 :start-open false})})
   :query [{:collapse-1 (prim/get-query b/Collapse)}
           {:child (prim/get-query SomeChild)}]}
  (dom/div
    (b/button {:onClick (fn [] (prim/transact! this `[(b/toggle-collapse {:id 1})]))} "Toggle")
    (b/ui-collapse collapse-1
      (ui-child child))))

4.6. Controlled Inputs

Form inputs in React can take two possible approaches: controlled and uncontrolled. The browser normally maintains the value state of inputs for you as mutable data; however, this breaks our overall model of pure rendering! The advantage is UI interaction speed: If your UI gets rather large, it is possible that UI updates on keystrokes in form inputs may be too slow. This is the same sort of trade-off that we talked about when covering component local state for rendering speed with more graphical components. If you follow the basic optimization guidelines then your application should be fast enough to do database updates on every keystroke, and you can keep all input changes in your client database.

In general it is recommended that you use controlled inputs and retain the benefits of pure rendering: no embedded state, your UI exactly represents your data representation, concrete devcards support for UI prototyping, and full support viewer support.

Most inputs become controlled when you set their :value property. The table below lists the mechanism whereby a form input is completely controlled by React:

Input type Attribute Notes

input

:value

(not checkboxes or radio)

checkbox

:checked

radio

:checked

(only one in a group should be checked)

textarea

:value

select

:value

Instead of marking an option selected. Match select’s `:value to the :value of a nested option.

Important
React will consider nil to mean you want an uncontrolled component. This can result in a warning about converting uncontrolled to controlled components. In order to prevent this warning you should make sure that :checked is always a boolean, and that other inputs have a valid :value (e.g. an empty string). The select input can be given an "extra" option that stands for "not selected yet" so that you can start its value at something valid.

See React Forms for more details.

4.7. React Lifecycle Examples

Note
Fulcro 2.6+ includes React 16.4+ Lifecycle support (except getDerivedStateFromProps). You must explicitly include react and react-dom as dependencies in order to get the newer React versions since Fulcro still defaults to 15.6 to prevent breakage of existing apps.

There are some common use-cases that can only be solved by working directly with the React Lifecycle methods.

Some topics you should be familiar with in React to accomplish many of these things are:

  • Component references: A mechanism that allows you access to the real DOM of the component once it’s on-screen.

  • Component-local state: A stateful mechanism where mutable data is stored on the component instance.

  • General DOM manipulation. Clojurescript builds using the Google Closure compiler and therefore includes the Google Closure library, which in turn has all sorts of helpful low-level functions should you need them.

4.7.1. Focusing an input

Focus is a stateful browser mechanism, and React cannot force the rendering of "focus". As such, when you need to deal with UI focus it generally involves some interpretation, and possibly component local state. One way of dealing with deciding when to focus is to look at a component’s prior vs. next properties. This can be done in componentDidUpdate. For example, say you have an item that renders as a string, but when clicked turns into an input field. You’d certainly want to focus that, and place the cursor at the end of the existing data (or highlight it all).

If your component had a property called editing? that you made true to indicate it should render as an input instead of just a value, then you could write your focus logic based on the transition of your component’s props from :editing? false to :editing? true.

Show/Hide Source
(ns book.ui.focus-example
  (:require [fulcro.client.primitives :as prim :refer [defsc]]
            [fulcro.client.dom :as dom]
            [fulcro.client.mutations :as m]))

(defsc ClickToEditField [this {:keys [value editing?]}]
  {:initial-state      {:value    "ABC"
                        :db/id    1
                        :editing? false}
   :query              [:db/id :value :editing?]
   :ident              [:field/by-id :db/id]
   :componentDidUpdate (fn [prev-props _]
                         (when (and (not (:editing? prev-props)) (:editing? (prim/props this)))
                           (let [input-field        (dom/node this "edit_field")
                                 input-field-length (.. input-field -value -length)]
                             (.focus input-field)
                             (.setSelectionRange input-field input-field-length input-field-length))))}
  (dom/div
    ; trigger a focus based on a state change (componentDidUpdate)
    (dom/a {:onClick #(m/toggle! this :editing?)}
      "Click to focus (if not already editing): ")
    (dom/input {:value    value
                :onChange #(m/set-string! this :event %)
                :ref      "edit_field"})
    ; do an explicit focus
    (dom/button {:onClick (fn []
                            (let [input-field        (dom/node this "edit_field")
                                  input-field-length (.. input-field -value -length)]
                              (.focus input-field)
                              (.setSelectionRange input-field 0 input-field-length)))}
      "Highlight All")))

(def ui-click-to-edit (prim/factory ClickToEditField))

(defsc Root [this {:keys [field] :as props}]
  {:query         [{:field (prim/get-query ClickToEditField)}]
   :initial-state {:field {}}}
  (ui-click-to-edit field))
Note
React documentation encourages a more functional form of ref (you supply a function instead of a string). This example could also cache that in component local state like this:
(defsc ClickToEditField [this {:keys [value editing?]}]
  ...
     (dom/input {:ref (fn [r] (prim/set-state! this {:input r}))})
  ...

However, The wrapped inputs of Fulcro 2.3.0 and earlier (which fixed a different issue with React) did not work with the functional ref technique (use strings and dom/node instead). As of 2.3.1 this is fixed, and while inputs still have the internal wrapping to prevent "lost keys" bugs, they work with both the older string support or functional refs.

4.7.2. Taking control of the sub-DOM (D3, etc)

Libraries like D3 are great for dynamic visualizations, but they need full control of the portion of the DOM that they create and manipulate.

In general this means that your render method should be called once (and only once) to install the base DOM onto which the other library will control.

For example, let’s say we wanted to use D3 to render things. We’d first write a function that would take the real DOM node and the incoming props:

(defn db-render [DOM-NODE props] ...)

This function should do everything necessary to render the sub-dom (and update it if the props change). Then we’d wrap that under a component that doesn’t allow React to refresh that sub-tree via shouldComponentUpdate.

Below is a demo of this:

Show/Hide Source
(ns book.ui.d3-example
  (:require [fulcro.client.dom :as dom]
            cljsjs.d3
            [fulcro.client.mutations :as m :refer [defmutation]]
            [fulcro.client.primitives :as prim :refer [defsc]]))

(defn render-squares [component props]
  (let [svg       (-> js/d3 (.select (dom/node component)))
        data      (clj->js (:squares props))
        selection (-> svg
                    (.selectAll "rect")
                    (.data data (fn [d] (.-id d))))]
    (-> selection
      .enter
      (.append "rect")
      (.style "fill" (fn [d] (.-color d)))
      (.attr "x" "0")
      (.attr "y" "0")
      .transition
      (.attr "x" (fn [d] (.-x d)))
      (.attr "y" (fn [d] (.-y d)))
      (.attr "width" (fn [d] (.-size d)))
      (.attr "height" (fn [d] (.-size d))))
    (-> selection
      .exit
      .transition
      (.style "opacity" "0")
      .remove)
    false))

(defsc D3Thing [this props]
  {:componentDidMount         (fn [] (render-squares this (prim/props this)))
   :shouldComponentUpdate     (fn [next-props next-state] false)
   :componentWillReceiveProps (fn [props] (render-squares this props))}
  (dom/svg {:style   {:backgroundColor "rgb(240,240,240)"}
            :width   200 :height 200
            :viewBox "0 0 1000 1000"}))

(def d3-thing (prim/factory D3Thing))

(defn random-square []
  {
   :id    (rand-int 10000000)
   :x     (rand-int 900)
   :y     (rand-int 900)
   :size  (+ 50 (rand-int 300))
   :color (case (rand-int 5)
            0 "yellow"
            1 "green"
            2 "orange"
            3 "blue"
            4 "black")})

(defmutation add-square [params]
  (action [{:keys [state]}]
    (swap! state update :squares conj (random-square))))

(defmutation clear-squares [params]
  (action [{:keys [state]}]
    (swap! state assoc :squares [])))

(defsc Root [this props]
  {:query         [:squares]
   :initial-state {:squares []}}
  (dom/div
    (dom/button {:onClick #(prim/transact! this
                             `[(add-square {})])} "Add Random Square")
    (dom/button {:onClick #(prim/transact! this
                             `[(clear-squares {})])} "Clear")
    (dom/br)
    (dom/br)
    (d3-thing props)))

The things to note for this example are:

  • We override the React lifecycle method shouldComponentUpdate to return false. This tells React to never ever call render once the component is mounted. D3 is in control of the underlying stuff.

  • We override componentWillReceiveProps and componentDidMount to do the actual D3 render/update. The former will get incoming data changes, and the latter is called on initial mount. Our render method delegates all of the hard work to D3.

4.7.3. Dynamically rendering into a canvas

Sometimes you need to use component-local state to avoid the overhead in running a query to feed props. An example of this is when handing mouse interactions like drag. You’ll typically use React refs to grab the actual low-level canvas.

In Fulcro version before 2.6 there are actually two ways to change component-local state. One of them defers rendering to the next animation frame, but it also reconciles the database with the stateful components. This one will not give you as much of a speed boost (though it may be enough, since you’re not changing the database or recording more UI history).

The other mechanism completely avoids this, and just asks React for an immediate forced update.

In Fulcro 2.6 set-state! is a Fulcro wrapper of React’s setState, but it let’s you use cljs maps instead of js ones. The shallow merge described by React is honored as if you used an updater function (there is no need for an updater function). The set-state! also allows a callback to give you full React compatibility.

The update-state! function in Fulcro is like Clojurescripts swap! function, and uses set-state! behind the scenes.

In older versions of Fulcro:

  • (set-state! this data) and (update-state! this data) - trigger a reconcile against the database at the next animation frame. Limits frame rate to 60 fps (in 2.6+ is identical to React setState with an updater and optional callback, but you just supply the map that will be merged. Use update-state! if you need more functional control.

  • (react-set-state! this data) - triggers a React render according to React’s setState documentation.

In this example we’re using set-state!, and you can see it is still plenty fast!

Show/Hide Source
(ns book.ui.hover-example
  (:require
    [fulcro.client.cards :refer [defcard-fulcro]]
    [fulcro.client.mutations :refer [defmutation]]
    [fulcro.client.primitives :as prim :refer [defsc InitialAppState initial-state]]
    [fulcro.client.dom :as dom]))

(defn change-size*
  "Change the size of the canvas by some (pos or neg) amount.."
  [state-map amount]
  (let [current-size (get-in state-map [:child/by-id 0 :size])
        new-size     (+ amount current-size)]
    (assoc-in state-map [:child/by-id 0 :size] new-size)))

; Make the canvas smaller. This will cause
(defmutation ^:intern make-smaller [p]
  (action [{:keys [state]}]
    (swap! state change-size* -20)))

(defmutation ^:intern make-bigger [p]
  (action [{:keys [state]}]
    (swap! state change-size* 20)))

(defmutation ^:intern update-marker [{:keys [coords]}]
  (action [{:keys [state]}]
    (swap! state assoc-in [:child/by-id 0 :marker] coords)))


(defn event->dom-coords
  "Translate a javascript evt to a clj [x y] within the given dom element."
  [evt dom-ele]
  (let [cx (.-clientX evt)
        cy (.-clientY evt)
        BB (.getBoundingClientRect dom-ele)
        x  (- cx (.-left BB))
        y  (- cy (.-top BB))]
    [x y]))

(defn event->normalized-coords
  "Translate a javascript evt to a clj [x y] within the given dom element as normalized (0 to 1) coordinates."
  [evt dom-ele]
  (let [cx (.-clientX evt)
        cy (.-clientY evt)
        BB (.getBoundingClientRect dom-ele)
        w  (- (.-right BB) (.-left BB))
        h  (- (.-bottom BB) (.-top BB))
        x  (/ (- cx (.-left BB))
             w)
        y  (/ (- cy (.-top BB))
             h)]
    [x y]))

(defn render-hover-and-marker
  "Render the graphics in the canvas. Pass the component props and state. "
  [props state]
  (let [canvas             (:canvas state)
        hover              (:coords state)
        marker             (:marker props)
        size               (:size props)
        real-marker-coords (mapv (partial * size) marker)
        ; See HTML5 canvas docs
        ctx                (.getContext canvas "2d")
        clear              (fn []
                             (set! (.-fillStyle ctx) "white")
                             (.fillRect ctx 0 0 size size))
        drawHover          (fn []
                             (set! (.-strokeStyle ctx) "gray")
                             (.strokeRect ctx (- (first hover) 5) (- (second hover) 5) 10 10))
        drawMarker         (fn []
                             (set! (.-strokeStyle ctx) "red")
                             (.strokeRect ctx (- (first real-marker-coords) 5) (- (second real-marker-coords) 5) 10 10))]
    (.save ctx)
    (clear)
    (drawHover)
    (drawMarker)
    (.restore ctx)))

(defn place-marker
  "Update the marker in app state. Derives normalized coordinates, and updates the marker in application state."
  [child evt]
  (prim/transact! child `[(update-marker
                            {:coords ~(event->normalized-coords evt (prim/get-state child :canvas))})]))

(defn hover-marker
  "Updates the hover location of a proposed marker using canvas coordinates. Hover location
   is stored in component local state (meaning that a low-level app database query will not
   run to do the render that responds to this change)"
  [child evt]
  (let [current-state  (prim/get-state child)
        updated-coords (event->dom-coords evt (:canvas current-state))
        new-state      (assoc current-state :coords updated-coords)]
    (prim/set-state! child new-state)
    (render-hover-and-marker (prim/props child) new-state)))

(defsc Child [this {:keys [id size]}]
  {:query          [:id :size :marker]
   :initial-state  (fn [_] {:id 0 :size 50 :marker [0.5 0.5]})
   :ident          (fn [] [:child/by-id id])
   :initLocalState (fn [] {:coords [-50 -50]})}
  ; Remember that this "render" just renders the DOM (e.g. the canvas DOM element). The graphical
  ; rendering within the canvas is done during event handling.
  ; size comes from props. Transactions on size will cause the canvas to resize in the DOM
  (dom/canvas {:width       (str size "px")
               :height      (str size "px")
               :onMouseDown (fn [evt] (place-marker this evt))
               :onMouseMove (fn [evt] (hover-marker this evt))
               ; This is a pure React mechanism for getting the underlying DOM element.
               ; Note: when the DOM element changes this fn gets called with nil
               ; (to help you manage memory leaks), then the new element
               :ref         (fn [r]
                              (when r
                                (prim/update-state! this assoc :canvas r)
                                (render-hover-and-marker (prim/props this) (prim/get-state this))))
               :style       {:border "1px solid black"}}))

(def ui-child (prim/factory Child))

(defsc Root [this {:keys [child]}]
  {:query         [{:child (prim/get-query Child)}]
   :initial-state (fn [params] {:ui/react-key "K" :child (initial-state Child nil)})}
  (dom/div
    (dom/button {:onClick #(prim/transact! this `[(make-bigger {})])} "Bigger!")
    (dom/button {:onClick #(prim/transact! this `[(make-smaller {})])} "Smaller!")
    (dom/br)
    (dom/br)
    (ui-child child)))

The component receives mouse move events to show a hover box. To make this move in real-time we use component local state. Clicking to set the box, or resize the container are real transactions, and will actually cause a refresh from application state to update the rendering.

4.8. Using Javascript React Components

One of the great parts about React is the ecosystem. There are some great libraries out there. However, the interop story isn’t always straight forward. The goal of this section is to make that story a little clearer.

4.8.1. Factory Functions for JS React Components

Integrating React components is fairly straightforward if you have used React from JS before. The curve comes having spent time with libraries or abstractions like Om and friends. JSX will also abstract some of this away, so it’s not just the cljs wrappers. For a good article explaining some of the concepts read, React Elements The take-aways here are:

  1. If you are importing third party components, you should be importing the class, not a factory.

  2. You need to explicitly create the react elements with factories. The relevant js functions are React.createElement, and React.createFactory.

It is very important to consider when using any of these functions - the children. JS does not have a built in notion of lazy sequences. Clojurescript does. This can create subtle bugs when evaluating the children of a component.

fulcro.util/force-children helps us in this regard by taking a seq and returning a vector. We can use this to create our own factory function, much like React.createFactory:

(defn factory-force-children
  [class]
  (fn [props & children]
    (js/React.createElement class
      props
      (util/force-children children))))

This is fine, but you will notice that children in our factory may be missing keys. Because we passed a vector in, React won’t attach the key attribute. We can solve this problem by using the apply function.

(defn factory-apply
  [class]
  (fn [props & children]
    (apply js/React.createElement
      class
      props
      children)))

Here the apply function will pass the children in as args to React.createElement, thus avoiding the key problem as well as the issue with lazy sequences.

Now that we have some background on creating React Elements it’s pretty simple to implement something. Let' look at making a chart using Victory. We are going to make a simple line chart, with an X axis that contains years, and a Y axis that contains dollar amounts. Really the data is irrelavent, it’s the implementation we care about.

The running code and source are below:

Show/Hide Source
(ns book.ui.victory-example
  (:require [cljs.pprint :refer [cl-format]]
            cljsjs.victory
            [fulcro.client.cards :refer [defcard-fulcro]]
            [fulcro.client.dom :as dom]
            [fulcro.client.primitives :as prim :refer [defsc]]
            [fulcro.util :as util]))

(defn us-dollars [n]
  (str "$" (cl-format nil "~:d" n)))

(defn factory-force-children
  [class]
  (fn [props & children]
    (js/React.createElement class
      props
      (util/force-children children))))

(defn factory-apply
  [class]
  (fn [props & children]
    (apply js/React.createElement
      class
      props
      children)))

(def vchart (factory-apply js/Victory.VictoryChart))
(def vaxis (factory-apply js/Victory.VictoryAxis))
(def vline (factory-apply js/Victory.VictoryLine))

;; " [ {:year 1991 :value 2345 } ...] "
(defsc YearlyValueChart [this {:keys [label plot-data x-step]}]
  (let [start-year (apply min (map :year plot-data))
        end-year   (apply max (map :year plot-data))
        years      (range start-year (inc end-year) x-step)
        dates      (clj->js (mapv #(new js/Date % 1 2) years))
        {:keys [min-value
                max-value]} (reduce (fn [{:keys [min-value max-value] :as acc}
                                         {:keys [value] :as n}]
                                      (assoc acc
                                        :min-value (min min-value value)
                                        :max-value (max max-value value)))
                              {}
                              plot-data)
        min-value  (int (* 0.8 min-value))
        max-value  (int (* 1.2 max-value))
        points     (clj->js (mapv (fn [{:keys [year value]}]
                                    {:x (new js/Date year 1 2)
                                     :y value})
                              plot-data))]
    (vchart nil
      (vaxis #js {:label      label
                  :standalone false
                  :scale      "time"
                  :tickFormat (fn [d] (.getFullYear d))
                  :tickValues dates})
      (vaxis #js {:dependentAxis true
                  :standalone    false
                  :tickFormat    (fn [y] (us-dollars y))
                  :domain        #js [min-value max-value]})
      (vline #js {:data points}))))

(def yearly-value-chart (prim/factory YearlyValueChart))

(defsc Root [this props]
  {:initial-state {:label     "Yearly Value"
                   :x-step    2
                   :plot-data [{:year 1983 :value 100}
                               {:year 1984 :value 100}
                               {:year 1985 :value 90}
                               {:year 1986 :value 89}
                               {:year 1987 :value 88}
                               {:year 1988 :value 85}
                               {:year 1989 :value 83}
                               {:year 1990 :value 80}
                               {:year 1991 :value 70}
                               {:year 1992 :value 80}
                               {:year 1993 :value 90}
                               {:year 1994 :value 95}
                               {:year 1995 :value 110}
                               {:year 1996 :value 120}
                               {:year 1997 :value 160}
                               {:year 1998 :value 170}
                               {:year 1999 :value 180}
                               {:year 2000 :value 180}
                               {:year 2001 :value 200}
                               ]}}
  (dom/div
    (yearly-value-chart props)))

4.8.2. The Function-As-a-Child Pattern

A common pattern in React libraries is to use a function as a single child instead of an actual element. This is an accepted and widely used pattern, but you need to do a simple extra step for it to work properly with Fulcro. You see, Fulcro components use some behind-the-scenes bindings to allow for targeted UI rendering optimizations, and when you embed them in a function that is invoked from external JS out of that context, things won’t work correctly.

For example, the react-motion library gives you React tools that can animate DOM motion. It animates variables that you apply to nested DOM, and it does this through the function-as-a-child pattern. Here’s an example from a demo project (which uses shadow-cljs to get easy access to NPM libraries):

(ns sample
  (:require ["react-motion" :refer [Motion spring]]
            [fulcro.client.primitives :as prim :refer [defsc]]
            [fulcro.client.dom :as dom]
            [goog.object :as gobj]))

(def ui-motion (factory-apply Motion))


(defsc Demo [this {:keys [ui/slid? block]}]
  {:query         [:ui/slid? {:block (prim/get-query Block)}]
   :initial-state {:ui/slid? false :block {:id 1 :name "N"}}
   :ident         (fn [] [:control :demo])}
  (dom/div
    (dom/button {:onClick (fn [] (m/toggle! this :ui/slid?))} "Toggle")
    (ui-motion (clj->js {:style {"x" (spring (if slid? 400 0))}})
      (fn [p]
        (let [x (gobj/get p "x")]
          ; The binding wrapper ensures that internal fulcro bindings are held within the lambda
          (prim/with-parent-context this
            (dom/div :.demo
              (ui-block (prim/computed block {:x x})))))))))

The key is the call to with-parent-context. It causes the enclosed elements to have bindings pulled from the component passed as the first parameter (in this case this). Rendering will work correctly without this wrapper, but interactions (particularly transact!) will not operate correctly without it.

4.9. Colocated CSS

Fulcro includes support for localizing CSS rules to components. It leverages Garden for interpreting component-localized hiccup-style CSS rules.

Before 2.4 you needed to include fulcrologic/fulcro-css as a dependency. As of 2.4, this is no longer the case.

The support make it easy to co-locate and compose localized component CSS with your UI! This leads to all sorts of wonderful and interesting results:

  • Your CSS is name-localized, so your component’s CSS is effectively private

  • You don’t have to go find the CSS file to include it in your application. It’s already in the code.

  • Since it’s code, you can use data manipualtion, variables, and more to create your CSS. It is the ultimate in code reuse and generality.

  • The composition facilities prevent duplication.

  • It becomes much easier to allow themed components with additional tool chains! Variables can be supplied that can simply be modified via code before CSS injection.

  • Google Closure’s minification can be applied to reduce the size of the resulting files during your normal compilation!

  • Server-side rendering can pre-inject CSS rules into the server-side rendered page! No more waiting for your CSS to load for the UI to look right!

4.9.1. Basics

CSS can be co-located on any component. This CSS does not take effect until it is embedded on the page (see Embedding The CSS below). The typical steps for usage are:

  1. Add localized rules to your component via the :css option of defsc or by implementing the fulcro-css.css/CSS protocol’s local-rules. The result of this must be a vector in Garden notation. Any rules included here will be automatically prefixed with the CSSified namespace and component name to ensure name collisions are impossible.

  2. (optional) Add "global" rules. The vector of rules (in the prior step) can use a $ prefix to prevent localization (:$container instead of :.container).

  3. (optional) Add the :css-include option to defsc. This MUST be a vector of components that are used within the render that also supply CSS. This allows the library to compose together your CSS according to what components you use. Pulling the CSS rules from some top-level component will dedupe any inclusions before generating the actual on-DOM CSS.

  4. Use one of many options to get the "munged" classnames for use in your code:

    • (fulcro-css.css/get-classnames Componenet) returns a map of namespaced CSS classes keyed by the simple name you used in your garden rules.

    • The 4th argument to defsc is that same map, on which you can apply destructuring to get the munged class names.

    • Use fulcro.client.localized-dom for DOM rendering, and have it all automatically done for you!

  5. Use the fulcro-css.css/upsert-css or fulcro-css.css/style-element function (or your own style element) to embed the CSS.

A typical file will have the following layout:

(ns my-ui
  (:require [fulcro.client.dom :as dom]
            [fulcro-css.css :as css]
            [fulcro.client.primitives :as prim :refer [defui defsc]]))

; the item binding is destructured as the fourth param. The actual CSS classname
; will be namespaced to the component as my_ui_ListItem__item, but will be available
; as the value :item in css-classnames map parameter, so you can easily
; destructure if and use it in the DOM without having to worry about how it is prefixed.
(defsc ListItem [this {:keys [label] :as props} computed {:keys [item] :as css-classes}]
  {:css [[:.item {:font-weight "bold"}]]}
  (dom/li {:className item} label))

(def ui-list-item (om/factory ListItem {:keyfn :id}))

(defsc ListComponent [this {:keys [id items]} computed {:keys [items-wrapper]}]
  {:css [[:.items-wrapper {:background-color "blue"}]] ; this component's css
   :css-include [ListItem]} ; components whose CSS should be included if this component is included
  (dom/div {:className items-wrapper}
    (dom/h2 (str "List " id))
    (dom/ul (map ui-list-item items))))

(def ui-list (om/factory ListComponent {:keyfn :id}))

(defsc Root [this props computed {:keys [text]}]
  {:css [[:.container {:background-color "red"}]]
   :css-include [ListComponent]}
    (let [the-list {:id 1 :items [{:id 1 :label "A"} {:id 2 :label "B"}]}]
      (dom/div {:className text}
        (ui-list the-list))))

; ...

; Add the CSS from Root as a HEAD style element. If it already exists, replace it. This
; will recursively follow all of the CSS includes *just* for components that Root includes!
(css/upsert-css "my-css" Root)

In the above example, the upsert results in this CSS on the page:

<style id="my-css">
.fulcro-css_cards-ui_Root__container {
  background-color: red;
}

.text {
  color: yellow;
}

.fulcro-css_cards-ui_ListComponent__items-wrapper {
  background-color: blue;
}

.fulcro-css_cards-ui_ListItem__item {
  font-weight: bold;
}
</style>

with a DOM for the UI of:

<div class="text">
  <div class="fulcro-css_cards-ui_ListComponent__items-wrapper">
    <h2>List 1</h2>
    <ul>
      <li class="fulcro-css_cards-ui_ListItem__item">A</li>
      <li class="fulcro-css_cards-ui_ListItem__item">B</li>
    </ul>
  </div>
</div>

Garden’s selectors are supported. These include the CSS combinators and the special & selector. Using the $-prefix will also prevent the selectors from being localized.

  :css [[(garden.selectors/> :.a :$b) {:color "blue"}]]
.namespace_Component__a > .b {
  color: blue;
}

See Garden’s documentation for more details on more complex rules.

4.9.2. Upsert vs. Style Element

There are two ways of placing the CSS for a (group of) components on a page:

(upsert-css ID Component) will pull (recurisvely) the rules of Component, translate them to legal CSS, and then insert them on the body of the page’s DOM at the given ID (overwriting the old style element with that same ID). If you ensure that this upsert happens on hot code reload, then your CSS will update as you edit your code.

Typically, you’ll run upsert-css with your initial mount and in your hot code reload trigger. This means that the computational overhead for the CSS is limited to initial startup.

(style-element Component) will pull (recursively) the rules for Component and return a React style element. This allows you to embed CSS for a sub-tree of components in an "on-demand" fashion, since the element will only render of the component that uses it renders. The problem with this method is that the computational overhead for computing the CSS is moved into your primary rendering. This, however, is quite convenient in situations like devcards where you’d like to easily ensure that the CSS is there, but don’t want to have to worry about conflicting with existing style elements on the top-level page.

The live demo below upserts the co-located CSS from the code itself, but the embedded rules also base the color on the value in an atom (think theme color). At any time you can change your various embedded data on colocated CSS and re-upsert the generated result!

Show/Hide Source
(ns book.demos.component-localized-css
  (:require
    [fulcro-css.css :as css]
    [fulcro.client.primitives :as prim :refer [defsc InitialAppState initial-state]]
    [fulcro.client.dom :as dom]
    [fulcro.client.localized-dom :as ldom]))

(defonce theme-color (atom :blue))

(defsc Child [this {:keys [label]} _ {:keys [thing]}]       ;; css destructuring on 4th argument, or use css/get-classnames
  {; define local rules via garden. Defined names will be auto-localized
   :css [[:.thing {:color @theme-color}]]}
  (dom/div
    (dom/h4 "Using css destructured value of CSS name")
    (dom/div {:className thing} label)
    (dom/h4 "Using automatically localized DOM in fulcro.client.localized-dom")
    (ldom/div :.thing label)))

(def ui-child (prim/factory Child))

(declare change-color)

(defsc Root [this {:keys [ui/react-key]}]
  {; Compose children with local reasoning. Dedupe is automatic if two UI paths cause re-inclusion.
   :css-include [Child]}
  (dom/div
    (dom/button {:onClick (fn [e]
                            ; change the atom, and re-upsert the CSS. Look at the elements in your dev console.
                            ; Figwheel and Closure push SCRIPT tags too, so it may be hard to find on
                            ; initial load. You might try clicking one of these
                            ; to make it easier to find (the STYLE will pop to the bottom).
                            (change-color "blue"))} "Use Blue Theme")
    (dom/button {:onClick (fn [e]
                            (change-color "red"))} "Use Red Theme")
    (ui-child {:label "Hello World"})))

(defn change-color [c]
  (reset! theme-color c)
  (css/upsert-css "demo-css-id" Root))

; Push the real CSS to the DOM via a component. One or more of these could be done to, for example,
; include CSS from different modules or libraries into different style elements.
(css/upsert-css "demo-css-id" Root)

4.9.3. Localized CSS and Server-Side Rendering

The upsert-css and style-element functions are conveniences for CLJS DOM, but they do not work in CLJ.

Instead use (garden/css (fulcro-css.css/get-css Component)) to get a string version of the generated CSS and embed that in the page. Version 2.4.1+ includes fulcro-css.css/raw-css for this expression.

You can use the raw string in generated HTML, or use the server-compatible DOM functions:

(ns app.ui
  (:require
    [fulcro.client.dom :as dom]
    [garden.core :as g]
    [fulcro-css.css :as css]))

(dom/style {:dangerouslySetInnerHTML {:__html (g/css (css/get-css component))}})

4.10. CSS Localized DOM

If you choose to add localized CSS rules to your components, then you will probably also want to use the DOM elements that support it natively. The fulcro.client.localized-dom uses the same more compact notation of dom, but it interprets the CSS keywords in the context of the component! This means that you can use localized CSS without having to destructure the munged names at all.

Instead of writing:

(ns app.ui
  (:require [fulcro.client.dom :as dom]))

(defsc Component [this props comp {:keys [a]}]
  {:css [[:.a {:color :red}]]}
  (dom/div {:className a} "Hi!))

you can instead write (note the different require for DOM):

(ns app.ui
  (:require [fulcro.client.localized-dom :as dom]))

(defsc Component [this props]
  {:css [[:.a {:color :red}]]}
  (dom/div :.a "Hi!))

and avoid the argument destructuring, and JS properties map.

This localization of the keyword does require a small bit of runtime overhead, but inline macro expansion is used wherever possible to make this as performant as possible.

The features include:

  • A class keyword may precede the props. :.c will be localized to the class, whereas :$c will be treated as a top-level (raw) class name.

  • The keyword can contain any number of classes, and one ID: :..c$c2.c3#id

  • Props are optional, but may contain :className or :classes

  • :className is a literal string. Its contents will be combined with the :classes and prefix keywords

  • :classes must be a vector of keywords (or expressions the result in keywords). Nil is allowed in the vector but is ignored.

Some examples:

(div :.a "Hi") ; <div class="namespace_Component__a">Hi</div>
(div :$a "Hi") ; <div class="a">Hi</div>
(div :$a$b "Hi") ; <div class="a b">Hi</div>
(div :$a.b "Hi") ; <div class="a namespace_Component__b">Hi</div>
(div {:classes [(when true :$hide) :.a]} "Hi") ; <div class="hide namespace_Component__a">Hi</div>
(div {:classes [(when false :$hide) :.a]} "Hi") ; <div class="namespace_Component__a">Hi</div>
(div :$c {:className "boo" :classes [:$a]} "Hi") ; <div class="c boo a">Hi</div>

The demo below shows most of these techniques combined with theme and rendering logic:

Show/Hide Source
(ns book.demos.localized-dom
  (:require
    [fulcro-css.css :as css]
    [fulcro.client.mutations :refer [defmutation]]
    [fulcro.client.primitives :as prim :refer [defsc InitialAppState initial-state]]
    [fulcro.client.localized-dom :as dom]))

(defonce theme-color (atom :blue))

(defsc Child [this {:keys [label invisible?]}]
  {:css           [[:.thing {:color @theme-color}]]
   :query         [:id :label :invisible?]
   :initial-state {:id :param/id :invisible? false :label :param/label}
   :ident         [:child/by-id :id]}
  (dom/div :.thing {:classes [(when invisible? :$hide)]} label))

(def ui-child (prim/factory Child))

(declare change-color)

(defmutation toggle-child [{:keys [id]}]
  (action [{:keys [state]}]
    (swap! state update-in [:child/by-id id :invisible?] not)))

(defsc Root [this {:keys [child]}]
  {:css           [[:$hide {:display :none}]]               ; a global CSS rule ".hide"
   :query         [{:child (prim/get-query Child)}]
   :initial-state {:child {:id 1 :label "Hello World"}}
   :css-include   [Child]}
  (dom/div
    (dom/button {:onClick (fn [e] (change-color "blue"))} "Use Blue Theme")
    (dom/button {:onClick (fn [e] (change-color "red"))} "Use Red Theme")
    (dom/button {:onClick (fn [e] (prim/transact! this `[(toggle-child {:id 1})]))} "Toggle visible")
    (ui-child child)))

(defn change-color [c]
  (reset! theme-color c)
  (css/upsert-css "demo-css-id" Root))

; Push the real CSS to the DOM via a component. One or more of these could be done to, for example,
; include CSS from different modules or libraries into different style elements.
(css/upsert-css "demo-css-id" Root)

4.10.1. Localized DOM and Server-Side Rendering

There is a fulcro.client.localized-dom-server namespace that provides the CLJ versions of the DOM functions. In order to write UI in CLJC files you will need to make sure you use a conditional reader tag to include the correct namespace for the correct langauge:

(ns app.ui
  (:require #?(:clj [fulcro.client.localized-dom-server :as dom] :cljs [fulcro.client.localized-dom :as dom]))

... same as before

4.11. Complex Graphical UI Demo: An Image Clip Tool

Fulcro has the start of an image clip tool. Right now it is mainly for demonstration purposes, and is a good example of a complex UI component where two components have to talk to each other and share image data.

You should study the source code (src/main/fulcro/ui/clip_tool.cljs) to get the full details, but here is an overview of the critical facets:

  • ClipTool creates a canvas on which to draw

    • It uses initial state and a query to track the setup (e.g. size, aspect ratio, image to clip)

    • For speed (and because some data is not serializable), it uses component-local state to track current clip region, a javascript Image object and the DOM canvas (via React ref) for rendering, and the current active operation (e.g. dragging a handle).

    • The mouse events are essentially handled as updates to the component local state, which causes a local component render update.

  • PreviewClip is a React component, but not data-driven (no query). Everything is just passed through props.

    • It technically knows nothing of the clip tool.

    • It expects an :image-object and clip data to be passed in…​it just renders it on a canvas.

    • It uses React refs to get the reference to the real DOM canvas for rendering

    • It renders whenever props change

  • A callback on the ClipTool communicates through the common parent’s component local state. The parent will re-render when its state changes, which will in turn force a new set of props to be passed to the preview).

As you can see, the interaction performance is quite good.

Show/Hide Source
(ns book.ui.clip-tool-example
  (:require [cljs.pprint :refer [cl-format]]
            cljsjs.victory
            [fulcro.ui.clip-tool :as ct]
            [fulcro.ui.elements :as ele]
            [fulcro.client.cards :refer [defcard-fulcro]]
            [fulcro.client.mutations :as m :refer [defmutation]]
            [fulcro.client.dom :as dom]
            [fulcro.client.primitives :as prim :refer [defsc]]
            [fulcro.util :as util]
            [fulcro.client :as fc]))

(def minion-image "https://s-media-cache-ak0.pinimg.com/736x/34/c2/f5/34c2f59284fcdff709217e14df2250a0--film-minions-minions-images.jpg")

(defsc Root [this {:keys [ctool]}]
  {:initial-state
          (fn [p] {:ctool (prim/get-initial-state ct/ClipTool {:id        :clipper :aspect-ratio 0.5
                                                               :image-url minion-image})})
   :query [:ctool]}
  (dom/div
    (ct/ui-clip-tool (prim/computed ctool {:onChange (fn [props] (prim/set-state! this props))}))
    (ct/ui-preview-clip (merge (prim/get-state this) {:filename "minions.jpg"
                                                      :width    100 :height 200}))))

4.12. React Higher-Order Components

When using external libraries with Fulcro you’ll often run into the higher-order component pattern. React Higher Order Components (HOC) can be used with Fulcro but due to how Fulcro works internally interop/glue code is needed.

Higher Order Components is a pattern in React similar to the Decorator pattern in object-oriented programming: you wrap one component class with another component to decorate it with extra behaviour.

Some React reusable components provide HOC components to wrap cross cutting logic needed for the wrapped components to work.

For example google-maps-react provides GoogleApiWrapper HOC that handles dynamic loading and initialisation of Google Maps Javascript library so it doesn’t have to be handled manually in every place where Google Maps React component (like Map or Marker) is used. In this particular example, GoogleApiWrapper creates a wrapper component class that behaves in the following way: - it will display a placeholder "Loading" component and trigger Google Maps script loading and initialisation - when the script loading and initialisation is complete it will replace "Loading" placeholder with the wrapped component

4.12.1. Fulcro and React HOC

So what’s the issue with using React HOC in Fulcro Interop: * Fulcro will embed React JS components (ones created by HOC) * React JS HOC will wrap Fulcro components

In the first case React JS components (like these from google-maps-react) expect props to be plain JavaScript objects, not ClojureScript maps. Fulcro components pass ClojureScript to nested components thus props need to be converted. This part is described in Fulcro Book’s chapter on Factory Functions for JS React Components. All we need to do is to have a factory function that will do the conversion. For example:

(ns hoc-example
  (:require
    [fulcro.client.dom :as dom]
    [fulcro.client.primitives :as prim :refer [defsc]]
    ["google-maps-react" :refer [GoogleApiWrapper Map Marker]]))

(defn factory-apply [js-component-class]
  (fn [props & children]
    (apply js/React.createElement
           js-component-class
           (dom/convert-props props) ;; convert-props makes sure that props passed to React.createElement are plain JS object
           children)))

(def ui-google-map (factory-apply Map)) ;; Fulcro wrapper factory for google-maps-react's Map component
(def ui-map-marker (factory-apply Marker)) ;; Another wrapper factory for Marker component

In the second scenario we will have React JS component passing plain JS object props to our Fulcro component and we need to add a layer that will do JS → Cljs props conversion.

Let’s create our sample Fulcro LocationView component. It queries for view title, location’s lat and lng and google which is the Google Maps API object required by google-maps-react components. It’s managed by google-maps-react HOC wrapper and provided in props passed in our wrapped component.

(defsc LocationView [this {:keys [title lat lng google]}]
  {:query [:lat :lng :google]}
  (dom/div
    (dom/h1 title)
    (dom/div {:style {:width "250px" :height "250px"}}
      (ui-google-map {:google google
                      :zoom 15
                      :initialCenter {:lat lat :lng lng}
                      :style {:width "90%" :height "90%"}}
        (ui-map-marker {:position {:lat lat :lng lng}})))))

Now we need to create a factory for our LocationView. It will need to sneak props as Cljs map so it’s available for our wrapped LocationView component:

(defn hoc-wrapper-factory-apply [component-class]
  (fn [props & children]
    (apply js/React.createElement
           component-class
           #js {:hocFulcroCljPropsPassthrough props}
           children)))

We also need LocationView factory that will get JS props received from HOC and will recover our Cljs map props enhancing it also with google object provided by HOC. We use "function as child" React pattern.

;; Plain Fulcro factory that will be used in our interop layer.
;; It won't be used directly in our client UI code.
(def ui-location-view-wrapped (prim/factory LocationView)

(defn ui-location-view-interop [js-props]
  (let [fulcro-clj-props (.-hocFulcroCljPropsPassthrough js-props) ;; unwrapping Fulcro Cljs props wrapped by hoc-factory-apply
        google (.-google js-props) ;; we need to extract google object passed by google-maps-react HOC
        props (assoc fulcro-clj-props :google google)] ;; final version of cljs props that has a proper format for our LocationView component
    (ui-location-view-wrapped props)))

Now we can finally create a factory for LocationView component that will be used in our client UI code:

(def ui-location-view
  ;; GoogleApiWrapper is a function returning HOC with specified configuration parameters like API key
  ;; Notice that it's a plain JS function thus it requires it's options to be plain JS map thus #js usage
  (let [hoc (GoogleApiWrapper #js {:apiKey "AIzaSyDAiFHA9fwVjW83jUFjgL43D_KP9EFcIfE"})
        WrappedLocationView (hoc ui-location-view-interop)]
    (hoc-wrapper-factory-apply WrappedLocationView)))

Now we can use our LocationView in our views:

(ui-location-view {:lat 37.778519 :lng -122.405640})

4.12.2. Reusable HOC factory

utils-hoc namespace presented below provides a reusable hoc-factory that can be used to handle all the boilerplate code and interop gluing. It supports :extra-props-fn in the opts argument that can be used to customize the final props passed to the wrapped component. The code below shows its usage where google value from js-props (injected by google-maps-react HOC wrapper) needs to be propagated under :google entry in Cljs props passed to the wrapped LocationView component.

(ns utils.hoc
  (:require
    goog.object
    [fulcro.client.primitives :as prim]))

(defn hoc-factory
  "Creates a factory for HOC wrapped component class.
  extra-props-fn allows for additional customization of props passed to the wrapped component. It will be provided
  plain js-props and cljs-props unwrapped from js-props and must return cljs-props with modified contents if needed."
  ([hoc-wrapper-fn wrapped-component-class]
   (hoc-factory hoc-wrapper-fn wrapped-component-class nil))
  ([hoc-wrapper-fn wrapped-component-class {:keys [extra-props-fn]}]
   (let [cljs-props-key (name (gensym "fulcro-cljs-props"))
         wrapped-component-factory (prim/factory wrapped-component-class)
         js->clj-props-interop (fn js->clj-props-interop [js-props]
                                 (let [clj-props (goog.object/get js-props cljs-props-key)
                                       props (if extra-props-fn
                                               (extra-props-fn js-props clj-props)
                                               clj-props)]
                                   (wrapped-component-factory props)))
         hoc-wrapped-component-class (hoc-wrapper-fn js->clj-props-interop)]
     (fn [props & children]
       (apply js/React.createElement
         hoc-wrapped-component-class
         (js-obj cljs-props-key props)
         children)))))

(comment
  "Example usage"

  (defsc LocationView [this {:keys [lat lon google]}]
    {:query [:lat :lon :google]}
    (dom/div {:style {:width "250px" :height "250px"}}
      (ui-google-map {:zoom 15
                      :google google
                      :initialCenter {:lat lat :lng lon}
                      :style {:width "90%" :height "90%"}}
      (ui-map-marker {:position {:lat lat :lng lon}}))))

  (def google-maps-hoc (gmaps/google-api-wrapper #js {:apiKey "AIzaSyDAiFHA9fwVjW83jUFjgL43D_KP9EFcIfE"}))

  (def ui-location-view
    (hoc-factory
      google-maps-hoc
      LocationView
      {:extra-props-fn (fn propagate-google-api [js-props cljs-props]
                         (assoc cljs-props :google (goog.object/get js-props "google")))}))

  (ui-location-view {:lat 37.778519 :lng -122.405640}))

4.13. The defui Macro (Fulcro 1.x. Legacy support in 2.x)

The defui macro generates a React component. It does the same thing as the defsc macro, but looks more like a defrecord and is a bit more OO in style. It does not error-check your work, nor does it allow you to destructure incoming data over the body or options; however, it is syntax-comptible with Om Next so if you’re porting from that library it can be useful.

It is 100% compatible with the React ecosystem. The macro is intended to look a bit like a class declaration, and borrows generation notation style from defrecord. There is no minimum required list of methods (e.g. you don’t even have to define render). This latter fact is useful for cases where you want a component for server queries and database normalization, but not for rendering.

4.13.1. React (Object) methods

defui is aware of the following React-centric methods, which you can override:

(defui MyComponent
  Object
  (initLocalState [this] ...)
  (shouldComponentUpdate [this next-props next-state] ...)
  (componentWillReceiveProps [this next-props] ...)
  (componentWillUpdate [this next-props next-state] ...)
  (componentDidUpdate [this prev-props prev-state] ...)
  (componentWillMount [this] ...)
  (componentDidMount [this] ...)
  (componentWillUnmount [this] ...)
  (render [this] ...))

See React Lifecycle Examples for some specific examples, and the React documentation for a complete description of each of these.

Note
Fulcro does override shouldComponentUpdate to short-circuit renders of a component whose props have not changed. You generally do not want to change this to make it render more frequently; however, when using Fulcro with libraries like D3 that want to "own" the portion of the DOM they render you may need to make it so that React never updates the component once mounted (by returning false always). The Developer’s Guide shows an example of this in the UI section.

4.13.2. The static Protocol Qualifier

defui supports implementations of protocols in a static context. It basically means that you’d like the methods you’re defining to go on the class (instead of instance), but conform to the given protocol. There is no Java analogue for this, but in Javascript the classes themselves are open.

Warning
Since there is no JVM equivalent of implementing static methods, a hack is used internally where the protocol methods are placed in metadata on the resulting symbol. This is the reason functions like get-initial-state exist. Calling the protocol (e.g. initial-state) in Javascript will work, but if you try that when doing server-side rendering on the JVM, it will blow up.

4.13.3. IQuery and Ident

There are two core protocols for supporting a component’s data in the graph database. They work in tandem to find data in the database for the component, and also to take data (e.g. from a server response or initial state) and normalize it into the database.

Both of these protocols must be declared static. The reason for this is initial normalization and query: The system has to be able to ask components about their ident and query generation in order to turn a tree of data into a normalized database.

queryidentoperation

Queries must be composed towards the root component (so you end up with a UI query that can pull the entire tree of data for the UI).

(defui ListItem
  static prim/IQuery
  (query [this] [:db/id :item/label])
  static prim/Ident
  (ident [this props] [:list-item/by-id (:db/id props)])
  ...)

(defui List
  static prim/IQuery
  (query [this] [:db/id {:list/items (prim/get-query ListItem)}])
  static prim/Ident
  (ident [this props] [:list/by-id (:db/id props)])
  ...)

;; queries compose up to root
Notes on the IQuery Protocol

Even though the method itself is declared statically, there are some interesting things about the query method:

  • Once mounted, a component can have a dynamic query. This means calling (prim/get-query this) will return either the static query, or whatever has been set on that component via (prim/set-query! …​).

  • The get-query accessor method not only helps with server-side invocation, it annotates the query with metadata that includes the component info. This is what makes normalization work.

Some rules about the query itself:

  • A query must not be stolen from another component (even if it seems more DRY):

    (defui PersonView1
      static prim/IQuery
      (query [this] (prim/get-query PersonView2)) ;; WRONG!!!!

    This is wrong because the query will end up annotated with PersonView2’s metadata. Never use the return value of `get-query as the return value for your own query.

  • The query will be structured with joins to follow the UI tree. In this manner the render and query follow form. If you query for some subcomponent’s data, then you should pass that data to that component’s factory function for rendering.

Notes on the Ident Protocol

The ident of a component is often needed in mutations, since you’re always manipulating the graph. To avoid typos, it is generally recommended that you write a function like this:

(defn person-ident [id-or-props]
  (if (map? id-or-props)
    [:person/by-id (:db/id id-or-props)]
    [:person/by-id id-or-props]))

and use that in both your component’s ident implementation and all of your mutations:

(defui Person
  static prim/Ident
  (ident [this props] (person-ident props)))

...

(defmutation change-name [{:keys [id name]}]
  (action [{:keys [state]}]
    (let [name-path (conj (person-ident id) :person/name)]
      (swap! state assoc-in name-path name))))

4.13.4. Om Next Compatibility

Fulcro’s defui is identical in syntax to Om Next’s defui. Porting Om Next UI code to Fulcro is a simple matter of changing namespaces.

5. The Query and Mutation Language

Before reading this chapter you should make sure you’ve read The Graph Database Section. It details the low-level format of the application state, and talks about general details that are referenced in this chapter.

In Fulcro all data is pulled from the database using a notation that is a subset of Datomic’s pull query syntax. Since the query is a graph walk it is a relative notation: it must start at some specific spot, but that spot is not always named in the query itself. On the client side the starting point is usually the root node of your database. Thus, a complete query from the Root UI component will be a graph query that can start at the root node of the database.

However, you’ll note that any query fragment is implied to be relative to where we are in the walk of the graph database. This is important to understand: no component’s query can just be grabbed and run against the database as-is. Then again, if you know the ident of a component, then you can start at that table entry in the database and go from there.

The mutation language is a data representation of the abstract actions you’d like to take on the data model. It is intended to be network agnostic: The UI need not be aware that a given mutation does local-only modifications and/or remote operations against any number of remote servers. As such, the mutations, like queries, are simply data. Data that can be interpreted by local logic, or data that can be sent over the wire to be interpreted by a server.

Queries can either be a vector or a map of vectors. The former is a regular component query, and the latter is known as a union query. Union queries are useful when you’re walking a graph edge and the target could be one of many different kinds of nodes, so you’re not sure which query to use until you actually are walking the graph.

5.1. Properties

The simplest thing to query are properties "right here" in the relative node of the graph. Properties are queried by a simple keyword. Their values can be any scalar data value that is serializable in EDN.

The query

[:a :b]

is asking for the properties known as :a and :b at the "current node" in the graph traversal.

5.2. Joins

A join represents a traversal of an edge to another node in the graph. The notation is a map with a single key (the local key on the current node that holds the "pointer" to another node) whose single value is the query for the remainder of the graph walk:

[{:children (prim/get-query Child)}]

The query itself cannot specify that this is a to-one or to-many join. The data in the database graph itself determines the arity when the query is being run. Basically, if walking the join property leads to a vector of links, it is to-many. If it leads to a single link, then it is to-one. Rendering the data is going to have the same concern so the arity of the relation more strongly affects the rendering code.

Joins should always use get-query to get the next component in the graph. This annotates (with metadata) the sub-query so that normalization can work correctly.

5.3. Unions

Unions represent a map of queries, only one of which applies at a given graph edge. This is a form of dynamic query that adjusts based on the actual linkage of data. Unions cannot stand alone. They are meant to select one of many possible alternate queries when a link (to-one or to-many join) in the graph is reached. Unions are always used in tandem with a join, and can therefore not be used on root-level components. The union query itself is a map of the possible queries:

(defsc PersonPlaceOrThingUnion [this props]
  ; lambda form required for unions (a limitation of the error checking routines in defsc)
  {:query (fn [] {:person/by-id (prim/get-query Person)
                  :place/by-id (prim/get-query Place)
                  :thing/by-id (prim/get-query Thing)})}
  ...)

and such a query must be joined by a parent component. Therefore, you’ll always end up with something like this:

(defsc Parent [this props]
  {:query [{:person-place-or-thing (prim/get-query PersonPlaceOrThingUnion)}]})

Union queries take a little getting used to because there are a number of rules to follow when using them in order for everything to work correctly (normalization, queries, and rendering).

Here is what a graph database might look like for the above query assuming we started at Parent:

{ :peron-place-or-thing [:place/by-id 3]
  :place/by-id { 3 { :id 3 :location "New York" }}}

The query would start at the root. When it saw the join it would detect a union. The union would be resolved by looking at the first element of the ident in the database (in this case :place from [:place 3]). That keyword would then be used to select the query from the subcomponent union (in this example, (prim/get-query Place)).

Processing of the query then continues as normal as if the join was just on Place.

A to-many linkage works just as well:

{ :peron-place-or-thing [[:person/by-id 1] [:place/by-id 3]]
  :person/by-id { 1 { :id 1 :name "Julie" }}
  :place/by-id { 3 { :id 3 :location "New York" }}}

and now you have a mixed to-many relationship where the correct sub-query will be used for each item in turn.

Normalization of unions requires that the union component itself have an ident function that can properly generate idents for all of the possible kinds of things that could be found. Often this means that you’ll need to encode some kind of type indicator in the data itself.

Say you had this incoming tree of data:

{:person-place-or-thing [ {:id 1 :name "Joe"} {:id 3 :location "New York"} ]}

In order to normalize this correctly we need to end up with the correct person and place idents. The resulting ident function might look like this:

(defsc PersonPlaceOrThingUnion [this props]
  {:ident (fn []
    (cond
      (contains? props :name) [:person/by-id (:id props)]
      (contains? props :location) [:place/by-id (:id props)]
      :else [:thing/by-id (:id props)]))}
  ...)

Often it is easier to just include a :type field so that ident can look up both the type and id.

Rendering the correct thing in the UI of the union component has the same concern: you must detect what kind of data (among the options) that you actually receive, and pass that on to the correct child factory (e.g. ui-person, ui-place, or ui-thing. This is most commmonly done with a simple case statement.

5.3.1. Demo – Using Unions as a UI Type Selector

The fulcro.client.routing/defrouter macro emits a union component that can be switched to point at any kind of component that it knows about. The support for parameterized routers in the routing tree makes it possible to very easily reuse the UI router as a component that can show one of many screens in the same location.

This is particularly useful when you have a list of items that have varying types, and you’d like to, for example, show the list on one side of the screen and the detail on the other.

To write such a thing one would follow these steps:

  1. Create one component for each item type that represents how it will look in the list.

  2. Create one component for each item type that represents the fine detail view for that item.

  3. Join (1) together into a union component and use it in a component that shows them as a list. In other words the union will represent a to-many edge in your graph. Remember that unions cannot stand alone, so there will be a union component (to switch the UI) and a list component to iterate through the items.

  4. Combine the detail components from (2) into a defrouter (e.g. named :detail-router).

  5. Create a routing tree that includes the :detail-router, and parameterize both elements of the target ident (kind and id)

  6. Hook a click event from the items to a route-to mutation, and send route parameters for the kind and id.

The output of this defrouter (macro):

(defrouter ItemDetail :detail-router
  (ident [this props] (item-ident props))
  :person/by-id PersonDetail
  :place/by-id PlaceDetail
  :thing/by-id ThingDetail)

is roughly this (cleaned up from macro output):

(defsc ItemDetail-Union [this props]
  {:initial-state [clz params] (prim/get-initial-state PersonDetail params)) ;; defaults to the first one listed
   :ident [this props] (item-ident props))
   :query (fn [] {:person/by-id (prim/get-query PersonDetail),
                  :place/by-id (prim/get-query PlaceDetail),
                  :thing/by-id (prim/get-query ThingDetail)}}
   (let [page (first (prim/get-ident this))]
    (case page
     :person/by-id ((prim/factory PersonDetail) (prim/props this))
     :place/by-id ((prim/factory PlaceDetail) (prim/props this))
     :thing/by-id ((prim/factory ThingDetail) (prim/props this))
     (dom/div (str "Cannot route: Unknown Screen " page))))

(defsc ItemDetail [this props]
  {:initial-state (fn [params] {:fulcro.client.routing/id :detail-router
                                :fulcro.client.routing/current-route (prim/get-initial-state ItemDetail-Union params)})
   :ident (fn [] [:fulcro.client.routing.routers/by-id :detail-router])
   :query [this] [:fulcro.client.routing/id {:fulcro.client.routing/current-route (prim/get-query ItemDetail-Union)}]}
  (let [computed (prim/get-computed this)
        props (:fulcro.client.routing/current-route (prim/props this))
        props-with-computed (prim/computed props computed)]
   ((prim/factory ItemDetail-Union) props-with-computed)))))

You can see that this just defines a union whose "selection" is controlled by the :current-route property!

Here is a complete working example that uses this to make a UI around displaying things of various types:

Show/Hide Source
(ns book.queries.union-example-1
  (:require [fulcro.client.dom :as dom]
            [fulcro.client.routing :as r :refer [defrouter]]
            [fulcro.client.primitives :as prim :refer [defsc]]
            [fulcro.client :as fc]
            [fulcro.ui.bootstrap3 :as b]
            [fulcro.ui.elements :as ele]
            [fulcro.client.cards :refer [defcard-fulcro]]))

(defn item-ident
  "Generate an ident from a person, place, or thing."
  [props] [(:kind props) (:db/id props)])

(defn item-key
  "Generate a distinct react key for a person, place, or thing"
  [props] (str (:kind props) "-" (:db/id props)))

(defn make-person [id n] {:db/id id :kind :person/by-id :person/name n})
(defn make-place [id n] {:db/id id :kind :place/by-id :place/name n})
(defn make-thing [id n] {:db/id id :kind :thing/by-id :thing/label n})

(defsc PersonDetail [this {:keys [db/id person/name] :as props}]
  ; defrouter expects there to be an initial state for each possible target. We'll cause this to be a "no selection"
  ; state so that the detail screen that starts out will show "Nothing selected". We initialize all three in case
  ; we later re-order them in the defrouter.
  {:ident         (fn [] (item-ident props))
   :query         [:kind :db/id :person/name]
   :initial-state {:db/id :no-selection :kind :person/by-id}}
  (dom/div
    (if (= id :no-selection)
      "Nothing selected"
      (str "Details about person " name))))

(defsc PlaceDetail [this {:keys [db/id place/name] :as props}]
  {:ident         (fn [] (item-ident props))
   :query         [:kind :db/id :place/name]
   :initial-state {:db/id :no-selection :kind :place/by-id}}
  (dom/div
    (if (= id :no-selection)
      "Nothing selected"
      (str "Details about place " name))))

(defsc ThingDetail [this {:keys [db/id thing/label] :as props}]
  {:ident         (fn [] (item-ident props))
   :query         [:kind :db/id :thing/label]
   :initial-state {:db/id :no-selection :kind :thing/by-id}}
  (dom/div
    (if (= id :no-selection)
      "Nothing selected"
      (str "Details about thing " label))))

(defsc PersonListItem [this
                       {:keys [db/id person/name] :as props}
                       {:keys [onSelect] :as computed}]
  {:ident (fn [] (item-ident props))
   :query [:kind :db/id :person/name]}
  (dom/li {:onClick #(onSelect (item-ident props))}
    (dom/a {:href "javascript:void(0)"} (str "Person " id " " name))))

(def ui-person (prim/factory PersonListItem {:keyfn item-key}))

(defsc PlaceListItem [this {:keys [db/id place/name] :as props} {:keys [onSelect] :as computed}]
  {:ident (fn [] (item-ident props))
   :query [:kind :db/id :place/name]}
  (dom/li {:onClick #(onSelect (item-ident props))}
    (dom/a {:href "javascript:void(0)"} (str "Place " id " : " name))))

(def ui-place (prim/factory PlaceListItem {:keyfn item-key}))

(defsc ThingListItem [this {:keys [db/id thing/label] :as props} {:keys [onSelect] :as computed}]
  {:ident (fn [] (item-ident props))
   :query [:kind :db/id :thing/label]}
  (dom/li {:onClick #(onSelect (item-ident props))}
    (dom/a {:href "javascript:void(0)"} (str "Thing " id " : " label))))

(def ui-thing (prim/factory ThingListItem item-key))

(defrouter ItemDetail :detail-router
  (ident [this props] (item-ident props))
  :person/by-id PersonDetail
  :place/by-id PlaceDetail
  :thing/by-id ThingDetail)

(def ui-item-detail (prim/factory ItemDetail))

(defsc ItemUnion [this {:keys [kind] :as props}]
  {:ident (fn [] (item-ident props))
   :query (fn [] {:person/by-id (prim/get-query PersonListItem)
                  :place/by-id  (prim/get-query PlaceListItem)
                  :thing/by-id  (prim/get-query ThingListItem)})}
  (case kind
    :person/by-id (ui-person props)
    :place/by-id (ui-place props)
    :thing/by-id (ui-thing props)))

(def ui-item-union (prim/factory ItemUnion {:keyfn item-key}))

(defsc ItemList [this {:keys [items]} {:keys [onSelect]}]
  {
   :initial-state (fn [p]
                    ; These would normally be loaded...but for demo purposes we just hand code a few
                    {:items [(make-person 1 "Tony")
                             (make-thing 2 "Toaster")
                             (make-place 3 "New York")
                             (make-person 4 "Sally")
                             (make-thing 5 "Pillow")
                             (make-place 6 "Canada")]})
   :ident         (fn [] [:lists/by-id :singleton])
   :query         [{:items (prim/get-query ItemUnion)}]}
  (dom/ul
    (map (fn [i] (ui-item-union (prim/computed i {:onSelect onSelect}))) items)))

(def ui-item-list (prim/factory ItemList))

(defsc Root [this {:keys [item-list item-detail]}]
  {:query         [{:item-list (prim/get-query ItemList)}
                   {:item-detail (prim/get-query ItemDetail)}]
   :initial-state (fn [p] (merge
                            (r/routing-tree
                              (r/make-route :detail [(r/router-instruction :detail-router [:param/kind :param/id])]))
                            {:item-list   (prim/get-initial-state ItemList nil)
                             :item-detail (prim/get-initial-state ItemDetail nil)}))}
  (let [; This is the only thing to do: Route the to the detail screen with the given route params!
        showDetail (fn [[kind id]]
                     (prim/transact! this `[(r/route-to {:handler :detail :route-params {:kind ~kind :id ~id}})]))]
    ; devcards, embed in iframe so we can use bootstrap css easily
    (ele/ui-iframe {:frameBorder 0 :height "300px" :width "100%"}
      (dom/div {:key "example-frame-key"}
        (dom/style ".boxed {border: 1px solid black}")
        (dom/link {:rel "stylesheet" :href "bootstrap-3.3.7/css/bootstrap.min.css"})
        (b/container-fluid {}
          (b/row {}
            (b/col {:xs 6} "Items")
            (b/col {:xs 6} "Detail"))
          (b/row {}
            (b/col {:xs 6} (ui-item-list (prim/computed item-list {:onSelect showDetail})))
            (b/col {:xs 6} (ui-item-detail item-detail))))))))

5.4. Mutations

Mutations are also just data, as we mentioned earlier. However, they are intended to look like single- argument function calls where the single argument is a map of parameters:

[(do-something)]

The main concern is that this expression, in normal Clojure, will be evaluated because it contains a raw list. In order to keep it data, one must quote expressions with mutations. Of course you may use syntax quoting or literal quoting. Usually we recommend namespacing your mutations (with defmutation) and then using syntax quoting to get reasonably short expressions:

(ns app.mutations)

(defmutation do-something [params] ...)
(ns app.ui
  (:require [app.mutations :as am]))

...
   (prim/transact! this `[(am/do-something {})])

We talked about syntax quoting transactions in the Getting Started chapter. You may want to review that or just read more online about Clojure syntax quoting.

The parameter map on mutations is optional, but recommended, if you’re using IDE support since the IDE will always see mutations as if they were function calls with an arity of one.

5.5. Parameters

Most of the query elements also support a parameter map. In Fulcro these are mainly useful when sending a query to the server, and it is rare you will write such a query "by hand". However, for completeness you should know what these look like. Basically, you just surround the property or join with parentheses, and add a map as parameters. This is just like mutations, except instead of a symbol as the first element of the list it is either a keyword (prop) or a map (join).

Thus a property can be parameterized:

[(:prop {:x 1})]

This would cause, for example, a server’s query processing to see {:x 1} in the params when handling the read for :prop.

A join is similarly parameterized:

[({:child (prim/get-query Child)} {:x 1})]

with the same kind of effect.

Note
The plain list has the same requirement as for mutations: quoting. Generally syntax quoting is again the best choice, since you’ll often need unquoting. For example, the join example above would actually be written in code as:
  ...
  (query [this] `[({:child ~(prim/get-query Child)} {:x 1})])
  ...

to avoid trying to use the map as a function for execution, yet allowing the nested get-query to run and embed the proper subquery.

5.6. Queries on Idents

Idents are valid in queries as a plain prop or a join. When used alone (not in a join) this will pull a table entry from the database without normalizing it or following any subquery:

[ [:person/by-id 1] ]

results in something like this:

(defsc Phone [this props]
  {:query [:id :phone/number]
   :ident [:phone/by-id :id]}
  ...)

(defsc Person [this props]
  {:query [:id {:person/phone (prim/get-query Phone)}]
   :ident [:person/by-id :id]}
  ...)

(defsc X [this props]
  {:query [ [:person/by-id 1] ] } ; query just for the ident
  (let [person (get props [:person/by-id 1]) ; NOT get-in. The key of the result is an ident
        ...
        ; person contains {:id 1 :person/phone [:phone/by-id 4]}

This is not typically what you want because you’d typically want it to follow the graph links (resolving phone number).

Therefore these kinds of queries are normally done via a join:

(defsc X [this props]
  {:query [{[:person/by-id 1] (prim/get-query Person)}]}
  (let [person (get props [:person/by-id 1])
        ...
        ; person contains {:id 1 :person/phone {:phone/id 4 :phone/number "555-1212"}}

This has the effect of "re-rooting" the graph walk at that ident’s table entry and continuing from there for the rest of the subtree. In fact this is how Fulcro’s ident-based rendering optimization works.

There are times when you want to start "back at the root" node. This is useful for pulling data that has a singleton representation in the root node itself. For example, the current UI locale or currently logged-in user. There is a special notation for this the looks like an ident without an ID:

[ [:ui/locale '_] ]

This component query would result in :ui/locale in your props (not an ident) with a value that came from the overall root node of the database. Of course, denormalization just requires you use a join:

[ {[:current-user '_] (prim/get-query Person)} ]

would pull :current-user into the component’s props with a continued walk of the graph. In other words this is just like the ident join, except the special symbol _ indicates there is only one of them and it is in the root node.

Components can query for whatever they want, and sometimes it is useful to write components that only query for things "elsewhere" in the database:

(defsc LocaleSwitcher [this {:keys [ui/locale]}]
  {:query [[:ui/locale '_]]}
  (dom/div ...))

When the database is constructed for such components they will have no state of their own.

Sadly, even if you compose it into your UI properly it may not receive any data:

(defsc Root [this {:keys [locale-selector]}]
  {:query [{:locale-selector (prim/get-query LocaleSelector)}]}
  (ui-locale-selector locale-selector)) ; data comes back nil!!!

The problem is that the query engine walks the database and query in tandem. When it sees a join (:locale-selector in this case) it goes looking for an entry in the database at the current location (root node in this case) to process the subquery against. If it finds an ident it follows it and processes the subquery. If it is a map it uses that to fulfill the subquery. If it is a vector then it processes the subquery against every entry. But if it is missing then it stops.

The fix is simple: make sure the component has a presence in the database, even if empty:

(defsc LocaleSwitcher [this {:keys [ui/locale]}]
  {:query [[:ui/locale '_]]
   :initial-state {}} ; empty map
  (dom/div ...))

(defsc Root [this {:keys [locale-selector]}]
  {:query [{:locale-selector (prim/get-query LocaleSelector)}]
   :initial-state (fn [params] {:locale-selector (prim/get-initial-state LocaleSwitcher)})} ; empty map from child
  (ui-locale-selector locale-selector)) ; link query data is found/passed

5.8.1. Using Shared State

An alternative to ident and link queries is shared state. The first thing to note is that it is not a complete replacement for link queries. It is a low-level feature that is meant for two basic scenarios:

  • Data that needs to be visible to all components, but never changes once the app is mounted.

  • Data that is derived from the UI props (from root) or globals, but only updates on root-level renders (not component-local updates).

The first use-case might be handy if you pass some data to your mount through the HTML page itself. The latter is useful for data that affects everything in your application, such as the current user.

The primary thing to remember is that components that look at shared state will not see updates unless a root render occurs with those updates. This typically means calling prim/force-root-render!.

Say we wanted all components to be able to see :pi (a constant) and :current-user (a value from the database). We could declare this as follows:

(fc/new-fulcro-client
  :reconciler-options {:shared {:pi 3.14} ; never changes
                       :shared-fn #(select-keys % :current-user)} ; updates on root render

Now any component can access these as follows:

(defsc C [this props]
  (let [{:keys [pi current-user]} (prim/shared this)]
    ; current-user will be DEnormalized...it comes from the root props (Root must query for it still)
    ...))
Warning
Remember that this is not equivalent to a link query for [:current-user '_]. There are two differences. The first is that pulling :current-user still requires that your root component query for it (or it won’t even be in the props). Second, the shared value will not visibly change until a root render happens, where link queries can refresh locally with a component. The final difference is that if you use data in your shared-fn that is derived from anything other than the state database then it will not work correctly in the history support viewer.

5.9. Recursive Queries

Fulcro’s query syntax includes support for recursive queries. Recursion is always expressed on a join, and it always means that the recursive item has the same type as the component you’re on. There are two notations for this: …​ and a number. The former means "recurse until there are no more links (circular detection is included to prevent infinite loops)", and the other is the recursion limit:

Note
At the time of this writing you must use the lamba mode of defsc for queries that include recursion.

The following demo (with source) demonstrates the core basics of recursion:

Show/Hide Source
(ns book.queries.recursive-demo-1
  (:require [fulcro.client.primitives :as prim :refer [defsc]]
            [fulcro.client.dom :as dom]))

(defn make-person
  "Make a person data map with optional children."
  [id name children]
  (cond-> {:db/id id :person/name name}
    children (assoc :person/children children)))

(declare ui-person)

; The ... in the query means there will be children of the same type, of arbitrary depth
; it is equivalent to (prim/get-query Person), but calling get query on yourself would
; lead to infinite compiler recursion.
(defsc Person [this {:keys [:person/name :person/children]}]
  {:query         (fn [] [:db/id :person/name {:person/children '...}])
   :initial-state (fn [p]
                    (make-person 1 "Joe"
                      [(make-person 2 "Suzy" [])
                       (make-person 3 "Billy" [])
                       (make-person 4 "Rae"
                         [(make-person 5 "Ian"
                            [(make-person 6 "Zoe" [])])])]))
   :ident         [:person/by-id :db/id]}
  (dom/div
    (dom/h4 name)
    (when (seq children)
      (dom/div
        (dom/ul
          (map (fn [p]
                 (ui-person p))
            children))))))

(def ui-person (prim/factory Person {:keyfn :db/id}))

(defsc Root [this {:keys [person-of-interest]}]
  {:initial-state {:person-of-interest {}}
   :query         [{:person-of-interest (prim/get-query Person)}]}
  (dom/div
    (ui-person person-of-interest)))

5.9.1. Circular Recursion

It is perfectly legal to include recursion in your graph, and it is equally fine to query for it. The query engine will automatically stop if a loop is detected.

However, this is not the whole story. You see, components can be updated in a relative fasion when all optimizations are enabled. This means that a refresh could happen anywhere in the (recursive) UI, and the query would run until it detects the loop again. This can lead to funny-looking results.

The demo below lets you modify people and their spouse (a circular relation). Try it out and you’ll see that something isn’t quite right (try making Sally older):

Show/Hide Source
(ns book.queries.recursive-demo-2
  (:require [fulcro.client.primitives :as prim :refer [defsc]]
            [fulcro.client.mutations :refer [defmutation]]
            [fulcro.client.dom :as dom]))

(declare ui-person)

(defmutation make-older [{:keys [id]}]
  (action [{:keys [state]}]
    (swap! state update-in [:person/by-id id :person/age] inc)))

(defsc Person [this {:keys [db/id person/name person/spouse person/age]}]
  {:query         (fn [] [:db/id :person/name :person/age {:person/spouse 1}]) ; force limit the depth
   :initial-state (fn [p]
                    ; this does look screwy...you can nest the same map in the recursive position,
                    ; and it'll just merge into the one that was previously normalized during normalization.
                    ; You need to do this or you won't get the loop in the database.
                    {:db/id         1
                     :person/name   "Joe"
                     :person/age    20
                     :person/spouse {:db/id         2
                                     :person/name   "Sally"
                                     :person/age    22
                                     :person/spouse {:db/id 1 :person/name "Joe"}}})
   :ident         [:person/by-id :db/id]}
  (dom/div
    (dom/div "Name:" name)
    (dom/div "Age:" age
      (dom/button {:onClick
                   #(prim/transact! this `[(make-older {:id ~id})])} "Make Older"))
    (when spouse
      (dom/ul
        (dom/div "Spouse:" (ui-person spouse))))))

(def ui-person (prim/factory Person {:keyfn :db/id}))

(defsc Root [this {:keys [person-of-interest]}]
  {:initial-state {:person-of-interest {}}
   :query         [{:person-of-interest (prim/get-query Person)}]}
  (dom/div
    (ui-person person-of-interest)))

The problem is that when you touch Sally the UI refresh updates just that component; however, that component has a recursive query of depth 1, so it ends up returning Joe as her spouse! This is technically correct, but almost certainly isn’t what you want!

The fix is equally simple: calculate depth and pass it to the child. Use the calculated depth to prevent the extra rendering when local refresh gives you data you don’t need. The demo below has this fix.

Show/Hide Source
(ns book.queries.recursive-demo-3
  (:require [fulcro.client.primitives :as prim :refer [defsc]]
            [fulcro.client.mutations :refer [defmutation]]
            [fulcro.client.dom :as dom]))

(declare ui-person)

(defmutation make-older [{:keys [id]}]
  (action [{:keys [state]}]
    (swap! state update-in [:person/by-id id :person/age] inc)))

; We use computed to track the depth. Targeted refreshes will retain the computed they got on
; the most recent render. This allows us to detect how deep we are.
(defsc Person [this
               {:keys [db/id person/name person/spouse person/age]} ; props
               {:keys [render-depth] :or {render-depth 0}}] ; computed
  {:query         (fn [] [:db/id :person/name :person/age {:person/spouse 1}]) ; force limit the depth
   :initial-state (fn [p]
                    {:db/id         1 :person/name "Joe" :person/age 20
                     :person/spouse {:db/id         2 :person/name "Sally"
                                     :person/age    22
                                     :person/spouse {:db/id 1 :person/name "Joe"}}})
   :ident         [:person/by-id :db/id]}
  (dom/div
    (dom/div "Name:" name)
    (dom/div "Age:" age
      (dom/button {:onClick
                   #(prim/transact! this `[(make-older {:id ~id})])} "Make Older"))
    (when (and (= 0 render-depth) spouse)
      (dom/ul
        (dom/div "Spouse:"
          ; recursively render, but increase the render depth so we can know when a
          ; targeted UI refresh would accidentally push the UI deeper.
          (ui-person (prim/computed spouse {:render-depth (inc render-depth)})))))))

(def ui-person (prim/factory Person {:keyfn :db/id}))

(defsc Root [this {:keys [person-of-interest]}]
  {:initial-state {:person-of-interest {}}
   :query         [{:person-of-interest (prim/get-query Person)}]}
  (dom/div
    (ui-person person-of-interest)))

5.9.2. Duplicates and Recursion

Of course it is perfectly fine for there to be multiple edges in your graph that point to the same node. Below is a recursive bullet list example. We’ve intentionally nested item B.1 under B and D so you can see that it all works itself out.

Normalization of initial state (which must be a tree) is perfectly happy to see duplcate entries. It simply merges the multiple copies into the same normalized entry in the table.

Since the two entries merge to the same entry, it also means the modifications will be shared among them. Try checking item B.1 in either location.

Show/Hide Source
(ns book.queries.recursive-demo-4
  (:require [fulcro.client.primitives :as prim :refer [defsc]]
            [fulcro.client.mutations :as m :refer [defmutation]]
            [fulcro.client.dom :as dom]))

(declare ui-item)

(defsc Item [this {:keys [ui/checked? item/label item/subitems]}]
  {:query (fn [] [:ui/checked? :db/id :item/label {:item/subitems '...}])
   :ident [:item/by-id :db/id]}
  (dom/li
    (dom/input {:type     "checkbox"
                :checked  (if (boolean? checked?) checked? false)
                :onChange #(m/toggle! this :ui/checked?)})
    label
    (when subitems
      (dom/ul
        (map ui-item subitems)))))

(def ui-item (prim/factory Item {:keyfn :db/id}))

(defsc ItemList [this {:keys [db/id list/items] :as props}]
  {:query [:db/id {:list/items (prim/get-query Item)}]
   :ident [:list/by-id :db/id]}
  (dom/ul
    (map ui-item items)))

(def ui-item-list (prim/factory ItemList {:keyfn :db/id}))

(defsc Root [this {:keys [list]}]
  {:initial-state (fn [p]
                    {:list {:db/id      1
                            :list/items [
                                         {:db/id 2 :item/label "A"
                                          :item/subitems
                                                 [{:db/id      7
                                                   :item/label "A.1"
                                                   :item/subitems
                                                               [{:db/id         8
                                                                 :item/label    "A.1.1"
                                                                 :item/subitems []}]}]}
                                         {:db/id      3
                                          :item/label "B"
                                          :item/subitems
                                                      [{:db/id 6 :item/label "B.1"}]}
                                         {:db/id         4
                                          :item/label    "C"
                                          :item/subitems []}
                                         {:db/id         5
                                          :item/label    "D"
                                          ; just for fun..nest a dupe under D
                                          :item/subitems [{:db/id 6 :item/label "B.1"}]}]}})
   :query         [{:list (prim/get-query ItemList)}]}
  (dom/div
    (ui-item-list list)))

5.10. The AST

You can convert any expression in the query/mutation language into an AST (abstract syntax tree) and vice versa. This lends itself to doing complex parsing of the query (typically on the server). The functions of interest are fulcro.client.primitives/query→ast and ast→query.

There are many uses for this. One such use might be to convert the graph expression into another form. For example, say you wanted to run a query against an SQL database. You could write an algorithm that translates the AST into a series of SQL queries to build the desired result. The AST is always available as one of the parameters in the mutation/query env on the client and server.

Another use for the AST is in mutations targeted at a remote: it turns out you can morph a mutation before sending it to the server.

5.10.1. Morphing Mutations

The most common use of the AST is probably adding parameters that the UI is unaware need to be sent to a remote. When processing a mutation with defmutation (or just the raw defmethod) you will receive the AST of the mutation in the env. It is legal to return any valid AST from the remote side of a mutation. This has the effect of changing what will be sent to the server:

(defmutation do-thing [params]
  (action [env] ...)
  (remote [{:keys [ast]}] ast)) ; same effect as `true`

(defmutation do-thing [params]
  (action [env] ...)
  (remote [{:keys [ast]}] (prim/query->ast `[(do-other-thing)])) ; completely change what gets sent to `remote`

(defmutation do-thing [params]
  (action [env] ...)
  (remote [{:keys [ast]}] (assoc ast :params {:y 3}))) ; change the parameters

For more on mutations see the chapter on Handling Mutations

6. Initial Application State

When starting any application one thing has to be done before just about anything else: Establish a starting state. In Fulcro this just means generating a client-side application database (normalized). Other parts of this guide have talked about the Graph Database. You can well imagine that hand-coding one of these for a large application’s starting state could be kind of a pain. Actually, even though coding it would be a pain, it turns out that the bigger pain happens later when you want to refactor! That can become a real mess!

However, Fulcro already knows how to normalize a tree of data, and your UI is already the tree you’re interested in. So, Fulcro encourages you to co-locate initial application state with the components that need the state and compose it towards the root, just like you do for queries. This gives some nice results:

  • Your initial application state is reasoned about locally to each component, just like the queries.

  • Refactoring the UI just means modifying the local composition of queries and initial state from one place to another in the UI.

  • Fulcro understands unions (you can only initialize one branch of a to-one relation), and can scan for and initialize alternate branches.

6.1. Adding Initial State to Components

To add initial state, follow these steps:

  1. For each component that should appear initially: add the :initial-state option.

  2. Compose the components in (1) all the way to your root.

That’s it! Fulcro will automatically detect initial state on the root, and use it for the application!

Note
Pulling the initial state from a component should be done with fulcro.core/get-initial-state. Calling a static protocol cannot work on the server, so this helper method makes server-side rendering possible for your components.
(defui Child
  static fulcro.core/InitialAppState
  (initial-state [cls params] { :x 1 }) ; set :x to 1 for this component's state
  static prim/IQuery
  (query [this] [:x]) ; query for :x
  static prim/Ident
  (ident [this props] ...) ; how to normalize
  Object
  (render [this]
    (let [{:keys [x]} (prim/props this)] ; pull x from props
      ...)))

(defui Parent
  static fulcro.core/InitialAppState
  (initial-state [cls params] { :y 2 :child (prim/get-initial-state Child {}) }) ; set y, and compose in child's state
  static prim/IQuery
  (query [this] [:y {:child (prim/get-query Child)}]) ; query for :y and compose child's query
  static prim/Ident
  (ident [this props] ...) ; how to normalize
  Object
  (render [this]
    (let [{:keys [y child]} (prim/props this)] ; pull y and child from props
      ...)))

...

Notice the nice symmetry here. The initial state is (usually) a map that represents (recursively) the entity and it’s children. The query is a vector that lists the "scalar" props, and joins as maps. So, in Child we have initial state for :x and a query for :x. In the parent we have a query for the property :y and a join to the child, and initial state for the scalar value of :y and the composed initial state of the Child. Render has the same thing: the things you pull out of props will be the things for which you queried. Thus, all three essentially list the same things, but in slightly different forms.

6.1.1. Initial State and Alternate Branches of Unions

The one "extra" feature that initial state support does for you is to initialize alternate branches of components that have to-one union query. Remember that a to-one relation from a union could be to any number of alternates.

Take this union query: {:person (prim/get-query Person) :place (prim/get-query Place)}

It means "if you find an ident in the graph pointing to a :person, then query for the person. If you find one for :place, then query for a place. The problem is: if it is a to-one relation then only one can be in the initial state tree at startup!

{ :person-or-place [:person 2]
  :person {2 {:id 2 ...}}}

If you look at a proposed initial state, it will make the problem more clear:

(defui Person
  static prim/InitialAppState
  (initial-state [c {:keys [id name]}] {:id id :name name :type :person})
  ...)

(defui PersonPlaceUnion
  static prim/InitialAppState
  (initial-state [c p] (prim/get-initial-state Person {:id 1 :name "Joe"})) ; I can only put in one of them!
  static prim/IQuery
  (query [this] {:person (prim/get-query Person) :place (prim/get-query Place)})
  ...)

(defui Parent
  static prim/InitialAppState
  (initial-state [c p] {:person-or-place (prim/get-initial-state PersonPlaceUnion)})
  static prim/IQuery
  (query [this] [{:person-or-place (prim/get-query PersonPlaceUnion)}]))

This would result in a person in the initial state, but not a place.

Fulcro solves this at startup in the following manner: It pulls the query from root and walks it. If it finds a union component, then for each branch it sees if that component (via the query metadata) has initial state. If it does, it places it in the correct table in app state. This does not, of course, join it to anything in the graph since it isn’t the "default branch" that was explicitly listed (in PersonPlaceUnion’s `InitialAppState).

This behavior is critical when using unions to handle UI routing, which is in turn essential for good application performance.

6.1.2. Initial State Demo

The following demo shows all of this in action.

Show/Hide Source
(ns book.demos.initial-app-state
  (:require
    [fulcro.client.dom :as dom]
    [fulcro.client :as fc]
    [fulcro.client.mutations :as m]
    [fulcro.client.primitives :as prim :refer [defsc InitialAppState initial-state]]))

(defonce app (atom (fc/new-fulcro-client)))

(defmethod m/mutate 'nav/settings [{:keys [state]} sym params]
  {:action (fn [] (swap! state assoc :panes [:settings :singleton]))})

(defmethod m/mutate 'nav/main [{:keys [state]} sym params]
  {:action (fn [] (swap! state assoc :panes [:main :singleton]))})

(defsc ItemLabel [this {:keys [value]}]
  {:initial-state (fn [{:keys [value]}] {:value value})
   :query         [:value]
   :ident         (fn [] [:labels/by-value value])}
  (dom/p value))

(def ui-label (prim/factory ItemLabel {:keyfn :value}))

;; Foo and Bar are elements of a mutli-type to-many union relation (each leaf can be a Foo or a Bar). We use params to
;; allow initial state to put more than one in place and have them be unique.
(defsc Foo [this {:keys [label]}]
  {:query         [:type :id {:label (prim/get-query ItemLabel)}]
   :initial-state (fn [{:keys [id label]}] {:id id :type :foo :label (prim/get-initial-state ItemLabel {:value label})})}
  (dom/div
    (dom/h2 "Foo")
    (ui-label label)))

(def ui-foo (prim/factory Foo {:keyfn :id}))

(defsc Bar [this {:keys [label]}]
  {:query         [:type :id {:label (prim/get-query ItemLabel)}]
   :initial-state (fn [{:keys [id label]}] {:id id :type :bar :label (prim/get-initial-state ItemLabel {:value label})})}
  (dom/div
    (dom/h2 "Bar")
    (ui-label label)))

(def ui-bar (prim/factory Bar {:keyfn :id}))

;; This is the to-many union component. It is the decision maker (it has no state or rendering of it's own)
;; The initial state of this component is the to-many (vector) value of various children
;; The render just determines which thing it is, and passes on the that renderer
(defsc ListItem [this {:keys [id type] :as props}]
  {:initial-state (fn [params] [(prim/get-initial-state Bar {:id 1 :label "A"}) (prim/get-initial-state Foo {:id 2 :label "B"}) (prim/get-initial-state Bar {:id 3 :label "C"})])
   :query         (fn [] {:foo (prim/get-query Foo) :bar (prim/get-query Bar)}) ; use lambda for unions
   :ident         (fn [] [type id])}                        ; lambda for unions
  (case type
    :foo (ui-foo props)
    :bar (ui-bar props)
    (dom/p "No Item renderer!")))

(def ui-list-item (prim/factory ListItem {:keyfn :id}))

;; Settings and Main are the target "Panes" of a to-one union (e.g. imagine tabs...we use buttons as the tab switching in
;; this example). The initial state looks very much like any other component, as does the rendering.
(defsc Settings [this {:keys [label]}]
  {:initial-state (fn [params] {:type :settings :id :singleton :label (prim/get-initial-state ItemLabel {:value "Settings"})})
   :query         [:type :id {:label (prim/get-query ItemLabel)}]}
  (ui-label label))

(def ui-settings (prim/factory Settings {:keyfn :type}))

(defsc Main [this {:keys [label]}]
  {:initial-state (fn [params] {:type :main :id :singleton :label (prim/get-initial-state ItemLabel {:value "Main"})})
   :query         [:type :id {:label (prim/get-query ItemLabel)}]}
  (ui-label label))

(def ui-main (prim/factory Main {:keyfn :type}))

;; This is a to-one union component. Again, it has no state of its own or rendering. The initial state is the single
;; child that should appear. Fulcro (during startup) will detect this component, and then use the query to figure out
;; what other children (the ones that have initial-state defined) should be placed into app state.
(defsc PaneSwitcher [this {:keys [id type] :as props}]
  {:initial-state (fn [params] (prim/get-initial-state Main nil))
   :query         (fn [] {:settings (prim/get-query Settings) :main (prim/get-query Main)})
   :ident         (fn [] [type id])}
  (case type
    :settings (ui-settings props)
    :main (ui-main props)
    (dom/p "NO PANE!")))

(def ui-panes (prim/factory PaneSwitcher {:keyfn :type}))

;; The root. Everything just composes to here (state and query)
;; Note, in core (where we create the app) there is no need to say anything about initial state!
(defsc Root [this {:keys [panes items]}]
  {:initial-state (fn [params] {:panes (prim/get-initial-state PaneSwitcher nil)
                                :items (prim/get-initial-state ListItem nil)})
   :query         [{:items (prim/get-query ListItem)}
                   {:panes (prim/get-query PaneSwitcher)}]}
  (dom/div
    (dom/button {:onClick (fn [evt] (prim/transact! this '[(nav/settings)]))} "Go to settings")
    (dom/button {:onClick (fn [evt] (prim/transact! this '[(nav/main)]))} "Go to main")

    (ui-panes panes)

    (dom/h1 "Heterogenous list:")

    (dom/ul
      (mapv ui-list-item items))))

6.2. Initial State and State Progressions

If you remember from the diagram about pure rendering, then you’ll also note that this step generates the first state in that progression. Rendering any state results in the UI for that state.

An interesting note is that this model also results in a really useful property: You can take the initial state, run it though the implementation of one or more mutations, and end up with any other state. This means you can easily reason about initializing your application into any state, which is useful for things like testing and server-side rendering.

There are all sorts of very useful features that fall out of this. For example, it is also possible to record a series of "user interactions" (which can be recorded as a list of the mutations that ran) and replay those. This could be used to send a tester a sequence of steps to show recent development work, run automated demos/tests, teleport your development environment to a specific page, etc.

Writing tests against the state model and mutation implementations is a great way to unit test your application without needing to involve the UI itself at all! You can read more about that in the section on Visual Regression Testing

7. Normalization

Normalization is a central mechanism in Fulcro. It is the means by which your data trees (which you receive from component queries against servers) can be placed into your normalized graph database.

7.1. Internals

The function prim/tree→db is the workhorse that turns an incoming tree of data into normalized data (which can then be merged into the overall database).

Imagine an incoming tree of data:

{ :people [ {:db/id 1 :person/name "Joe" ...} {:db/id 2 ...} ... ] }

and the query:

[{:people (prim/get-query Person)}]

which expands to:

[{:people [:db/id :person/name]}]
          ^ metadata {:component Person}

tree→db recursively walks the data structure and query:

  • At the root, it sees :people as a root key and property. It remembers it will be writing :people to the root.

  • It examines the value of :people and finds it to be a vector of maps. This indicates a to-many relationship.

  • It examines the metadata on the subquery of :people and discovers that the entries are represented by the component Person

  • For each map in the vector, it calls the ident function of Person (which it found in the metadata) to get a database location. It then places the "person" values into the result via assoc-in on the ident.

  • It replaces the entries in the vector with the idents.

If the metadata was missing then it would assume the person data did not need normalization. This is why it is critical to compose queries correctly. The query and tree of data must have a parallel structure, as should the UI. In template mode defsc will try to check some things for you, but you must ensure that you compose the queries correctly.

7.2. Normalization: Initial State, Server Interations, and Mutations

The process described above is how most data interactions occur. At startup the :initial-state supplies data that exactly matches the tree of the UI. This gives your UI some initial state to render. The normalization mechanism described above is exaclty what happens to that initial tree when it is detected by Fulcro at startup.

Network interactions send a UI-based query (which remember is annotated with the components). The query is remembered and when a response tree of data is received (which must match the tree structure of the query), the normalization process is applied and the resulting normalized data is merged with the database.

If using websockets, it is the same thing: A server push gives you a tree of data. You could hand-normalize that data, but actually if you know the structure of the incoming data you can easily generate a client-side query (using defsc or defui) that can be used in conjunction with prim/tree→db to normalize that incoming data.

Mutations can do the same thing. If a new instance of some entity is being generated by the UI as a tree of data, then the query for that UI component can be used to turn it into normalized data that can be merged into the state within the mutation.

Some useful functions to know about:

  • prim/merge-component - A utility function for merging new instances of a (possibly recursive) entity state into the normalized database. Usable from within mutations.

  • prim/merge-state! - A utility function for merging out-of-band (e.g. push notification) data into your application. Includes ident integration options, and honors the Fulcro merge clobbering algorithm (if the query doesn’t ask for it, then merge doesn’t affect it). Also queues rendering for affected components (derived from integration of idents). Generally not used within mutations (use merge-component and integrate-ident! instead).

  • prim/tree→db - General utility for normalizing data via a query and chunk of data.

  • prim/integrate-ident! - A utility for adding an ident into existing to-one and to-many relations in your database. Can be used within mutations.

  • fulcro.client.util/deep-merge - An implementation of merge that is recursive

8. Handling Mutations

Mutations are known by their symbol and are dispatched to the internal multimethod fulcro.client.mutations/mutate. To handle a mutation you can do two basic things: use defmethod to add a mutation support, or use the macro defmutation. The macro is recommended for most cases because it namespaces the mutation, prevents some common errors, and works better with IDEs.

8.1. Stages of Mutation

There are multiple passes on a mutation: one local, and one for each possible remote. It is technically the job of the mutation handler to return a lambda for the local pass, and a boolean (or AST) for each remote. Returning nil from any pass means to not do anything for that concern.

For example, say you have three remotes: one for normal API, one that hits a REST API, and one for file uploads. Each would have a name, and each pass of the mutation handling would be interested in knowing what you’d like to do for the local or remote.

The mutation environment (env in the examples) contains a target that is set to a remote’s name when the mutation is being asked for details about how to handle the mutation with respect to that remote.

For each pass the mutation is supposed to return a map whose key is :action or the name of the remote, and whose value is the thing to do (a lambda for :action, and AST or true/false for remotes).

Summary:

  1. You transact! somewhere in the UI

  2. The internals call your mutation with :target set to nil in env. You return a map with an :action key whose value is the function to run.

  3. The internals call your mutation once for each remote, with :target set. You return a map with that remote’s keyword as the key, and either a boolean or AST as the remote action. (true means send the AST for the expression sent in (1) to the remote)

8.2. Using the Multimethod Directly

Typically the multipass nature is ignored by the mutation itself, and it just returns a map containing all of the possible things that should be done. This looks like:

(defmethod fulcro.client.mutations/mutate `mutation-symbol [{:keys [state ast target] :as env} k params]
   {:action (fn [] ...)
    :rest-api true ; trigger this remotely on the rest API AND the normal one.
    :remote true })

Since the action is just data, it doesn’t matter that we "generate" it for the multiple passes. Same for the remotes.

Reminder

The example above uses syntax quoting on the symbol which will add the current namespace to it. In any case the symbol is just that: a symbol (data) that acts as the dispatch key for the multimethod. If you use a plain quote (') then you should manually namespace the symbol.

Some common possible mistakes are:

  1. You side-effect. Your mutation will be called at least two times so this is a bad idea. Side effects should be wrapped in the action.

  2. You assume that the remote expression "sees" the old state (e.g. you might build an AST based on what is in app state). The local action is usually run before the remote passes, meaning the state has already changed and the remote logic is seeing the "new" client database state.

  3. You forget to return a map with the correct keys (usually if you made mistake 1).

There is no guaranteed order to evaluation. Therefore if you need a value from state as it was seen when the mutation was triggered: send it as a parameter to the mutation from the UI (where you knew the old value). That way the call itself has captured the old value.

8.3. Using defmutation

defmutation is a macro that writes the multimethod for you. It looks like this:

(defmutation mutation-symbol
  "docstring"
  [params]
  (action [{:keys [state] :as env}]
    (swap! state ...))
  (rest-api [env] true)
  (remote [env] true))

Thus it ends up looking more like a function definition. IDE’s like Cursive can be told how to resolve the macro (as defn in this case) and will then let you read the docstrings and navigate to the definition from the usage site in transact. This makes development a lot easier.

Another advantage is that the symbol is placed into the namespace in which it is declared (not interned, just given the namespace…​it is still just symbol data). Syntax quoting can expand aliasing, which means you get a very nice tool experience at usage site:

(ns app
  (:require [app.mutations :as am]))

...
   (prim/transact! this `[(am/mutation-symbol {})]) ; am gets expanded to app.mutations

The final advantage is it is harder to accidentally side-effect. The action section of defmutation will wrap the logic in a lambda, meaning that it can read as-if you’re side-effecting, but in fact will do the right thing.

In general these advantages mean you should generally use the macro to define mutations, but it is good to be aware that underneath is just a multimethod.

8.4. Refreshing Non-local UI

In the default rendering mode Fulcro is capable of optimizing your rendering refresh so that the bare minimum amount of activity happens in the query engine and React. The basic rules are as follows:

  1. Refresh the subtree starting at the component that ran transact!. If it was the reconciler, run from root.

  2. Refresh any component’s that share the same ident with the component that ran the transaction.

  3. Refresh any component that queries for any of the data that was indicated in "follow-on" reads.

8.4.1. Parent-Child Relationships

The most common case of non-local UI refresh comes up with parent-child relationships. In these cases, the parent can be seen as a UI component in control of the children (it is responsible for telling them to render). In such cases it is usually better to reason about the management logic (i.e. deleting, reordering, etc) from the parent; however, it is commonly the case the you want the child to render some controls, such as a delete button. Thus, the control that wants to modify the state (the delete button on the item) is not local to the component that will need to refresh (the parent’s list).

The solution is simple: create a callback in the parent that can run the delete transaction and pass it through as a computed value.

In our example, we’ll assume the delete is global (removes the item from the list and the normalized table):

(m/defmutation delete-item
  "Mutation: Delete an item from a list"
  [{:keys [id]}]
  (action [{:keys [state]}]
    (letfn [(filter-item [list] (filterv #(not= (second %) id) list))]
      (swap! state
        (fn [s]
          (-> s
            (update :items dissoc id) ; remove the item from the db (optional)
            (update-in [:lists 1 :list/items] filter-item))))))) ; remove the item from the list of idents

The source of the components for this demo are below. Make note of the use of computed.

Show/Hide Source
(ns book.demos.parent-child-ownership-relations
  (:require
    [fulcro.client.dom :as dom]
    [fulcro.client.mutations :as m]
    [fulcro.client :as fc]
    [fulcro.client.primitives :as prim :refer [defsc]]))

; Not using an atom, so use a tree for app state (will auto-normalize via ident functions)
(def initial-state {:ui/react-key "abc"
                    :main-list    {:list/id    1
                                   :list/name  "My List"
                                   :list/items [{:item/id 1 :item/label "A"}
                                                {:item/id 2 :item/label "B"}]}})

(defonce app (atom (fc/new-fulcro-client :initial-state initial-state)))

(m/defmutation delete-item
  "Mutation: Delete an item from a list"
  [{:keys [id]}]
  (action [{:keys [state]}]
    (letfn [(filter-item [list] (filterv #(not= (second %) id) list))]
      (swap! state
        (fn [s]
          (-> s
            (update :items dissoc id)
            (update-in [:lists 1 :list/items] filter-item)))))))

(defsc Item [this
             {:keys [item/id item/label] :as props}
             {:keys [on-delete] :as computed}]
  {:initial-state (fn [{:keys [id label]}] {:item/id id :item/label label})
   :query         [:item/id :item/label]
   :ident         [:items :item/id]}
  (dom/li label (dom/button {:onClick #(on-delete id)} "X")))

(def ui-list-item (prim/factory Item {:keyfn :item/id}))

(defsc ItemList [this {:keys [list/name list/items]}]
  {:initial-state (fn [p] {:list/id    1
                           :list/name  "List 1"
                           :list/items [(prim/get-initial-state Item {:id 1 :label "A"})
                                        (prim/get-initial-state Item {:id 2 :label "B"})]})
   :query         [:list/id :list/name {:list/items (prim/get-query Item)}]
   :ident         [:lists :list/id]}
  (let [; pass the operation through computed so that it is executed in the context of the parent.
        item-props (fn [i] (prim/computed i {:on-delete #(prim/transact! this `[(delete-item {:id ~(:item/id i)})])}))]
    (dom/div
      (dom/h4 name)
      (dom/ul
        (map #(ui-list-item (item-props %)) items)))))

(def ui-list (prim/factory ItemList))

(defsc Root [this {:keys [main-list]}]
  {:initial-state (fn [p] {:main-list (prim/get-initial-state ItemList {})})
   :query         [{:main-list (prim/get-query ItemList)}]}
  (dom/div (ui-list main-list)))

8.4.2. Follow-on Reads

A follow-on read is a property listed in a transaction:

(transact! this `[(f) :person/name])

It indicates that the given data will have changed, and therefore any on-screen component that queries for that particular data should also be refreshed when the transaction completes the optimistic update (and again after the remote interaction, if there is one).

Follow-on reads allow the developer to reason more abstractly about non-local UI refresh. They need only think about what data is changing, and not about what components might be displaying it. This allows the UI to evolve without additional concerns that refresh will be slow (due to expensive data analysis) or will become broken (because you didn’t know a component needed refresh).

(defsc Right [this {:keys [db/id right/value]}]
  {:query         [:db/id :right/value]}
  (dom/div
    ; needs a follow-on read because it is updating something that is queried elsewhere...
    (dom/button {:onClick #(prim/transact! this `[(ping-left {}) :left/value])} "Ping Left")
    value))

This model was inherited from Om Next, and it is the correct model to use when the transaction might, for example, modify opaque data that is not directly queried but which would cause a component to need a refresh; however, since most transactions run a mutation that is really more aware of what data is changing it makes quite a bit of sense for you to be able to declare this on the mutation itself.

8.4.3. Declarative Refresh

Fulcro 2.0+ supports a way of declaring follow-on reads that allows for better local reasoning: co-locate the follow-on reads with the mutation itself. The mechanism is quite simple: add a refresh section on your mutation and return the list of keywords for the data that changed:

(defmutation ping-left [params]
  (action [{:keys [state]}]
    (swap! state update-in [:left/by-id 5 :left/value] inc))
  (refresh [env] [:left/value]))

The above mutation just indicates what it changed: :left/value.

The UI can then drop the follow-on reads:

(defsc Right [this {:keys [db/id right/value]}]
  {:query         [:db/id :right/value] }
  (dom/div
    ; no longer need the :left/value as a follow-on read
    (dom/button {:onClick #(prim/transact! this `[(ping-left {})])} "Ping Left")
    value))

In this case the transaction is running on a component that doesn’t query for the data being changed (it is pinging the Left component). The built-in refresh list on the mutation takes care of the update!

The live example below is a full-stack demo of this. The buttons update data that the other button displays. The `transact!`s on these would normally require follow-on reads or a callback to the parent to refresh properly. With the refresh list on the mutation itself the UI designer is freed from this responsibility.

The right button uses data from the server in a pessimistic fashion (it does no optimistic update, and you can increase the simulated delay on the server), so pinging it from the left actually reads a value from the server. This demonstrates that the refresh is working for full-stack operations.

Show/Hide Source
(ns book.demos.declarative-mutation-refresh
  (:require [fulcro.client.dom :as dom]
            [fulcro.client.primitives :as prim :refer [defsc]]
            [fulcro.client.dom :as dom]
            [fulcro.client.mutations :as m :refer [defmutation]]
            [fulcro.client.data-fetch :as df]
            [fulcro.server :as server]
            [fulcro.util :as util]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SERVER:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(server/defmutation ping-right [params]
  (action [env]
    {:db/id 1 :right/value (util/unique-key)}))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CLIENT:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmutation ping-left [params]
  (action [{:keys [state]}]
    (swap! state update-in [:left/by-id 5 :left/value] inc))
  (refresh [env] [:left/value]))

(declare Right)

(defmutation ping-right [params]
  (remote [{:keys [state ast]}]
    (m/returning ast state Right))
  (refresh [env] [:right/value]))

(defsc Left [this {:keys [db/id left/value]}]
  {:query         [:db/id :left/value]
   :initial-state {:db/id 5 :left/value 42}
   :ident         [:left/by-id :db/id]}
  (dom/div {:style {:float "left"}}
    (dom/button {:onClick #(prim/transact! this `[(ping-right {})])} "Ping Right")
    value))

(def ui-left (prim/factory Left {:keyfn :db/id}))

(defsc Right [this {:keys [db/id right/value]}]
  {:query         [:db/id :right/value]
   :initial-state {:db/id 1 :right/value 99}
   :ident         [:right/by-id :db/id]}
  (dom/div {:style {:float "right"}}
    (dom/button {:onClick #(prim/transact! this `[(ping-left {})])} "Ping Left")
    value))

(def ui-right (prim/factory Right {:keyfn :db/id}))

(defsc Root [this {:keys [left right]}]
  {:query         [{:left (prim/get-query Left)}
                   {:right (prim/get-query Right)}]
   :initial-state {:left {} :right {}}}
  (dom/div {:style {:width "600px" :height "50px"}}
    (ui-left left)
    (ui-right right)))

8.5. Recommendations About Writing Mutations

Mutations themselves are meant to be abstractions across the entire stack; however, the optimistic side of them are really just functions on your application state. Components have nice clean abstractions, and you will often benefit from writing low-level functions that represent the general operations on a component’s data. As you move towards higher-level abstractions you’ll want to compose those lower-level functions. As such, it pays to think a little about how this will look over time.

If you write the actual logic of a mutation into a defmutation, then composition is difficult because the model does not encourage recursive calls to transact!. This will either lead to code duplication or other bad practices.

To maximize code reuse, local reasoning, and general readability it pays to think about your mutations in the following manner:

  1. A mutation is a function that changes the state of the application: state → mutation → state'

  2. Within a mutation, you are essentially doing operations to a graph, which means you have operations that work on some node in the graph: node → op → node'. These operations may modifiy a scalar or an edge to another node.

8.5.1. Node-Specific Mutation

You can run a node-specific operation with:

(swap! state update-in node-ident op args)

For example, say you want to implement a mutation that adds a person to another person’s friend list. The data representation of a person is: {:db/id 1 :person/name "Nancy" :person/friends []}.

The ident function can be coded into a top-level function and used by the component:

(defn person-ident [id-or-props]
  (if (map? id-or-props)
    (person-ident (:db/id id-or-props))
    [:person/by-id id-or-props]))

(defsc Person [this props]
  {:ident (fn [] (person-ident props))}
  ...)

and our desired operation on the general state of a person can also be written as a simple function (add an ident to the :person/friends field):

(defn add-friend*
   "Add a friend to an existing person. Returns the updated person."
   [person target-person-id]
   (update person :person/friends (fnil conj []) (person-ident target-person-id)))

Note, in particular, that we always choose to put the item to be updated as the first argument so that functions like update are easier to use with it.

Now (add-friend* {:db/id 1 :person/name "Nancy"} 33) results in {:db/id 1, :person/name "Nancy", :person/friends [[:person/by-id 33]]}.

You can write a mutation to support this operation within Fulcro:

(defmutation add-friend [{:keys [source-person-id target-id]}]
  (action [{:keys [state]}]
    (swap! state update-in (person-ident source-person-id)
      add-friend* target-id)))

You could even take it a step further with a little more sugar by defining a helper that can turn a node-specific op into a db-level operation (again note that the thing being updated is the first argument):

(defn update-person [state-map person-id person-fn & args]
  (apply update-in state-map (person-ident person-id) person-fn args))

(defmutation add-friend [{:keys [source-person-id target-id]}]
  (action [{:keys [state]}]
    (swap! state update-person source-person-id add-friend* target-id)))

If you find that a given entity is always modified in the context of the state map itself (a common case) then it can be a bit shorter to just push the table logic (ident resolution) into the operation itself:

(defn add-friend**
   "Add a friend to an existing person in the client database."
   [app-state person-id friend-id]
   (update-in app-state [:person/by-id person-id field-name] (fnil conj []) (person-ident friend-id)))

(defmutation add-friend [{:keys [source-person-id target-id]}]
  (action [{:keys [state]}]
    (swap! state add-friend** source-person-id target-id)))

8.5.2. Composition in Mutations

Once you have your general operations written as basic functions on either the entire state (like update-person) or targeted to nodes or the state map itself (like add-friend*), then it becomes much easier to create mutations that compose together operations to accomplish any higher-level task.

For example, the fulcro.client.routing/update-routing-links function takes a state map, and changes all of the routers in the application state to show that particular screen. So, say you wanted add-friend to also take you to the screen that shows the details of that particular person. The top-level abstract mutation in the UI might still be called add-friend, but the internals now have two things to do.

Having all of these functions on the graph database allows you to write this in a very nice form as a sequence of operations on the state map itself through threading:

(defmutation add-friend
  "Locally add a friend, and show them. Full stack operation.
  [{:keys [source-id target-id]}]
  (action [{:keys [state]}]
    (swap! state
      (fn [s]
        (-> s
          (r/update-routing-links {:handler :show-friend :route-params {:person-id target-id}})
          ; Could use (add-friend** source-id target-id) or:
          (update-person source-id add-friend* target-id)))))
  (remote [env] true)

and your mutations become a thing of beauty!

Of course, this can also be overkill. It is true that it is often handy to be able to compose many db operations together into one abstract mutation, but don’t forget that more that one mutation can be triggered by a single call to transact!:

(prim/transact! this `[(route-to {:handler :show-friend :route-params {:person-id ~target-id}})
                       (add-friend {:source-person-id ~my-id :target-id ~target-id})])

You’ll want to balance your mutations just like you do any other library of code: so that reuse and clarity are maximized. In the case of mutations the deciding factor is often how you want to deal with remote mutations.

8.6. Advanced Mutation Topics

This section covers a number of additional mutation techniques that arise in more advanced situations. Almost all of these circumstances arise from needing to modify your application database outside of the normal prim/transact! mechanism at the UI layer.

A lot of these things are handled for you with normal full-stack operations, so you might want to skip this section until you’re comfortable with that material.

8.6.1. Saving a Reconciler

The first note is that prim/transact! can be used on the reconciler. If you’ve saved your Fulcro Application in a top-level atom then you can run a transaction "globally" like this:

(prim/transact! (:reconciler @app) ...)

This should generally be used in cases where there is an abstract operation (e.g. you want setTimeout to update a js/Date to the current time and have the screen refresh). Using (prim/transact! (:reconciler @app) '[(update-time) :current-time]) is much clearer and in the spirit of the framework than any other low-level data tweaking. That could also be done in the context of a component to prevent an overall root re-render, though you’d want to be careful to use both sides of the component lifecycle to install and remove a timer that triggers such an update.

8.6.2. Swapping on the State Atom?

Yes. There is a watch on Fulcro’s state atom. Doing so will cause a refresh from the root of your UI.

(let [{:keys [reconciler]} @app state-atom (prim/app-state reconciler)] (swap! state-atom …​))

8.6.3. Leveraging fulcro.client.primitives/tree→db

In some cases you will have obtained some data (or perhaps invented it) and you need to integrate that data into the database. If the data matches your UI structure (as a tree) and you have proper Ident declarations on those components then you can simply transform the data into the correct shape via the tree→db function using a component’s query.

Unfortunately, you would then need to follow that transform by a sequence of operations on app state to merge those various bits.

The function also requires a query in order to do normalization (split the tree into tables).

Important
The general interaction with the world requires integration of external data (often in a tree format) with your app database (normalized graph of maps/vectors). As a result, you almost always want a component-based query when integrating data so that the result is normalized.

8.6.4. Using integrate-ident

When in a mutation you very often need to place an ident in various spots in your graph database. The helper function fulcro.client/integrate-ident can by used from within mutations to help you do this. It accepts any number of named parameters that specify operations to do with a given ident:

(swap! state
  (fn [s]
    (-> s
       (do-some-op)
       (integrate-ident the-ident
         :append [:table id :field]
         :prepend [:other id :field]))))

This function checks for the existence of the given ident in the target list, and will refuse to add it if it is already there. The :replace option can be used on a to-one or to-many relation. When replacing on a to-many, you use an index in the target path (e.g. [:table id :field 2] would replace the third element in a to-many :field)

8.6.5. Creating Components Just For Their Queries

If your UI doesn’t have a query that is convenient for sending to the server (or for working on tree data like this), then it is considered perfectly fine to generate components just for their queries (no render). This is often quite useful, especially in the context of pre-loading data that gets placed on the UI in a completely different form (e.g. the UI queries don’t match what you’d like to ask the server).

(defsc SubQuery [t p]
  {:ident [:sub/by-id :id]
   :query [:id :data]})

(defsc TopQuery [t p]
  {:ident [:top/by-id :id]
   :query [:id {:subs (prim/get-query SubQuery)}]})
Show/Hide Source
(ns book.tree-to-db
  (:require [fulcro.client.primitives :as prim :refer [defsc]]
            [fulcro.client.dom :as dom]
            [devcards.util.edn-renderer :refer [html-edn]]
            [fulcro.client.mutations :as m :refer [defmutation]]))

(defsc SubQuery [t p]
  {:ident [:sub/by-id :id]
   :query [:id :data]})

(defsc TopQuery [t p]
  {:ident [:top/by-id :id]
   :query [:id {:subs (prim/get-query SubQuery)}]})

(defmutation normalize-from-to-result [ignored-params]
  (action [{:keys [state]}]
    (let [result (prim/tree->db TopQuery (:from @state) true)]
      (swap! state assoc :result result))))

(defmutation reset [ignored-params] (action [{:keys [state]}] (swap! state dissoc :result)))

(defsc Root [this {:keys [from result]}]
  {:query         [:from :result]
   :initial-state (fn [params]
                    ; some data we're just shoving into the database from root...***not normalized***
                    {:from {:id :top-1 :subs [{:id :sub-1 :data 1} {:id :sub-2 :data 2}]}})}
  (dom/div
    (dom/div
      (dom/h4 "Pretend Incoming Tree")
      (html-edn from))
    (dom/div
      (dom/h4 "Normalized Result (click below to normalize)")
      (when result
        (html-edn result)))
    (dom/button {:onClick (fn [] (prim/transact! this `[(normalize-from-to-result {})]))} "Normalized (Run tree->db)")
    (dom/button {:onClick (fn [] (prim/transact! this `[(reset {})]))} "Clear Result")))

of course, you can see that you’re still going to need to merge the database table contents into your main app state and carefully integrate the other bits as well.

Reminder

The ident part of the component is the magic here. This is why you need component queries for this work work right. The ident functions are used to determine the table locations and idents to place into the normalized database!

8.6.6. Using fulcro.client.primitives/merge!

Fulcro includes a function that takes care of the rest of these bits for you. It requires the reconciler (which as we mentioned earlier can be obtained from the Fulcro App). The arguments are similar to tree→db:

(prim/merge! (:reconciler @app) ROOT-data ROOT-query)

The same things apply as tree→db (idents especially), however, the result of the transform will make it’s way into the app state (which is owned by the reconciler).

IMPORTANT: The biggest challenge with using this function is that it requires the data and query to be structured from the ROOT of the database! That is sometimes perfectly fine, but our next section talks about a helper that might be easier to use.")

8.6.7. Using fulcro.client.primitives/merge-component!

There is a common special case that comes up often: You want to merge something that is in the context of some particular UI component.

(prim/merge-component! app ComponentClass ComponentData)

Think of this case as: I have some data for a given component (which MUST have an ident). I want to merge into that component’s entry in a table, but I want to make sure the recursive tree of data also gets normalized properly.

merge-component! also integrates the functionality of integrate-ident! to pepper the ident of the merged entity throughout your app database, and can often serve as a total one-stop shop for merging data that is coming from some external source.

This first argument can be an application or reconciler.

Merge-component! Demo

In the card below the button simulates some external event that has brought in data that we’d like to merge (a newly arrived counter entity):

{ :counter/id 5 :counter/n 66 }

We’ll want to:

  • Add the counter to the counter’s table (which is not even present because we have none in our initial app state)

  • Add the ident of the counter to the UI panel so it’s UI shows up

(defsc Counter [this {:keys [counter/id counter/n] :as props} {:keys [onClick] :as computed}]
  {:query [:counter/id :counter/n]
   :ident [:counter/by-id :counter/id]}
   ...

(defn add-counter
  "Merge the given counter data into app state and append it to our list of counters"
  [reconciler counter]
  (prim/merge-component! reconciler Counter counter
    :append [:panels/by-kw :counter :counters]))

...
(defsc Root ...
  (let [reconciler (prim/get-reconciler this)]  ; one way to get to the reconciler
    (dom/button {:onClick #(add-counter reconciler {:counter/id 4 :counter/n 22})} "Simulate Data Import")
   ...

Here is the full running example with source:

Show/Hide Source
(ns book.merge-component
  (:require [fulcro.client.primitives :as prim :refer [defsc]]
            [fulcro.client.dom :as dom]
            [fulcro.client.cards :refer [defcard-fulcro]]
            [fulcro.client.mutations :as m :refer [defmutation]]
            [fulcro.client.data-fetch :as df]))

(defsc Counter [this {:keys [counter/id counter/n] :as props} {:keys [onClick] :as computed}]
  {:query [:counter/id :counter/n]
   :ident [:counter/by-id :counter/id]}
  (dom/div :.counter
    (dom/span :.counter-label
      (str "Current count for counter " id ":  "))
    (dom/span :.counter-value n)
    (dom/button {:onClick #(onClick id)} "Increment")))

(def ui-counter (prim/factory Counter {:keyfn :counter/id}))

; the * suffix is just a notation to indicate an implementation of something..in this case the guts of a mutation
(defn increment-counter*
  "Increment a counter with ID counter-id in a Fulcro database."
  [database counter-id]
  (update-in database [:counter/by-id counter-id :counter/n] inc))

(defmutation increment-counter [{:keys [id] :as params}]
  ; The local thing to do
  (action [{:keys [state] :as env}]
    (swap! state increment-counter* id))
  ; The remote thing to do. True means "the same (abstract) thing". False (or omitting it) means "nothing"
  (remote [env] true))

(defsc CounterPanel [this {:keys [counters]}]
  {:initial-state (fn [params] {:counters []})
   :query         [{:counters (prim/get-query Counter)}]
   :ident         (fn [] [:panels/by-kw :counter])}
  (let [click-callback (fn [id] (prim/transact! this
                                  `[(increment-counter {:id ~id}) :counter/by-id]))]
    (dom/div
      ; embedded style: kind of silly in a real app, but doable
      (dom/style ".counter { width: 400px; padding-bottom: 20px; }
                  button { margin-left: 10px; }")
      ; computed lets us pass calculated data to our component's 3rd argument. It has to be
      ; combined into a single argument or the factory would not be React-compatible (not would it be able to handle
      ; children).
      (map #(ui-counter (prim/computed % {:onClick click-callback})) counters))))

(def ui-counter-panel (prim/factory CounterPanel))

(defonce timer-id (atom 0))
(declare sample-of-counter-app-with-merge-component-fulcro-app)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; The code of interest...
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn add-counter
  "NOTE: A function callable from anywhere as long as you have a reconciler..."
  [reconciler counter]
  (prim/merge-component! reconciler Counter counter
    :append [:panels/by-kw :counter :counters]))

(defsc Root [this {:keys [panel]}]
  {:query         [{:panel (prim/get-query CounterPanel)}]
   :initial-state {:panel {}}}
  (let [reconciler (prim/get-reconciler this)]              ; pretend we've got the reconciler saved somewhere...
    (dom/div {:style {:border "1px solid black"}}
      ; NOTE: A plain function...pretend this is happening outside of the UI...we're doing it here so we can embed it in the book...
      (dom/button {:onClick #(add-counter reconciler {:counter/id 4 :counter/n 22})} "Simulate Data Import")
      (dom/hr)
      "Counters:"
      (ui-counter-panel panel))))

9. Full Stack Operation

One of the most interesting and powerful things about Fulcro is that the model for server interaction is unified into a clean data-driven structure. At first the new concepts can be challenging, but once you’ve seen the core primitive (component-based queries/idents for normalization) we think you’ll find that it dramatically simplifies everything!

In fact, now that you’ve completed the materials of this guide on the graph database, queries, idents, and normalization, it turns out that the server interactions become nearly trivial!

Not only is the structure of server interaction well-defined, Fulcro come with pre-written server-side code that handle all of the Fulcro plumbing for you. You can choose to provide as little or as much as you like. The easy server code provides everything in a Ring-based stack, and you can also choose to hand-build your server using a simple API handler for the Fulcro API route.

Even then, there are a lot of possible pitfalls when writing distributed applications. People often underestimate just how hard it is to get web applications right because they forget that.

So, while the API and mechanics of how you write Fulcro server interactions are as simple as possible there is no getting around that there are some hairy things to navigate in distributed apps independent of your choice of tools. Fulcro tries to make these things apparent, and it also tries hard to make sure you’re able to get it right without pulling out your hair.

Here are some of the basics:

  • Networking is provided.

    • The protocol is EDN on the wire (via transit), which means you just speak clojure data on the wire, and can easily extend it to encode/decode new types.

  • All network requests (queries and mutations) are processed sequentially unless you specify otherwise. This allows you to reason about optimistic updates (Starting more than one at a time via async calls could lead to out-of-order execution, and impossible-to-reason-about recovery from errors).

  • You may provide fallbacks that indicate error-handling mutations to run on failures.

  • Writes and reads that are enqueued together will always be performed in write-first order. This ensures that remote reads are as current as possible.

  • Any :ui/ namespaced query elements are automatically elided when generating a query from the UI to a server, allowing you to easily mix UI concerns with server concerns in your component queries.

  • Normalization of a remote query result is automatic.

  • Deep merge of query results uses intelligent overwrite for properties that are already present in the client database.

  • Any number of remotes can be defined (allowing you to easily integrate with microservices).

  • Protocol and communication is strictly constrained to the networking layer and away from your application’s core structure, meaning you can actually speak whatever and however you want to a remote. In fact the concept of a remote is just "something you can talk to via queries and mutations". You can easily define a "remote" that reads and writes browser local storage or a Datascript database in the browser. This is an extremely powerful generalization for isolating side-effect code from your UI.

Note
To those of you with REST APIs. Fulcro can be made to work with REST, but full stack data-driven architectures are often at odds with REST. You will end up writing a network layer on your client that translates graph queries to one or more REST calls, and then combines those results into a tree of response data that you can pass back up the chain. The UI is wonderfully protected from this, but when possible you will find the most leverage by writing the server to support the graph queries directly (consider your REST API a legacy thing that your new UI doesn’t want/need). Things like GraphQL are a different story. That is a much simpler translation layer.

9.1. General Theory of Operation

There are only a few general kinds of interactions with a server:

  • Initial loads when the application starts

  • Incremental loads of sub-graphs of something that was previously loaded.

  • Event-based loads (e.g. user or timed events)

  • Integrating data from other external sources (e.g. server push)

In standard Fulcro networking, all of the above have the following similarities:

  • A component-based graph query needs to be involved (to enable auto-normalization). Even the server push (though in that case the client needs to know what implied question the server is sending data about.

  • The data from the server will be a tree that has the same shape as the query.

  • The data needs to be normalized into the client database.

  • Optionally: after integrating new data there may be some need to transform the result to a form the UI needs (e.g. perhaps you need to sort or paginate some list of items that came in).

IMPORTANT: Remember what you learned about the graph database, queries, and idents. This section cannot possibly be understood properly if you do not understand those topics!

9.1.1. Integration of ALL new external data is just a Query-Based Merge

So, here is the secret: When external data needs to go into your database it all uses the exact same mechanism: a query-based merge. So, for a simple load: you send a UI-based query to the server, the server responds with a tree of data that matches that graph query, and then the query itself (which is annotated with the components and ident functions) can be used to normalize the result. Finally, the normalized result can be merged into your existing client database.

Query --> Server --> Response + Original Query w/Idents --> Normalized Data --> Database Merge --> New Database

Any other kind of extern data integration just starts at the "Response" step by manually providing the query that is annotated with components/idents:

New External Data + Query w/Idents --> Normalized Data --> Database Merge --> New Database

There is a primitive function merge! function that implements this, so that you can simplify the picture to:

Tree of Data + Query --> merge! --> New Database

9.1.2. The Central Functions: transact! and merge!

The two core functions that allow you to trigger abstract operations or data merges externally (via a reconciler) are:

prim/transact!

The central function for running abstract changes in the application. Can be run with a component or reconciler. If run with the reconciler, will typically cause a root re-render.

prim/merge!

A function that can be run against the reconciler to merge a tree of data via a UI query. This is the primary function that is used to integrate data in response to things like websocket server push.

These (and related/derived helpers) are the primary tools to use when trying to make your application respond to external stimuli that is not triggered from the UI.

9.1.3. Query Mismatch

We have all sorts of ways we’d like to view data. Perhaps we’d like to view "all the people who’ve ever had a particular phone number". That is something we can very simply represent with a UI graph, but may not be trivial to pull from our database.

In general, there are a few approaches to resolving our graph differences:

  1. Use a query parser on the server to piece together data based on the graph of the query.

  2. Ask the server for exactly what you want, using an invented well-known "root" keyword, and hand-code the database code to create the UI-centric view.

  3. Ask the server for the data in a format it can easily provide and morph it on the client.

The first two have the advantage of making the client blissfully unaware of the server schema. It just asks for what it needs, and someone on the server programming team is stuck with satisfying the query. This is the down-side: the number of possible UI-centric queries could become quite large. Theoretically a parser solution makes this more tractable than a hand-coded variant, but in practice the parser is hard to make general in a way that allows UI developers to just run willy-nilly queries and get what they want.

In the example of "people who’ve had a particular phone number", the graph would be phone-number centric. Maybe [:db/id :phone/number {:phone/historical-owners (prim/get-query Person)}]. There probably isn’t a graph edge in the real database called :phone/historical-owners. One could write parser code that understood this particular edge, and did the logic. In this case, that really isn’t even that hard and may be a good choice.

Fulcro gives you another easy-to-access option: morph something the server can easily provide (on the real graph without custom code). We’ll show you an example of this as we explore the data fetch options.

9.1.4. Server Interaction Order

Fulcro will serialize requests unless you mark queries as parallel (an option you can specify on load). Two different events that queue mutations or loads will be processed in order. For example The user clicks on something and you trigger two loads during that event, then both of those will be combined (if they don’t conflict by querying for the same thing) sent as one network request. If the user clicks on something else and that handler queues two more loads, then the latter two loads will not start over the network until the first load sequence has completed.

This ensures that you don’t get out-of-order server execution. This is a distributed system, so it is possible for a second request to hit a less congested server (or even thread) than the first and get processed out of order. That would hurt your ability to reason about your program, so the default behavior in Fulcro is to ensure that server interactions happen on the server in the same order as they do on the client.

If you combine mutations and reads in the same event processing (before giving up the thread), then Fulcro also ensures that remote mutations go over the wire and complete before reads. The idea being that you don’t want to fetch data and then immediately make it stale through mutations. This additional detail is also aimed at preventing subtle classes of application bugs.

In Summary:

  • Loads/transactions queued while "holding the UI thread" will be joined together in a single network request.

  • Remote writes go before reads

  • Loads/transactions queued at a later event "new UI thread event" are guaranteed to be processed after ones queued earlier.

  • You can override this behavior with the parallel option of load.

9.1.5. Server Result Query Merging

There is a potential for a data-driven app to create a new class of problem related to merging data. The normalization guarantees that the data for any number of views of the same thing are normalized to the same node in the graph database.

Thus, your PersonListRow view and PersonDetail view should both normalize the same person data to a location like [:person/by-id id]. Let’s say you have a quick list of people on the screen that is paginated and demand-loaded, where each row is a PersonListRow with query [:db/id :person/name {:person/image (prim/get-query Image)}]. Now say that you can click on one of these rows and a side-by-side view of that person’s PersonDetail is shown, but the query for that is a whole bunch of stuff: name, age, address, phone numbers, etc. A much larger query.

A naive merge could cause you all sorts of nightmares. For example, Refreshing the list rows would load only name and image, but if merge overwrote the entire table entry then the current detail would suddenly empty out!

Fulcro provides you with an advanced merging algorithm that ensures these kinds of cases don’t easily occur. It does an intelligent merge with the following algorithm:

  • It is a detailed deep merge. Thus, the target table entry is updated, not overwritten.

  • If the query asks for a value but the result does not contain it, then that value is removed.

  • If the query didn’t ask for a value, then the existing database value is untouched.

This reduces the problem to one of potential staleness. It is technically possible for an entity in the resulting client database to be in a state that has never existed on the server because of such a partial update. This is considered to be a better result than the arbitrary UI madness that a more naive approach would cause.

For example, in the example above a query for a list row will update the name and image. All of the other details (if already loaded) would remain the same; however, it is possible that the server has run a mutation that also updated this person’s phone number. The detail part of the UI will be showing an updated name and image, but the old phone number. Technically this "state of Person" has never existed in the server’s timeline, but from a user’s perspective it looks better than the phone number disappearing.

In practice this isn’t that big of a deal; however, if you are switching the UI to an edit mode it is generally a good practice to on-demand load the entity being edited to help prevent user confusion and accidental overwrite based on stale values.

9.1.6. React Lifecycle and Load

React lifecycle and Load may not mix well. It is technically legal to issue transactions and loads from React Lifecycle, methods, but it is recommended that you carefully check the state of the system in said mutations before actually queuing network traffic. Perhaps you wish to ensure something is loaded. To do that: trigger a mutation that checks state and optionally submits a load. You’ll learn how to do this in the sections on the data featch API.

9.2. The Data Fetch API: load

Let’s say we want to load all of our friends from the server. A query has to be rooted somewhere, so we’ll invent a root-level keyword, :all-friends, and join it to the UI query for a Person:

[{:all-friends (prim/get-query Person)}]

What we’d like to see then from the server is a return value with the shape of the query:

{ :all-frields [ {:db/id 1 ...} {:db/id 2 ...} ...] }

If we combine that query with that tree result and were to manually call merge! we’d end up with this:

{ :all-frields [ [:person/by-id 1] [:person/by-id 2] ...]
  :person/by-id { 1 { :db/id 1 ...}
                  2 { :db/id 2 ...}
                  ...}}

The data fetch API has a simple function for this that will do all of these together (query derivation, server interaction, and merge):

(ns my-thing
  (:require [fulcro.client.data-fetch :as df]))

...

  (df/load comp-reconciler-or-app :all-friends Person)

An important thing to notice is that load can be thought of as a function that loads normalized data into the root node of your graph (:all-friends appears in your root node).

This is sometimes what you want, but more often you really want data loaded at some other spot in your graph. We’ll talk about this more in a moment, first, let’s see how to handle that on the server.

9.2.1. Handling a Root Query

If you’re using the standard server support of Fulcro, then the API hooks are already predefined for you and you can use helper macros to generate handlers for queries. If your load specified a keyword then this is seen by the server as a load targeted to your root node. Thus, the server macro is called defquery-root:

(ns api
(:require [fulcro.server :refer [defquery-root defquery-entity defmutation]]))

(defquery-root :all-friends
   "optional docstring"
   (value [env params]
      [{:db/id 1 :person/name ....} {:db/id 2 ...} ...]))

It’s as simple as that! Write a function that returns the correct value for the query. The query itself will be available in the env, and you can use libraries like pathom, Datomic, and fulcro-sql to parse those queries into the proper tree result from various data sources. See the Query Parsing chapter.

9.2.2. Entity Queries

You can also ask to load a specific entity. You do this simply by telling load the ident of the thing you’d like to load:

(load this [:person/by-id 22] Person)

Such a load will send a query to the server that is a join on that ident. The helper macro to handle those is defquery-entity, and is dispatched by the table name:

(defquery-entity :person/by-id
   "optional docstring"
   (value [env id params]
      {:db/id id ...}))

Note that this call gets an id in addition to parameters (in the above call, id would be 22). Again, the full query is in env so you can process the data driven response properly.

9.2.3. A Loading Example

This example shows two very common ways that data is loaded (or reloaded) in an application.

On startup, the following loads are run via the started callback:

(fc/new-fulcro-client
  :started-callback
    (fn [app]
      (df/load app :load-samples/people Person {:target [:lists/by-type :enemies :people]
                                                :params {:kind :enemy}})
      (df/load app :load-samples/people Person {:target [:lists/by-type :friends :people]
                                                :params {:kind :friend}})))

In this demo we’re loading lists of people (thus the keyword’s name). There are two kinds of people available from the server: friends and enemies. We use the :params config parameter to add a map of parameters to the network request to specify what we want. The :target key is the path to the (normalized) entity’s property under which the response should be stored once received.

Since all components should be normalized, this target path is almost always 2 (loading a whole component into a table) or 3 elements (loading one or more things into a property of an existing component).

The server-side code for handling these queries uses a global table on the server, and is:

(def all-users [{:db/id 1 :person/name "A" :kind :friend}
                {:db/id 2 :person/name "B" :kind :friend}
                {:db/id 3 :person/name "C" :kind :enemy}
                {:db/id 4 :person/name "D" :kind :friend}])

; incoming param (kind) is destructured from third arg
(server/defquery-root :load-samples/people
  (value [env {:keys [kind]}]
    (let [result (->> all-users
                   (filter (fn [p] (= kind (:kind p))))
                   (mapv (fn [p] (-> p
                                   (select-keys [:db/id :person/name])
                                   (assoc :person/age-ms (now))))))]
      result)))

Once started, any given person can be refreshed at any time. Timeouts, user event triggers, etc. There are two ways to refresh a given entity in a database. In the code of Person below you’ll see that we are using load again, but this time with an ident:

(df/load this (prim/ident this props) Person)

All of the parameters of this call can be easily derived when calling it from the component needing refreshed, so there is a helper function called refresh! that makes this a bit shorter to type:

(df/refresh! this)

The server-side implementation of refresh for person looks like this:

(server/defquery-entity :load-samples.person/by-id
  (value [{:keys [] :as env} id p]
    (let [person (first (filter #(= id (:db/id %)) all-users))]
      (assoc person :person/age-ms (now))))) ; include a timestamp so we can see refreshes change something
Show/Hide Source
(ns book.demos.loading-data-basics
  (:require
    [fulcro.client :as fc]
    [fulcro.client.data-fetch :as df]
    [book.demos.util :refer [now]]
    [fulcro.client.mutations :as m]
    [fulcro.client.dom :as dom]
    [fulcro.client.primitives :as prim :refer [defsc InitialAppState initial-state]]
    [fulcro.client.data-fetch :as df]
    [fulcro.server :as server]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SERVER:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def all-users [{:db/id 1 :person/name "A" :kind :friend}
                {:db/id 2 :person/name "B" :kind :friend}
                {:db/id 3 :person/name "C" :kind :enemy}
                {:db/id 4 :person/name "D" :kind :friend}])

(server/defquery-entity :load-samples.person/by-id
  (value [{:keys [] :as env} id p]
    (let [person (first (filter #(= id (:db/id %)) all-users))]
      (assoc person :person/age-ms (now)))))

(server/defquery-root :load-samples/people
  (value [env {:keys [kind]}]
    (let [result (->> all-users
                   (filter (fn [p] (= kind (:kind p))))
                   (mapv (fn [p] (-> p
                                   (select-keys [:db/id :person/name])
                                   (assoc :person/age-ms (now))))))]
      result)))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CLIENT:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defsc Person [this {:keys [db/id person/name person/age-ms] :as props}]
  {:query [:db/id :person/name :person/age-ms :ui/fetch-state]
   :ident (fn [] [:load-samples.person/by-id id])}
  (dom/li
    (str name " (last queried at " age-ms ")")
    (dom/button {:onClick (fn []
                            ; Load relative to an ident (of this component).
                            ; This will refresh the entity in the db. The helper function
                            ; (df/refresh! this) is identical to this, but shorter to write.
                            (df/load this (prim/ident this props) Person))} "Update")))

(def ui-person (prim/factory Person {:keyfn :db/id}))

(defsc People [this {:keys [people]}]
  {:initial-state (fn [{:keys [kind]}] {:people/kind kind})
   :query         [:people/kind {:people (prim/get-query Person)}]
   :ident         [:lists/by-type :people/kind]}
  (dom/ul
    ; we're loading a whole list. To sense/show a loading marker the :ui/fetch-state has to be queried in Person.
    ; Note the whole list is what we're loading, so the render lambda is a map over all of the incoming people.
    (df/lazily-loaded #(map ui-person %) people)))

(def ui-people (prim/factory People {:keyfn :people/kind}))

(defsc Root [this {:keys [friends enemies]}]
  {:initial-state (fn [{:keys [kind]}] {:friends (prim/get-initial-state People {:kind :friends})
                                        :enemies (prim/get-initial-state People {:kind :enemies})})
   :query         [{:enemies (prim/get-query People)} {:friends (prim/get-query People)}]}
  (dom/div
    (dom/h4 "Friends")
    (ui-people friends)
    (dom/h4 "Enemies")
    (ui-people enemies)))

(defn initialize
  "To be used in :started-callback to pre-load things."
  [app]
  ; This is a sample of loading a list of people into a given target, including
  ; use of params. The generated network query will result in params
  ; appearing in the server-side query, and :people will be the dispatch
  ; key. The subquery will also be available (from Person). See the server code above.
  (df/load app :load-samples/people Person {:target [:lists/by-type :enemies :people]
                                            :params {:kind :enemy}})
  (df/load app :load-samples/people Person {:target [:lists/by-type :friends :people]
                                            :params {:kind :friend}}))

9.2.4. Targeting Loads

A short while ago we noted that loads are targeted at the root of your graph, and that this wasn’t always what you wanted. After all, your graph database will always have other UI stuff. For example there might be the concept of a "current screen" (join from root) that might currently point to a "friends screen", which in turn is where you want to load that list of friends:

{ :current-screen [:screens/by-type :friends]
  ...
  :screens/by-type { :friends { :friends/list [] ... }}}

If you’ve followed our earlier recommendations then your application’s UI is normalized and any given spot in your graph is really just an entry in a top-level table. Thus, the path to the desired location of our friends is usually just 3 deep. In this case: [:screens/by-type :friends :friends/list].

If we were to merge the earlier load into that database we could get what we want by just moving graph edges (where a to-one edge is an ident, and a to-many edge is a vector of idents):

{ :all-frields [ [:person/by-id 1] [:person/by-id 2] ...]  ; (1) MOVE this to (2)
  :person/by-id { 1 { :db/id 1 ...}
                  2 { :db/id 2 ...}
  :current-screen [:screens/by-type :friends]
  :screens/by-type { :friends { :friends/list [] ... }}} ; (2) where friends *should* be

Since the graph database is just nodes and edges, there really aren’t many more operations to worry about! You’ve essentially got to normalize a tree, merge normalized data, and move graph edges.

The load API supports several kinds of graph edge targeting to allow you to put data where you want it in your graph.

Warning
Technically data is always loaded into the root, then relocated. So, be careful not to name your top-level edge after something that already exists there!
Simple Targeting

The simplest targeting is to just relocate an edge from the root to somewhere else. The load function can do that with a simple additional parameter:

(df/load comp :all-friends Person {:target [:screens/by-type :friends :friends/list]})

So, the server will still see the well-known query for :all-friends, but your local UI graph will end up seeing the results in the list on the friends screen.

Advanced Targets

You can also ask the target parameter to modify to-many edges instead of replacing them. For example, say you were loading one new person, and wanted to append it to the current list of friends:

(df/load comp :best-friend Person {:target (df/append-to [:screens/by-type :friends :friends/list])})

The append-to function in the data-fetch namespace augments the target to indicate that the incoming items (which will be normalized) should have their idents appended onto the to-many edge found at the given location.

Note
append-to will not create duplicates.

The other available helper is prepend-to. Using a plain target is equivalent to full replacement.

You may also ask the targeting system to place the result(s) at more than one place in your graph. You do this with the multiple-targets wrapper:

(df/load comp :best-friend Person {:target (df/multiple-targets
                                              (df/append-to [:screens/by-type :friends :friends/list])})
                                              [:other-spot]
                                              (df/prepend-to [:screens/by-type :summary :friends]))})

Note that multiple-targets can use plain target vectors (replacement) or any of the special wrappers.

An Example of Targeting

In the example below, you’ll see we’ve set up two panes in a panel, and each pane can render a person.

Initially, there are no people loaded (none in initial state). The load buttons in the root component let you see entity loads targeted at one or both of them.

The main loader looks like this (and is called from root):

(defn load-random-person [component where]
  (let [load-target  (case where
                       (:left :right) [:pane/by-id where :pane/person]
                       :both (df/multiple-targets
                               [:pane/by-id :left :pane/person]
                               [:pane/by-id :right :pane/person]))
        person-ident [:person/by-id (rand-int 100)]]
    (df/load component person-ident Person {:target load-target :marker false})))

The server is rather simple. It just makes up a person for any given ID:

(server/defquery-entity :person/by-id
  (value [env id params]
    {:db/id id :person/name (str "Person" id)}))
Show/Hide Source
(ns book.demos.loading-data-targeting-entities
  (:require
    [fulcro.client.mutations :as m]
    [fulcro.client.primitives :as prim :refer [defsc]]
    [fulcro.client.dom :as dom]
    [fulcro.server :as server]
    [fulcro.client.data-fetch :as df]
    [fulcro.client.primitives :as prim]))

;; SERVER

(server/defquery-entity ::person-by-id
  (value [env id params]
    {:db/id id :person/name (str "Person " id)}))

;; CLIENT

(defsc Person [this {:keys [person/name]}]
  {:query [:db/id :person/name]
   :ident [::person-by-id :db/id]}
  (dom/div (str "Hi, I'm " name)))

(def ui-person (prim/factory Person {:keyfn :db/id}))

(defsc Pane [this {:keys [db/id pane/person] :as props}]
  {:query         [:db/id {:pane/person (prim/get-query Person)}]
   :initial-state (fn [{:keys [id]}] {:db/id id :pane/person nil})
   :ident         [:pane/by-id :db/id]}

  (dom/div
    (dom/h4 (str "Pane " id))
    (if person
      (ui-person person)
      (dom/div "No person loaded..."))))

(def ui-pane (prim/factory Pane {:keyfn :db/id}))

(defsc Panel [this {:keys [panel/left-pane panel/right-pane]}]
  {:query         [{:panel/left-pane (prim/get-query Pane)}
                   {:panel/right-pane (prim/get-query Pane)}]
   :initial-state (fn [params] {:panel/left-pane  (prim/get-initial-state Pane {:id :left})
                                :panel/right-pane (prim/get-initial-state Pane {:id :right})})
   :ident         (fn [] [:PANEL :only-one])}
  (dom/div
    (ui-pane left-pane)
    (ui-pane right-pane)))

(def ui-panel (prim/factory Panel {:keyfn :db/id}))

(defn load-random-person [component where]
  (let [load-target  (case where
                       (:left :right) [:pane/by-id where :pane/person]
                       :both (df/multiple-targets
                               [:pane/by-id :left :pane/person]
                               [:pane/by-id :right :pane/person]))

        person-ident [::person-by-id (rand-int 100)]]
    (df/load component person-ident Person {:target load-target :marker false})))

(defsc Root [this {:keys [root/panel] :as props}]
  {:query         [{:root/panel (prim/get-query Panel)}]
   :initial-state (fn [params] {:root/panel (prim/get-initial-state Panel {})})}
  (dom/div
    (ui-panel panel)
    (dom/button {:onClick #(load-random-person this :left)} "Load into Left")
    (dom/button {:onClick #(load-random-person this :right)} "Load into Right")
    (dom/button {:onClick #(load-random-person this :both)} "Load into Both")))

9.2.5. Refreshing the UI After Load

The component that issued the load will automatically be refreshed when the load completes. You may use the data-driven nature of the app to request other components refresh as well. The :refresh option tells the system what data has changed due to the load. It causes all live components that have queried those things to refresh. You can supply keywords and/or idents:

; load my best friend, and re-render every live component that queried for the name of a person
(df/load comp :best-friend Person {:refresh [:person/name]})

9.2.6. Other Load Options

Loads allow a number of additional arguments. Many of these are discussed in more detail in later sections:

:post-mutation and :post-mutation-params

A mutation to run once the load is complete (local data transform only).

:remote

The name of the remote you want to load from.

:refresh

A vector of keywords and idents. Any component that queries these will be re-rendered once the load completes.

:parallel

Boolean. Defaults to false. When true, bypasses the sequential network queue. Allows multiple loads to run at once, but causes you to lose any guarantees about ordering since the server might complete them out-of-order.

:fallback

A mutation to run if the server throws an error during the load.

:focus

A subquery to filter from your component query. Covered in Incremental Loading.

:without

A set of keywords to elide from the query. Covered in Incremental Loading.

:params

A map. If supplied the params will appear as the params of the query on the server.

:initialize bool|map

If true, uses component’s initial state as a basis for incoming merge. If a map, uses the map as the basis for incoming merge.

Parallel vs. Sequential Loading

The :parallel option of load bypasses the normal network sequential queue. Below is a simple live example that shows off the difference between regular loads and those marked parallel. In order to see the effect increase your server latency to something like 5 seconds.

Normally, Fulcro runs separate event-based loads in sequence, ensuring that your reasoning can be synchronous; however, for loads that might take some time to complete, and for which you can guarantee order of completion doesn’t matter, you can specify an option on load (:parallel true) that allows them to proceed in parallel.

Pressing the sequential buttons on all three (in any order) will take at least 3x your server latency to complete from the time you click the first one (since each will run after the other is complete). If you rapidly click the parallel buttons, then the loads will not be sequenced and you will see them all complete in roughly 1x your server latency (from the time you click the last one).

Show/Hide Source
(ns book.demos.parallel-vs-sequential-loading
  (:require
    [fulcro.server :refer [defquery-root defquery-entity defmutation]]
    [fulcro.client.data-fetch :as df]
    [fulcro.client.dom :as dom]
    [fulcro.client.primitives :as prim :refer [defsc]]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SERVER:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defquery-entity :background.child/by-id
  (value [{:keys [parser query] :as env} id params]
    (when (= query [:background/long-query])
      (parser env query))))

(defquery-root :background/long-query
  (value [{:keys [ast query] :as env} params] 42))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CLIENT:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn render-result [v] (dom/span v))

(defsc Child [this {:keys [name name background/long-query]}]
  {:query [:id :name :background/long-query]
   :ident [:background.child/by-id :id]}
  (dom/div {:style {:display "inline" :float "left" :width "200px"}}
    (dom/button {:onClick #(df/load-field this :background/long-query :parallel true)} "Load stuff parallel")
    (dom/button {:onClick #(df/load-field this :background/long-query)} "Load stuff sequential")
    (dom/div
      name
      (df/lazily-loaded render-result long-query))))

(def ui-child (prim/factory Child {:keyfn :id}))

(defsc Root [this {:keys [children] :as props}]
  ; cheating a little...raw props used for child, instead of embedding them there.
  {:initial-state (fn [params] {:children [{:id 1 :name "A"} {:id 2 :name "B"} {:id 3 :name "C"}]})
   :query         [{:children (prim/get-query Child)}]}
  (dom/div
    (mapv ui-child children)
    (dom/br {:style {:clear "both"}}) (dom/br)))
Initializing Loaded Items
Warning
This support is not complete yet (in version 2.0.0). It should be considered ALPHA and subject to change.

On occasion you may find that your entities have :ui/??? attributes that you would like to default to something on a loaded entity. This is the purpose of the :initialize option to load. If it is set to true, then load will call get-initial-state on the component of the load, and merge the return value from the server into that before merging it to app state.

Alternatively, you can pass :initialize a map, and that will be used as the target for the server response merge before normalizing the result into app state.

Note
The value of :initialize must either be true or a map that matches the correct shape of the component’s sub-tree of data. It must not be a normalized database fragment.

The steps are:

  1. Send the request

  2. Merge the response into the basis defined by :initialize.

  3. Merge the result of (2) into the database using the component’s query (auto-normalize)

Post Mutations – Morphing Loaded Data

The targeting system that we discussed in the prior section is great for cases where your data-driven query gets you exactly what you need for the UI. In fact, since you can process the query on the server it is entirely possible that load with targeting is all you’ll ever need; however, from a practical perspective it may turn out that you’ve got a server that can easily understand certain shapes of data-driven queries, but not others.

For example, say you were pulling a list of items from a database. It might be trivial to pull that graph of data from the server from the perspective of a list of items, but let’s say that each item had a category. Perhaps you’d like to group the items by category in the UI.

The data-driven way to handle that is to make the server understand the UI query that has them grouped by category; however, that implies that you might end up embedding code on your server to handle a way of looking at data that is really specific to one kind of UI. That tends to push us back toward a proliferation of code on the server that was a nightmare in REST.

Another way of handling this is to accept the fact that our data-driven queries have some natural limits: If the database on the server can easily produce the graph, then we should let it do so from the data-driven query; however, in some cases it may make more sense to let the UI morph the incoming data into a shape that makes more sense to that UI.

server interactions

We all understand doing these kinds of transforms. It’s just data manipulation. So, you may find this has some distinct advantages:

  • Simple query to the server (only have to write one query handler) that is a natural fit for the database there.

  • Simple layout in resulting UI database (normalized into tables and a graph)

  • Straightforward data transform into what we want to show

Using defsc For Server Queries

It is perfectly legal to use defsc to define a graph query (and normalization) for something like this that doesn’t exactly exist on your UI. This can be quite useful in the presence of post mutations that can re-shape the data.

Simply code your (nested) queries using defsc, and skip writing the body:

(defsc ItemQuery [this props]
  {:query [:db/id :item/name {:item/category (prim/get-query CategoryQuery)}]
   :ident [:items/by-id :db/id]})
(df/load this :all-items ItemQuery {:post-mutation `group-items})
Note
We know that the name defsc seems a bit of a misnomer for this, so feel free to create an alias for it.
Live Demo of Post Mutations

The example below simulates post mutations to show how a load of simple data could be morphed into something that the UI wants to display. In this case we’re pretending that the load has brought in a number of items (as a collection) and normalized it, but we’d prefer to show the items organized by category..

You can interact with it and view the database to A/B compare the before/after state.

Show/Hide Source
(ns book.server.morphing-example
  (:require [fulcro.client.primitives :as prim :refer [defsc]]
            [fulcro.client.dom :as dom]
            [book.macros :refer [defexample]]
            [fulcro.client.cards :refer [defcard-fulcro]]
            [fulcro.client.mutations :refer [defmutation]]
            [fulcro.client :as fc]))

(defsc CategoryQuery [this props]
  {:query [:db/id :category/name]
   :ident [:categories/by-id :db/id]})

(defsc ItemQuery [this props]
  {:query [:db/id :item/name {:item/category (prim/get-query CategoryQuery)}]
   :ident [:items/by-id :db/id]})

(def sample-server-response
  {:all-items [{:db/id 5 :item/name "item-42" :item/category {:db/id 1 :category/name "A"}}
               {:db/id 6 :item/name "item-92" :item/category {:db/id 1 :category/name "A"}}
               {:db/id 7 :item/name "item-32" :item/category {:db/id 1 :category/name "A"}}
               {:db/id 8 :item/name "item-52" :item/category {:db/id 2 :category/name "B"}}]})

(def component-query [{:all-items (prim/get-query ItemQuery)}])

(def hand-written-query [{:all-items [:db/id :item/name
                                      {:item/category [:db/id :category/name]}]}])

(defsc ToolbarItem [this {:keys [item/name]}]
  {:query [:db/id :item/name]
   :ident [:items/by-id :db/id]}
  (dom/li name))

(def ui-toolbar-item (prim/factory ToolbarItem {:keyfn :db/id}))

(defsc ToolbarCategory [this {:keys [category/name category/items]}]
  {:query [:db/id :category/name {:category/items (prim/get-query ToolbarItem)}]
   :ident [:categories/by-id :db/id]}
  (dom/li
    name
    (dom/ul
      (map ui-toolbar-item items))))

(def ui-toolbar-category (prim/factory ToolbarCategory {:keyfn :db/id}))

(defmutation group-items-reset [params]
  (action [{:keys [state]}]
    (reset! state (prim/tree->db component-query sample-server-response true))))

(defn add-to-category
  "Returns a new db with the given item added into that item's category."
  [db item]
  (let [category-ident (:item/category item)
        item-location  (conj category-ident :category/items)]
    (update-in db item-location (fnil conj []) (prim/ident ItemQuery item))))

(defn group-items*
  "Returns a new db with all of the items sorted by name and grouped into their categories."
  [db]
  (let [sorted-items   (->> db :items/by-id vals (sort-by :item/name))
        category-ids   (-> db (:categories/by-id) keys)
        clear-items    (fn [db id] (assoc-in db [:categories/by-id id :category/items] []))
        db             (reduce clear-items db category-ids)
        db             (reduce add-to-category db sorted-items)
        all-categories (->> db :categories/by-id vals (mapv #(prim/ident CategoryQuery %)))]
    (assoc db :toolbar/categories all-categories)))

(defmutation ^:intern group-items [params]
  (action [{:keys [state]}]
    (swap! state group-items*)))

(defsc Toolbar [this {:keys [toolbar/categories]}]
  {:query [{:toolbar/categories (prim/get-query ToolbarCategory)}]}
  (dom/div
    (dom/button {:onClick #(prim/transact! this `[(group-items {})])} "Trigger Post Mutation")
    (dom/button {:onClick #(prim/transact! this `[(group-items-reset {})])} "Reset")
    (dom/ul
      (map ui-toolbar-category categories))))

(defexample "Morphing Data" Toolbar "morphing-example" :initial-state (atom (prim/tree->db component-query sample-server-response true)))

9.3. Augmenting the Ring Response

The Ring stack is supplied for you in the server code and most responses are simple EDN with the HTTP details taken care of for you; however, there are times when you need to modify something about the low-level response itself (such as adding a cookie).

If you’re using the Fulcro server tools (handle API request or the easy server) then you can add a fully general response transform to your EDN response as follows:

(fulcro.server/augment-response your-EDN-response
  (fn [ring-response]
     ...
     modified-ring-response))

For example, if you were using Ring and Ring Session, you could cause a session cookie to be generated, and user information to be stored in a server session store simply by returning this from a query on user:

(server/augment-response user (fn [resp] (assoc-in resp [:session :uid] real-uid)))

9.4. Full-Stack Mutations

You’ve already seen how to define local mutations on the client. In this chapter we’re going to go into the details of making mutations work against the server.

9.5. Remote Mutations

Mutations are already plain data, so Fulcro can pass them over the network as-is when the client invokes them. All you need to do to indicate that a given mutation should affect a given remote is add that remote to the mutation:

(defmutation add-friend [params]
  (action ...) ; optimistic update of client state
  (remote [env] true)) ; send the mutation to the remote known as :remote

; or using the multimethod directly:
(defmethod m/mutate `add-friend [env k params]
  {:action (fn [] ...)
   :remote true})

Now you can see why choosing the right real mutations and amount of composition on the client can give you optimal server interaction: Anything that you run in transact! itself can stand alone as a remote mutation call in the transaction on the wire.

9.5.1. Optimistic by Default

The action portion of a mutation is run immediately on the client. When there is also a server interaction then the client-side operation is known as an optimistic update because by default we assume that the server will succeed in replicating the action. This gives the user immediate feedback and the ability to proceed quickly even in the presence of a slow network. We’ll discuss more on error handling shortly.

9.5.2. Mutations Revisited

A multi-method in Fulcro client (which is manipualted with defmutation) can indicate that a given mutation should be sent to any number of remotes. The default remote is named :remote, but you can define new ones or even rename the default one.

The technical structure of this looks like:

(require [fulcro.client.mutations :refer [mutate]])

(defmethod mutate 'some/mutation [env k params]
  ;; sends this mutation with the same `env`, `k`, and `params` arguments to the server
  {:remote true
   :action (fn[] ... )})

or preferably using the defmutation macro:

(defmutation do-thing [params]
  (action [env] ...)
  (remote [env] true))

Basically, you use the name of the remote as an indicator of which remote you want to replicate the mutation to. From there you either return true (which means send the mutation as-is), or you may return an expression AST that represents the mutation you’d like to send instead. The fulcro.client.primitives namespace includes ast→query and query-ast for arbitrary generation of ASTs, but the original AST of the mutation is also available in the mutation’s environment.

Therefore, you can alter a mutation simply by altering and returning the ast given in env:

(defmutation do-thing [params]
  (action [env] ...)
  ; send (do-thing {:x 1}) even if params are different than that on the client
  (remote [{:keys [ast]}] (assoc ast :params {:x 1})) ; change the param list for the remote

; or using the with-params helper
(defmutation do-thing [params]
  (action [env] ...)
  ; send (do-thing {:x 1}) even if params are different than that on the client
  (remote [{:keys [ast]}] (m/with-params ast {:x 1})) ; change the param list for the remote

or even change which mutation the server sees:

(defmutation some-mutation [ params]
  ;; Changes the mutation from the incoming client-side some-mutation to server-mutation
  (remote [{:keys [ast] :as env}] (assoc ast :key 'server-mutation :dispatch-key 'server-mutation)))
Warning
The state is available in remote, but the action will run first. This means that you should not expect the "old" values in state when computing anything for the remote because the optimistic update of the action will have already been applied! If you need to rely on data as it existed at the time of transact! then you must pass it as a parameter to the mutation so that the original data is closed over for the duration of the mutation processing.

9.5.3. Writing The Server Mutations

Server-side mutations in Fulcro are written the same way as on the client: A mutation returns a map with a key :action and a function of no variables as a value. The mutation then does whatever server-side operation is indicated. The env parameter on the server can contain anything you like (for example database access interfaces). You’ll see how to configure that when you study how to build a server.

The recommended approach to writing a server mutation is to use the pre-written server-side parser and multimethods, which allow you to mimic the same code structure of the client (there is a defmutation in fulcro.server for this).

If you’re using this approach (which is the default in the easy server), then here are the client-side and server-side implementations of the same mutation:

;; client
;; src/my_app/mutations.cljs
(ns my-app.mutations
  (:require [fulcro.client.mutations :refer [defmutation]]))

(defmutation do-something [{:keys [p]}]
  (action [{:keys [state]}]
    (swap! state assoc :value p))
  (remote [env] true))
;; server
;; src/my_app/mutations.clj
(ns my-app.mutations
  (:require [fulcro.server :refer [defmutation]]))

(defmutation do-something [{:keys [p]}]
  (action [{:keys [some-server-database]}]
     ... server code to make change ...))

It is recommended that you use the same namespace on the client and server for mutations so it is easy to find them, but the macro allows you to namespace the symbol if you choose to use a different namespace on the server:

(ns my-app.server
  (:require [fulcro.server :refer [defmutation]]))

(defmutation my-app.mutations/do-something [{:keys [p]}]
  (action [{:keys [some-server-database]}]
     ... server code to make change ...))

The ideal structure for many people is to use a CLJC file, where they can co-locate the mutations in the same source file. The only trick is that you have to make sure you use the correct defmutation!

(ns my-app.api
  (:require
    #?(:clj [fulcro.server :as server]
       :cljs [fulcro.client.mutations :refer [defmutation]])))

  #?(:clj (server/defmutation do-thing [params]
          (action [server-env] ...))
    :cljs (defmutation do-thing [params]
            (remote [env] true)))
Note
The server namespace includes support for defining a server mutation in the browser. This allows you to simulate a server in the browser instead of having to have a real server.

Please see Building A Server for more information about setting up a server with injected components in the mutation environment.

The Server Multimethods for Mutations

The defmutation macro on the server simply hits a multimethod. You can use defmethod on fulcro.server/server-mutate to define your mutations. The advantage of this is that you might want to write your own wrappers, macros, or code around the low-level implementation.

In general, we recommend using defmutation because it is better supported by IDEs (for navigation, docstrings, etc) and eliminates some classes of syntactic error.

9.5.4. New item creation – Temporary IDs

Fulcro has a built in function prim/tempid that will generate a unique temporary ID. This allows the normalization and denormalization of the client side database to continue working while the server processes the new data and returns the permanent identifier(s).

The idea is that these temporary IDs can be safely placed in your client database (and network queues), and will be automatically rewritten to their real ID when the server has managed to create the real persistent entity. Of course, since you have optimistic updates on the client it is important that things go in the correct sequence, and that queued operations for the server don’t get confused about what ID is correct!

Warning
Because mutation code can be called multiple times (at least once + once per each remote), you should take care to not call fulcro.client.primitives/tempid inside your mutation. Instead call it from your UI code that builds the mutation params.

Fulcro’s implementation works as follows:

  1. Mutations always run in the order specified in the call to transact!

  2. Transmission of separate calls to transact! run in the order they were called.

  3. If remote mutations are separated in time, then they go through a sequential networking queue, and are processed in order.

  4. As mutations complete on the server, they return tempid remappings. Those are applied to the application state and network queue before the next network operation (load or mutation) is sent.

This set of rules helps ensure that you can reason about your program, even in the presence of optimistic updates that could theoretically be somewhat ahead of the server.

For example, you could create an item, edit it, then delete it. The UI responds immediately, but the initial create might still be running on the server. This means the server has not even given it a real ID before you’re queuing up a request to delete it! With the above rules, it will just work! The network queue will have two backlogged operations (the edit and the delete), each with the same tempid that you currently know. When the create finally returns it will automatically rewrite all of the tempids in state and the network queues, then send the next operation. Thus, the edit will apply to the current server entity, as will the delete.

All the server code has to do is return a map with the special key :fulcro.client.primitives/tempids (or the legacy :tempids) whose value is a map of tempid→realid whenever it sees an ID during persistence operations. Here are the client-side and server-side implementations of the same mutation that create a new item:

;; client
;; src/my_app/mutations.cljs
(ns my-app.mutations
  (:require [fulcro.client.mutations :refer [defmutation]]))

(defmutation new-item [{:keys [tempid text]}]
  (action [{:keys [state]}]
    (swap! state assoc-in [:item/by-id tempid] {:db/id tempid :item/text text}))
  (remote [env] true))
;; server
;; src/my_app/mutations.clj
(ns my-app.mutations
  (:require [fulcro.client.primitives :as prim]
            [fulcro.server :refer [defmutation]]))

(defmutation new-item [{:keys [tempid text]}]
  (action [{:keys [state]}]
    (let [database-tempid (make-database-tempid)
          database-id (add-item-to-database database {:db/id database-tempid :item/text text})]
      {::prim/tempids {tempid database-id}})))

Other mutation return values are covered in Mutation Return Values.

9.5.5. Remote Reads After a Mutation

In earlier sections you learned that you can list properties with your mutation to indicate re-renders. These follow-on read keywords are always local re-render reads, and nothing more:

(prim/transact! this '[(app/f) :thing])
; Does mutation, and re-renders anything that has :thing in a query

Fulcro will automatically queue remote reads after writes when they are submitted in the same thread interaction:

(prim/transact! this `[(f)])
(df/load this :thing Thing)
(prim/transact! this `[(g)])

will result in two network interactions. The first will run [(f) (g)], and the second will be a load of :thing. This is a defined and official behavior.

Thus, one way you can implement a sequence of mutation followed by learning a result is to run a mutation and a load.

9.5.6. Pessimistic Transactions

There are scenarios where the above behavior is not what you want. In particular are cases like form submission where you might want to wait until the server completes, so that the user can be kept in the form until you’ve confirmed the server isn’t down or something.

Fulcro 2.0+ has support for pessimistic transactions that enable exactly this sort of behavior:

(prim/ptransact! this `[(a) (b)])

Will run `a’s action, `a’s remote, then `b’s action and `b’s remote. This can be combined with analysis of mutation return values to allow you to follow a remote operation with a UI one:

; assume submit-my-form blocks the UI, and leave-form-if-ok checks app state and moves on.
(prim/ptransact! this `[(submit-my-form) (leave-form-if-ok)])
Warning
Use caution when using mutations with conditional remote behavior. ptransact! detects which mutations are remote by pre-running them (they are side-effect free) against the app state as it is at the beginning of the transaction. If you have a mutation in the middle that relies on the state modifications of a prior mutation in the same transaction in order to decide if it is remote then it will be mis-detected.

9.5.7. Using Loads as Mutations

There is technically nothing wrong with issuing a load that has side-effects on the server (though one could argue that this is a bit sketchy from a design perspective). For example, one way to implement login is to issue a load with the user’s credentials:

(df/load :current-user User {:params {:username u :password p}})

The server query response can validate the credentials, set a cookie, and return the user info all at once! Your UI can simply base rendering on the values in :current-user. If they’re valid, you’re logged in.

If you remember from the General Operations section, you can modify the low-level Ring response by associating a lambda with your return value. If you were using Ring Session, then this might be how the query would be implemented on the server:

(ns my-api
  (:require [fulcro.server :as server :refer [defmutation defquery-root]]))

(def bad-user {:db/id 0})

(defquery-root :current-user
  (value [env params]
    (if-let [{:keys [db/id] :as user} (authenticate params)] ; look up the user, return nil if bad
      (server/augment-response user (fn [resp] (assoc-in resp [:session :uid] id)))
      bad-user)))

9.5.8. Running Mutations in the Context of an Entity

If you submit a transaction and include an ident:

(transact! reconciler [:person/by-id 4] `[(f)])

then the transaction will run as-if it were executed in the context of any live component on the screen that currently has that ident. This will make the ident available in the mutation’s environment as :ref, and will focus refresh at that component sub-tree(s). This can be useful when you have out-of-band data that causes you to want to run a transaction outside of the UI using the reconciler.

9.5.9. Mutations that Trigger one or more Loads

Mutations generally need not expose their full-stack nature to the UI. For example a next-page mutation might trigger a load for the next page of data or simply swap in some already cached data. The UI need not be aware of the logic of this distinction (though typically the UI will want to include loading markers, so it is common for there to be some kind of knowledge about lazy loading).

Instead of coding complex "do I need to load that?" logic in the UI (where it most certainly does not belong) one should instead write mutations that abstract it into a nice concept.

Fulcro handles loads by placing load markers into a special place in the application database. Whenever a remote operation is triggered, the networking layer will check this queue and process it.

The fulcro.client.data-detch/load function simply runs a transact! that does both (adds the load to the queue and triggers remote processing).

If you’d like to compose one or more loads into a mutation, there are helper functions that will help you do just that: df/load-action and df/remote-load.

The basic pattern is:

(defmutation next-page [params]
  (action [{:keys [state] :as env}]
    (swap! state ...) ; local optimistic db updates
    (df/load-action env :prop Component {:remote :remote}) ; same basic args as `load`, except state atom instead of `this`
    (df/load-action env :other Other) {:remote :other-remote}) ; as many as you need...
  (remote [env]
    (df/remote-load env))) ; notifies back-end that there is some loading to do
Important
The remote-load call need only be done for any one of the remotes you’re talking to. It merely tells the back-end code to process remote requests (which will hit all remote queues). Thus, the parameters to the load-action calls are where you actually specify which remote a given load should actually talk to.

9.6. Network Activity Indicators

Rendering is completely up to you, but Fulcro handles the networking (technically this is pluggable, but Fulcro still initiates the interactions). That means that you’re going to need some help when it comes to showing the user that something is happening on the network.

The first and easiest thing to use is a global activity marker that is automatically maintained at the root node of your client database.

9.6.1. Global network activity marker

Fulcro will automatically maintain a global network activity marker at the top level of the app state under the keyword :ui/loading-data. This key will have a true value when there are network requests awaiting a response from the server, and will have a false value when there are no network requests in process.

You can access this marker from any component that composes to root by including a link in your component’s query:

(defsc Item ... )
(def ui-item (prim/factory Item {:keyfn :id}))

(defsc List [this {:keys [title items ui/loading-data]}]
  {:query [:id :title {:items (prim/get-query Item)} [:ui/loading-data '_]]}
  ...
  (if (and loading-data (empty? items))
    (dom/div "Loading...")
    (dom/div
      (dom/h1 title)
      (map ui-item items))))

Because the global loading marker is at the top level of the application state, do not use the keyword as a follow-on read to mutations because it may unnecessarily trigger a re-render of the entire application.

9.6.2. Mutation Activity

Mutations can be passed off silently to the server. You may choose to block the UI if you have reason to believe there will be a problem, but there is usually no other reason to prevent the user from just continuing to use your application while the server processes the mutation. Thus, only the global activity marker is available for mutations. See Pessimistic Transactions for a method of controlling UI around the network activity of remote mutations.

9.6.3. Tracking Specific Loads

Loads are a different story. It is very often the case that you might have a number of loads running to populate different parts of your UI all at once. In this case it is quite useful to have some kind of load-specific marker that you can use to show that activity.

In Fulcro 1.0+ this can be done as follows:

  • The target of each load is replaced by a load marker until the load completed

  • You can detect these load markers and show an alternate UI while they are loading.

    • The component that is to be loaded must include :ui/fetch-state in its query (this is the key under which the marker is placed)

    • The data-fetch namespace has utility functions for detecting the state of the marker, though usually just its presence is enough.

  • When the data arrives the load marker is replaced by it.

A Live Example of 1.x Load Indicators

By default Fulcro places markers where the items will appear that are being loaded. These markers can be used to show progress indicators in the UI.

In the demo below the first button triggers a load of a child’s data from the server. Use the server latency controls to slow things down so you can see the markers. Once the child is loaded, a button appears indicating items can be loaded into that child.

Once the items are loaded, each has a refresh button. Again, use the server delay so you can watch the markers.

Show/Hide Source
(ns book.demos.legacy-load-indicators
  (:require
    [fulcro.client.primitives :as prim :refer [defsc]]
    [fulcro.client.data-fetch :as df]
    [fulcro.logging :as log]
    [fulcro.server :refer [defquery-entity]]
    [fulcro.client.dom :as dom]))

;; SERVER

(defquery-entity :lazy-load/ui
  (value [env id params]
    (case id
      :panel {:child {:db/id 5 :child/label "Child"}}
      :child {:items [{:db/id 1 :item/label "A"} {:db/id 2 :item/label "B"}]}
      nil)))

(defquery-entity :lazy-load.items/by-id
  (value [env id params]
    (log/info "Item query for " id)
    {:db/id id :item/label (str "Refreshed Label " (rand-int 100))}))

;; CLIENT

(declare Item)

(defsc Item [this {:keys [db/id item/label ui/fetch-state] :as props}]
  ;; The :ui/fetch-state is queried so the parent (Child in this case) lazy load renderer knows what state the load is in
  {:query [:db/id :item/label :ui/fetch-state]
   :ident [:lazy-load.items/by-id :db/id]}
  (dom/div nil label
    ; If an item is rendered, and the fetch state is present, you can use helper functions from df namespace
    ; to provide augmented rendering.
    (if (df/loading? fetch-state)
      (dom/span nil " (reloading...)")
      ; the `refresh!` function is a helper that can send an ident-based join query for a component.
      ; it is equivalent to `(load reconciler [:lazy-load.items/by-id ID] Item)`, but finds the params
      ; using the component itself.
      (dom/button {:onClick #(df/refresh! this)} "Refresh"))))

(def ui-item (prim/factory Item {:keyfn :db/id}))

(defsc Child [this {:keys [child/label items] :as props}]
  ;; The :ui/fetch-state is queried so the parent (Panel) lazy load renderer knows what state the load is in
  {:query [:ui/fetch-state :child/label {:items (prim/get-query Item)}]
   :ident (fn [] [:lazy-load/ui :child])}
  (let [; NOTE: Demostration of two ways of showing an item is refreshing...
        render-item (fn [idx i] (if (= idx 0)
                                  (ui-item i)               ; use the childs method of showing refresh
                                  (dom/span {:key (str "ll-" idx)} ; the span is so we have a react key in the list
                                    (df/lazily-loaded ui-item i)))) ; replace child with a load marker
        render-list (fn [items] (map-indexed render-item items))]
    (dom/div nil
      (dom/p "Child Label: " label)
      ; Rendering for all of the states can be supplied to lazily-loaded as named parameters
      (df/lazily-loaded render-list items
        :not-present-render (fn [items] (dom/button {:onClick #(df/load-field this :items)} "Load Items"))))))

(def ui-child (prim/factory Child {:keyfn :child/label}))

(defsc Panel [this {:keys [ui/loading-data child] :as props}]
  {:initial-state (fn [params] {:child nil})
   :query         (fn [] [[:ui/loading-data '_] {:child (prim/get-query Child)}])
   :ident         (fn [] [:lazy-load/ui :panel])}
  (dom/div
    (dom/div {:style {:float "right" :display (if loading-data "block" "none")}} "GLOBAL LOADING")
    (dom/div "This is the Panel")
    (df/lazily-loaded ui-child child
      :not-present-render (fn [_] (dom/button {:onClick #(df/load-field this :child)} "Load Child")))))

(def ui-panel (prim/factory Panel))

; Note: Kinda hard to do idents/lazy loading right on root...so generally just have root render a div
; and then render a child that has the rest.
(defsc Root [this {:keys [panel] :as props}]
  {:initial-state (fn [params] {:panel (prim/get-initial-state Panel nil)})
   :query         [:ui/loading-data {:panel (prim/get-query Panel)}]}
  (dom/div (ui-panel panel)))

This is still supported (and is still currently the default); however, it is deprecated because it was found to be less than ideal:

  1. It caused the old data to disappear. There was no place else to put the targeted load marker except over the old data. This caused flicker and workarounds (such as mis-targeting the data and using post-mutations to put it in place at the end)

  2. The load markers are rather large. Looking at your component’s app state during a load is kind of ugly.

  3. The load markers could not be queried from elsewhere, meaning activity indicators had to be local to the loaded data.

  4. Worst: you have to add :ui/fetch-state to the query of the component representing the thing being loaded, or the load marker isn’t available.

9.6.4. Normalized Load Markers

In Fulcro 2.0, it is recommended that you use a keyword (of your own invention) for the :marker option instead. This has the following behavior:

  1. Load markers are placed in a top-level table (the var fulcro.client.data-fetch/marker-table holds the table name), using your keyword as their ID. They are normalized!

  2. You can therefore explicitly query for them using an ident join

This solves all of the prior system’s weaknesses.

Working with Normalized Load Markers

The steps are rather simple: Include the :marker parameter with load, and issue a query for the load marker on the marker table. The table name for markers is stored in the data-fetch namespace in the var df/marker-table.

(defsc SomeComponent [this props]
  {:query [:data :prop2 :other [df/marker-table :marker-id]]} ; an ident in queries pulls in an entire entity
  (let [marker (get props [df/marker-table :marker-id])]
    ...)))

...

(df/load this :key Item {:marker :marker-id})

The data fetch load marker will be missing if no loading is in progress. You can use the following functions to detect what state the load is in:

  • (df/ready? m) - Returns true if the item is queued, but not yet active on the network

  • (df/loading? m) - Returns true if the item is active on the network

The marker will disappear from the table when network activity completes.

The rendering is up to you, but that is really all there is to it.

Marker IDs for Items That Have Many Instances

The most confusing part of normalized load markers is that the IDs are keywords, but you may need a marker for a specific entity. Say you have a list of people, and you’d like to show an activity marker on the one you’re refreshing. You have many on the screen, so you can’t just use a simple keyword as the marker ID or they might all show a loading indicator when only one is updating.

In this case you will need to generate a marker ID based on the entity ID, and then use a link query to pull the entire load marker table to see what is loading.

For example, you might define the marker IDs as (keyword "person-load-marker" (str person-id)). Your person component could then find its load marker like this:

(defn person-markerid [id] (keyword "person-load-marker" (str id)))

(defsc Person [this {:keys [db/id person/name] :as props}]
  {:query [:db/id :person/name [df/marker-table '_]]
   :ident [:person/by-id :db/id]}
  (let [marker-id   (person-markerid id)
        all-markers (get props df/marker-table)
        marker      (get all-markers marker-id)]
      ...))

the load might look something like this:

(df/load this [:person/by-id 42] Person {:marker (person-markerid 42)})

The example below uses these techniques to name load markers.

Note
Open up the DB view and turn your server’s latency way up so you can watch the marker state.
Show/Hide Source
(ns book.demos.loading-indicators
  (:require
    [fulcro.client.dom :as dom]
    [fulcro.client.data-fetch :as df]
    [fulcro.logging :as log]
    [fulcro.client :as fc]
    [fulcro.server :as server]
    [fulcro.client.primitives :as prim :refer [defsc]]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SERVER:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(server/defquery-entity :lazy-load/ui
  (value [env id params]
    (case id
      :panel {:child {:db/id 5 :child/label "Child"}}
      :child {:items [{:db/id 1 :item/label "A"} {:db/id 2 :item/label "B"}]}
      nil)))

(server/defquery-entity :lazy-load.items/by-id
  (value [env id params]
    (log/info "Item query for " id)
    {:db/id id :item/label (str "Refreshed Label " (rand-int 100))}))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CLIENT:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def initial-state {:ui/react-key "abc"
                    :panel        {}})

(defonce app (atom (fc/new-fulcro-client :initial-state initial-state)))

(declare Item)

(defsc Item [this {:keys [db/id item/label] :as props}]
  ; query for the entire load marker table. use the lambda form of query for link queries
  {:query (fn [] [:db/id :item/label [df/marker-table '_]])
   :ident (fn [] [:lazy-load.items/by-id id])}
  (let [marker-id (keyword "item-marker" (str id))
        marker    (get-in props [df/marker-table marker-id])]
    (dom/div label
      ; If an item is rendered, and the fetch state is present, you can use helper functions from df namespace
      ; to provide augmented rendering.
      (if (df/loading? marker)
        (dom/span " (reloading...)")
        ; the `refresh!` function is a helper that can send an ident-based join query for a component.
        ; it is equivalent to `(load reconciler [:lazy-load.items/by-id id] Item)`, but finds the params
        ; using the component itself.
        (dom/button {:onClick #(df/refresh! this {:marker marker-id})} "Refresh")))))

(def ui-item (prim/factory Item {:keyfn :db/id}))

(defsc Child [this {:keys [child/label items] :as props}]
  {:query [:child/label {:items (prim/get-query Item)}]
   :ident (fn [] [:lazy-load/ui :child])}
  (let [render-list (fn [items] (map ui-item items))]
    (dom/div
      (dom/p "Child Label: " label)
      (if (seq items)
        (map ui-item items)
        (dom/button {:onClick #(df/load-field this :items :marker :child-marker)} "Load Items")))))

(def ui-child (prim/factory Child {:keyfn :child/label}))

(defsc Panel [this {:keys [ui/loading-data child] :as props}]
  {:initial-state (fn [params] {:child nil})
   :query         (fn [] [[:ui/loading-data '_] [df/marker-table '_] {:child (prim/get-query Child)}]) ; link querys require lambda
   :ident         (fn [] [:lazy-load/ui :panel])}
  (let [markers (get props df/marker-table)
        marker  (get markers :child-marker)]
    (dom/div
      (dom/div {:style {:float "right" :display (if loading-data "block" "none")}} "GLOBAL LOADING")
      (dom/div "This is the Panel")
      (if marker
        (dom/h4 "Loading child...")
        (if child
          (ui-child child)
          (dom/button {:onClick #(df/load-field this :child :marker :child-marker)} "Load Child"))))))

(def ui-panel (prim/factory Panel))

; Note: Kinda hard to do idents/lazy loading right on root...so generally just have root render a div
; and then render a child that has the rest.
(defsc Root [this {:keys [panel] :as props}]
  {:initial-state (fn [params] {:panel (prim/get-initial-state Panel nil)})
   :query         [{:panel (prim/get-query Panel)}]}
  (dom/div (ui-panel panel)))

9.7. Server Mutation Return Values

The server mutation is always allowed to return a value. Normally the only value that makes sense is the temporary ID remapping as previoiusly discussed in the main section of full-stack mutations. It is automatically processed by the client and causes the tempid to be rewritten everywhere in your state and network backlog:

; server-side
(defmutation new-thing [params]
  (action [env]
    ...
    {::prim/tempids {old-id new-id}}))

In some cases you’d like to return other details. However, remember that any data merge needs a tree of data and a query. With a mutation there is no query! As such, return values from mutations are ignored by default because there is no way to understand how to merge the result into your database. Remember we’re trying to eliminate the vast majority of callback hell and keep asynchrony out of the UI. The processing pipeline is always: update the database state, re-render the UI.

If you want to make use of the returned values from the server then you need to add something to remedy the lack of a query.

9.7.1. Using Mutation Joins

The solution might be obvious to you: include the query with the mutation! This is called a mutation join. The explicit syntax for a mutation join looks like this:

`[{(f) [:x]}]

but you never write them this way because a manual query doesn’t have ident information and cannot aid normalization. Instead, you write them just like you do when grabbing queries for anything else:

`[{(f) ~(prim/get-query Item)}]

Running a mutation with this notation allows you to return a value from the server’s mutation that exactly matches the graph of the item, and it will be automatically normalized and merged into your database. So, if the Item query ended up being [:db/id :item/value] then the server mutation could just return a simple map like so:

; server-side
(defmutation f [params]
  (action [env]
     {:db/id 1 :item/value 42})) ; ok to return one (a map) OR many (as a vector of maps)
Note
At the time of this writing the query must come from a UI component that has an ident. Thus, mutations joins essentially normalize things into a specific table in your database (determined by the ID(s) of the return value and the ident on the query’s component). Newer versions may relax this restriction.

9.7.2. Mutation Joins: Simpler Notation

Writing transact! using mutation joins is a bit visually noisy. It turns out there is a better way. If you remember: the remote section of client mutations can return a boolean or an AST. Fulcro comes with helper functions that can rewrite the AST of the mutation to modify the parameters or convert it to a mutation join! This can simplify how the mutations look in the UI.

Here’s the difference. With the manual syntactic technique we just described your UI and client mutation would look something like this:

; in the UI
(transact! this `[{(f) ~(prim/get-query Item)}])

; in your mutation definition (client-side)
(defmutation f [params]
  (action [env] ...)
  (remote [env] true))

However, using the helpers you can instead write it like this:

(ns api
  (:require [fulcro.client.mutations :refer [returning]]))

; in the UI
(transact! this `[(f)])

; in your mutation definition (client-side)
(defmutation f [params]
  (action [env] ...)
  (remote [{:keys [ast state]}] (returning ast state Item))

This makes the mutation join an artifact of the network interaction, and less for you to manually code (and read) in the UI.

The server-side code is the same for both: just return a proper graph value!

9.7.3. Targeting Return Values From Mutation Joins

If you use the AST with mutation joins, then Fulcro gives you an additional bonus: A helper you can use with your mutation to indicate that the given mutation return value should be further integrated into your app state. By default, you’re just returning an entity. The data gets normalized, but there is no further linkage into your app state.

You will sometimes want to pepper idents around your app state as a result of the return. You can add this kind of targeting through the AST in the remote (not available at the UI layer):

(defmutation f [params]
  (action [env] ...)
  (remote [{:keys [ast state]}]
    (-> ast
      (m/returning state Item)
      (m/with-target [:path :to :field])))

Special targets are also supported:

(defmutation f [params]
  (action [env] ...)
  (remote [{:keys [ast state]}]
    (-> ast
      (m/returning state Item) ; Returns something of type Item, will merge/normalize
      (m/with-target           ; Place the ident pointing to the loaded item in app state at additional locations
        (df/multiple-targets
          (df/append-to [:table 3 :field])
          (df/prepend-to [:table-2 4 :field]))))))
Demo of Mutation Joins

The demo below cover the basics of using mutation joins. It demonstrates:

  • Targeting Raw Values

    If you don’t specify a component with returning, then your returned data can be targeted, but of course it won’t normalize. The error triggering mutation does this:

    (defmutation  trigger-error
      "This mutation causes an unstructured error (just a map), but targets that value
       to the field `:error-message` on the component that invokes it."
      [_]
      (remote [{:keys [ast ref]}]
        (m/with-target ast (conj ref :error-message))))

    and the server simply returns a raw value (map is recommended)

    (server/defmutation trigger-error [_]
      (action [env]
        {:error "something bad"}))
  • Targeting Graph Results

    In the demo The bulk of the work is done in the create-entity mutation. which is targeting to-many so we can demo more features.

    (defmutation  create-entity
      "This mutation simply creates a new entity, but targets it to a specific location
      (in this case the `:child` field of the invoking component)."
      [{:keys [where?] :as params}]
      (remote [{:keys [ast ref state]}]
        (let [path-to-target (conj ref :children)
              ; replacement cannot succeed if there is nothing present...turn those into appends
              no-items?      (empty? (get-in @state path-to-target))
              where?         (if (and no-items? (= :replace-first where?))
                               :append
                               where?)]
          (cond-> (-> ast
                    ; always set what kind of thing is coming back
                    (m/returning state Entity)
                    ; strip the where?...it is for local use only (not server)
                    (m/with-params (dissoc params :where?)))
            ; Add the targeting...based on where?
            (= :append where?) (m/with-target (df/append-to path-to-target)) ; where to put it
            (= :prepend where?) (m/with-target (df/prepend-to path-to-target))
            (= :replace-first where?) (m/with-target (df/replace-at (conj path-to-target 0)))))))

    The server mutation just returns the entity (mixed with tempid remappings, if you need them).

    (server/defmutation create-entity [{:keys [db/id]}]
      (action [env]
        (let [real-id (swap! ids inc)]
          {:db/id        real-id
           :entity/label (str "Entity " real-id)
           :tempids      {id real-id}})))
Show/Hide Source
(ns book.demos.server-targeting-return-values-into-app-state
  (:require
    [fulcro.client.dom :as dom]
    [fulcro.client.primitives :as prim :refer [defsc]]
    [fulcro.client.dom :as dom]
    [fulcro.client.mutations :as m :refer [defmutation]]
    [fulcro.server :as server]
    [fulcro.client.data-fetch :as df]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SERVER:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ids (atom 1))

(server/defmutation trigger-error [_]
  (action [env]
    {:error "something bad"}))

(server/defmutation create-entity [{:keys [db/id]}]
  (action [env]
    (let [real-id (swap! ids inc)]
      {:db/id        real-id
       :entity/label (str "Entity " real-id)
       :tempids      {id real-id}})))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CLIENT:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare Item Entity)

(defmutation trigger-error
  "This mutation causes an unstructured error (just a map), but targets that value
   to the field `:error-message` on the component that invokes it."
  [_]
  (remote [{:keys [ast ref]}]
    (m/with-target ast (conj ref :error-message))))

(defmutation create-entity
  "This mutation simply creates a new entity, but targets it to a specific location
  (in this case the `:child` field of the invoking component)."
  [{:keys [where?] :as params}]
  (remote [{:keys [ast ref state]}]
    (let [path-to-target (conj ref :children)
          ; replacement cannot succeed if there is nothing present...turn those into appends
          no-items?      (empty? (get-in @state path-to-target))
          where?         (if (and no-items? (= :replace-first where?))
                           :append
                           where?)]
      (cond-> (-> ast
                ; always set what kind of thing is coming back
                (m/returning state Entity)
                ; strip the where?...it is for local use only (not server)
                (m/with-params (dissoc params :where?)))
        ; Add the targeting...based on where?
        (= :append where?) (m/with-target (df/append-to path-to-target)) ; where to put it
        (= :prepend where?) (m/with-target (df/prepend-to path-to-target))
        (= :replace-first where?) (m/with-target (df/replace-at (conj path-to-target 0)))))))

(defsc Entity [this {:keys [entity/label]}]
  {:ident [:entity/by-id :db/id]
   :query [:db/id :entity/label]}
  (dom/div label))

(def ui-entity (prim/factory Entity {:keyfn :db/id}))

(defsc Item [this {:keys [db/id error-message children]}]
  {:query         [:db/id :error-message {:children (prim/get-query Entity)}]
   :initial-state {:db/id :param/id :children []}
   :ident         [:item/by-id :db/id]}
  (dom/div {:style {:float  "left"
                    :width  "200px"
                    :margin "5px"
                    :border "1px solid black"}}
    (dom/h4 (str "Item " id))
    (when error-message
      (dom/div "The generated error was: " (pr-str error-message)))
    (dom/button {:onClick (fn [evt] (prim/transact! this `[(trigger-error {})]))} "Trigger Error")
    (dom/h6 "Children")
    (map ui-entity children)
    (dom/button {:onClick (fn [evt] (prim/transact! this `[(create-entity {:where? :prepend :db/id ~(prim/tempid)})]))} "Prepend one!")
    (dom/button {:onClick (fn [evt] (prim/transact! this `[(create-entity {:where? :append :db/id ~(prim/tempid)})]))} "Append one!")
    (dom/button {:onClick (fn [evt] (prim/transact! this `[(create-entity {:where? :replace-first :db/id ~(prim/tempid)})]))} "Replace first one!")))

(def ui-item (prim/factory Item {:keyfn :db/id}))

(defsc Root [this {:keys [root/items]}]
  {:query         [{:root/items (prim/get-query Item)}]
   :initial-state {:root/items [{:id 1} {:id 2} {:id 3}]}}
  (dom/div
    (mapv ui-item items)
    (dom/br {:style {:clear "both"}})))

9.7.4. Augmenting the Merge

There is also a sledge-hammer approach to return values: plug into Fulcro’s merge routines. This is an advanced technique and is not recommended for most applications.

Fulcro gives a hook for a mutation-merge function that you can install when you’re creating the application. If you use a multi-method, then it will make it easier to co-locate your return value logic near the client-local mutation itself:

(defmulti return-merge (fn [state mutation-sym return-value] mutation-sym))
(defmethod return-merge :default [s m r] s)

(new-fulcro-client :mutation-merge return-merge ...)

Now you should be able to write your return merging logic next to the mutation that it goes with. For example:

(defmethod m/mutate 'some-mutation [env k p] {:remote true })
(defmethod app/return-merge 'some-mutation [state k returnval] ...new-state...)

Note that the API is a bit different between the two: mutations get the app state atom in an environment, and you swap! on that atom to change state. The return merge function is in an already running swap! during the state merge of the networking layer. So, it is a function that takes the application state as a map and must return a new state as a map.

This technique is fully general in terms of handling arbitrary return values, but is limited in that your only recourse is to merge the data into you app state. Of course, since your rendering is a pure function of app state this means you can, at the very least, visualize the result.

This works, but is not the recommended approach because it is very easy to make mistakes that affect your entire application.

Note
Mutation merge happens after server return values have been merged; however, it does happen before tempid remapping. Just work with the tempids, and they will be rewritten once your merge is complete.
Live Example of a Mutation Merge

In the example below the displayed volume is coming from the server’s mutation return value. Use the server latency to convince yourself of this. Notice if you click too rapidly then the value doesn’t increase any faster than the server can respond (since it computes the new volume based on what the client sends).

The merge function is most easily dealt with as a multimethod so you can dispatch on the mutation symbol:

(defmulti merge-return-value (fn [state sym return-value] sym))

We’re going to return a map with :new-volume in it from the server, so our merge can look like this:

(defmethod merge-return-value `crank-it-up
  [state _ {:keys [new-volume]}]
  (assoc-in state [:child/by-id 0 :volume] new-volume))

Our mutation asks a remote server to increase the volume. The client and server mutations are:

;; client-side
(m/defmutation crank-it-up [params]
  (remote [env] true))
(server/defmutation crank-it-up [{:keys [value]}]
  (action [env]
    {:new-volume (inc value)}))

The remainder of the setup is just giving the merge handler function to the application at startup:

(fc/new-fulcro-client :mutation-merge merge-return-value)
Show/Hide Source
(ns book.demos.server-return-values-manually-merging
  (:require
    [fulcro.client.dom :as dom]
    [fulcro.server :as server]
    [fulcro.client.mutations :as m]
    [fulcro.client.primitives :as prim :refer [defsc]]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SERVER:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(server/defmutation crank-it-up [{:keys [value]}]
  (action [env]
    {:new-volume (inc value)}))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CLIENT:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defmulti merge-return-value (fn [state sym return-value] sym))

; Do all of the work on the server.
(m/defmutation crank-it-up [params]
  (remote [env] true))

(defmethod merge-return-value `crank-it-up
  [state _ {:keys [new-volume]}]
  (assoc-in state [:child/by-id 0 :volume] new-volume))

(defsc Child [this {:keys [id volume]}]
  {:initial-state (fn [params] {:id 0 :volume 5})
   :query         [:id :volume]
   :ident         [:child/by-id :id]}
  (dom/div
    (dom/p "Current volume: " volume)
    (dom/button {:onClick #(prim/transact! this `[(crank-it-up ~{:value volume})])} "+")))

(def ui-child (prim/factory Child))

(defsc Root [this {:keys [child]}]
  {:initial-state (fn [params] {:child (prim/get-initial-state Child {})})
   :query         [{:child (prim/get-query Child)}]}
  (dom/div (ui-child child)))

9.8. Loads From Within Mutations

It is often the case that a load results from user interaction with the UI. But it is also the case that the load isn’t everything you want to do, or that you’d like to hide the load logic or base it on current state that the triggering component does not know.

9.8.1. Load Actions in Mutations

In reality load and load-field call prim/transact! under the hood, targeting fulcro’s built-in fulcro/load mutation, which is responsible for sending your request to the server.

There are similar functions: load-action and load-field-action that do not call prim/transact!, but instead just push a load request into the load queue and can be used to inside of one of your own client-side mutations.

Let’s look at an example of a standard load. Say you want to load a list of people from the server:

(require [fulcro.client.data-fetch :as df])

(defsc Person [this props]
  {:query [:id :name]}
  ... )
(def ui-person (prim/factory Person))

(defsc PeopleList [this {:keys [people]}]
  {:query [:db/id :list-title {:people (prim/get-query Person}]
   :ident [:people-list/by-id :db/id]}
   (dom/div
     (if (seq people)
       (dom/button {:onClick #(df/load-field this :people)} "Load People")
       (map ui-person people))))

Since we are in the UI and not inside of a mutation’s action thunk, we can use load-field to initialize the call to prim/transact!.

The action-suffixed load functions are useful when performing an action in the user interface that must both modify the client-side database and load data from the server.

Note
You must use the result of the fulcro.client.data-fetch/remote-load funtion as the value of the remote in the mutation. The action calls of load-action place the request on a queue. The remote-load returns the correct indicator to Fulcro so that it knows you queued a load. If you forget it, then your load won’t be processed until the next operation causes remote interactions.
(require [fulcro.client.data-fetch :as df]
         [fulcro.client.mutations :refer [mutate]]
         [app.ui :as ui])

(defmutation change-view [{:keys [new-view]}]
  (action [{:keys [state] :as env}]
              (let [new-view-comp (cond
                                     (= new-view :main)  ui/Main
                                     (= new-view :settings) ui/Settings]
                (df/load-action env new-view new-view-comp)  ;; Add the load request to the queue
                (swap! state update :app/current-view new-view))}))
  (remote [env] (df/remote-load env))) ;; Tell Fulcro you did something that requires remote

This snippet defines a mutation that modifies the app state to display the view passed in via the mutation parameters and loads the data for that view. A few important points:

  1. If an action thunk calls one or more action-suffixed load functions (which do nothing but queue the load request) then it MUST also call remote-load on the remote side.

  2. The remote-load function changes the mutation’s dispatch key to fulcro/load which in turn triggers the networking layer that one or more loads are ready. IMPORTANT: Remote loading cannot be mixed with a mutation that also needs to be sent remotely. I.e. one could not send change-view to the server in this example.

  3. If you find yourself wanting to put a call to any load-* in a React Lifecycle method try reworking the code to use your own mutations (which can check if a load is really needed) and the use the action-suffixed loads instead. Lifecycle methods are often misunderstood, leading to incorrect behaviors like triggering loads over and over again.

Fulcro has a built-in mutation fulcro/load (also aliased as fulcro.client.data-fetch/load).

The mutation can be used from application startup or anywhere you’d run a mutation (transact!). This covers almost all of the possible remote data integration needs!

The helper functions described above simply trigger this built-in Fulcro mutation (the *-action variants do so by modifying the remote mutation AST via the remote-load helper function).

You are allowed to use this mutation directly in a call to transact!, but you should never need to.

The arguments to this mutation include most of the options that load can take, but you do need to specify query. For most direct use-cases you’ll probably skip using the load-field specific parameters described in the docstring (:field and :ident). You can read the source of load-field if you’d like to simulate it by hand.

For example:

(load reconciler :prop nil)

is a simple helper that is ultimately identical to:

(prim/transact! reconciler '[(fulcro/load {:query [:prop]}) :ui/loading-data])

(the follow-on read is to ensure load markers update).

9.9. Incremental Loading

It is very common for your UI query to have a lot more in it than you want to load at any given time. In some cases, even a specific entity asks for more than you’d like to load. A good example of this is a component that allows comments. Perhaps you’d like the initial load of the component to not include the comments at all, then later load the comments when the user, for example, opens (or scrolls to) that part of the UI.

Fulcro makes this quite easy. There are three basic steps:

  1. Put the full query on the UI

  2. When you use that UI query with load, prune out the parts you don’t want.

  3. Later, ask for the part you do want.

Step 2 sounds like it will be hard, but it isn’t:

9.9.1. Pruning the Query

Sometimes your UI graph asks for things that you’d like to load incrementally. Let’s say you were loading a blog post that has comments. Perhaps you’d like to load the comments later:

(df/load app :server/blog Blog {:params {:id 1 }
                                :target [:screens/by-name :blog :current-page]
                                :without #{:blog/comments}})

The :without parameter can be used to elide portions of the query (it works recursively). The query sent to the server will not ask for :blog/comments. Of course, your server has to parse and honor the exact details of the query for this to work (if the server decides it’s going to returns the comments, you get them…​but this is why we disliked REST, right?)

(server/defquery-root :server/blog
  (value [{:keys [query]} {:keys [id]}] ; query will be the query of Blog, without the :comments
     ; use a parser on query to get the proper blog result. See Server Interactions - Query Parsing
     (get-blog id query)))

9.9.2. Filling in the Subgraph

Later, say when the user scrolls to the bottom of the screen or clicks on "show comments" we can load the rest from of this previously partially-loaded graph within the Blog itself using load-field, which does the opposite of :without on the query:

(defsc Blog [this props]
  {:ident  [:blog/by-id :db/id]
   :query  [:db/id :blog/title {:blog/content (prim/get-query BlogContent)} {:blog/comments (prim/get-query BlogComment)}]}
  (dom/div
     ...
     (dom/button {:onClick #(load-field this :blog/comments)} "Show Comments")
     ...)))

The load-field function prunes everything from the query except for the branch joined through the given key. It also generates an entity rooted query based on the calling component’s ident:

[{[:table ID] subquery}]

where the [:table ID] are the ident of the invoking component, and subquery is (prim/get-query invoking-component), but focused down to the one field. In the example above, this would end up something like this:

[{[:blog/by-id 1] [{:blog/comments [:db/id :comment/author :comment/body]}]}]

This kind of query can be handled on the server with defquery-entity (which is triggered on these kinds of ident joins):

(server/defquery-entity :blog/by-id
  (value [{:keys [query]} id params]
    (get-blog id query))) ; SAME HANDLER WORKS!!!
Load Focus

Another way to load a subgraph part is to use the :focus setting on the load, :focus allow you to define a subquery to be loaded from the component query, to start simple here is how we can write the same previous example using :focus:

(defsc Blog [this props]
  {:ident  [:blog/by-id :db/id]
   :query  [:db/id :blog/title {:blog/content (prim/get-query BlogContent)} {:blog/comments (prim/get-query BlogComment)}]}
  (dom/div
     ...
     (dom/button {:onClick #(load this (prim/get-ident this) Blog {:focus [:blog/comments]})}
       "Show Comments")
     ...)))

Although the interface requires more code, it’s more flexible, let’s say for instance you only want to load the comment id and author, you can write as:

(load this (get-ident this) Blog {:focus [{:blog/comments [:db/id :comment/author]}]})

As you might notice, in the first :focus example when we point to a join, the whole join sub-query will be pulled, but you can get more precise by expressing more of the sub-query.

Load focus on unions

A special case that worth mention about focus sub-query is how it handles unions. Other than on unions, :focus will only use the attributes mentioned on the sub-query, but on unions, if you don’t express some union branch it will be pulled as is, for example, let’s say you have this given query:

[{:feed/item {:message [:message/text :message/timestamp]
              :activity [:activity/source-id :activity/url]}}]

If you :focus on this:

[{:feed/item {:message [:message/text]}}]

Then you get out the query:

[{:feed/item {:message [:message/text]
              :activity [:activity/source-id :activity/url]}}]

This makes sure you will keep all union branches.

9.10. Full-Stack Error Handling

The first thing I want to challenge you to think about is this: why do errors happen, and what can we do about them?

In the early days of web apps, our UI was completely dumb: the server did all of the logic. The answer to these questions were clear, because it wasn’t even a distributed app: it was a remote display of an app running on a remote machine. In other words, the context of the error handling was available at the same time as our request to do the operation.

So we often block the UI so that the user cannot get ahead of things (like submit a form and move on before the server has confirmed the submission). Over the years we’ve gotten a little more clever with our error handling, but largely our users (and our ability to reason about our programs) has kept us firmly rooted to the block-until-we-know method of error handling because it is actually less like a distributed system. Unfortunately, such UI interactions are doomed to feel sluggish in congested or bandwidth-limited environments.

More and more code is moving to the client machine. In the world of single-page apps we want things to "make sense" and we also want them to be snappy. Unfortunately, we still also have security concerns at the server, so we get confused by the following fact: the server has to be able to validate a request for security reasons. There is no getting around this. You cannot trust a client.

However, I think many of us take this too far: security concerns are often a lot easier to enforce than the full client-level interaction with these concerns. For example, we can say on a server that a field must be a number. This is one line of code that can be done with an assertion.

The UI logic for this is much larger: we have to tell the user what we expected, why we expected it, constrain the UI to keep them from typing letters, etc. In other words, almost all of the real logic is already on the client, and unless there is a bug, our UI won’t cause a server error because it is pre-checking everything before sending it out.

So, in a modern UI, here are the scenarios for errors from the server:

  1. You have a bug. Is there anything you can really do? No, because it is a bug. If you could predict it going wrong, you would have already fixed it. Testing and user bug reports are your only recourse.

  2. There is a security violation. There is nothing for your UI to do, because your UI didn’t do it! This is an attack. Throw an exception on the server, and never expect it in the UI. If you get it, it is a bug. See (1).

  3. There is a user perspective outage (LAN/WiFi/phone). These are possibly recoverable. You can block the UI, and allow the user to continue once networking is re-established.

  4. There is an infrastructure outage. You’re screwed. Things are just down. If you’re lucky, it is networking and your user is seeing it as (3) and is just blocked. If you’re not lucky, your database crashed and you have no idea if your data is even consistent.

So, I would assert that the only full-stack error handling worth doing in any detail is for case (3). If communications are down, the client can retry. But in a distributed system this can be a little nuanced. Did that mutation partially complete?

If your application can assume reasonably reliable networking and you write your server operations to be atomic then your error handling can be a relatively small amount of code. Unrecoverable problems will be rare and at worst you throw up a dialog that says you’ve had an error and the user hits reload on their browser. If this happens to users once or twice a year, it isn’t going to hurt you.

But of course there is more to the story, and the devil is in the details.

9.10.1. Programming with Pure Optimism

The general philosophy of a Fulcro application is that optimistic updates are not even triggered on the client unless they expect to succeed on the server. In other words, you write the application in such a way that operations cannot be triggered from the UI unless you’re certain that a functioning server will execute them. A server should not throw an exception and trigger a need for error handling unless there is a real, non-recoverable situation.

If this is true, then a functioning server does need to do sanity checking for security reasons, but in general you don’t need to give friendly errors when those checks fail: you should assume they are attempted hacks. Other serious problems are similar: there is usually nothing you can do but throw an exception and let the user contact support. Exceptions to this rule certainly exist, but they are few and far between.

There are some cases where the server has to be involved in a validation interaction non-optimistically. Login is a great example of this. However, invalid credentials on login need not be treated as an error! Instead they can be treated as a response to a question. "Can I log in with these credentials?". Yes or no. This is a query and response, not an error handling interaction. Thus, something like login can be handled with a query (to get the answer) and post-mutation (to update the screen with a message or change the UI route).

This philosophy eases the overhead in general application programming. You need not write a bunch of code in the UI that gives a nice friendly message for every kind of error that can possibly occur (nor does anyone really do that anywhere anyhow). If an error occurs, you can pretty much assume it is either a bug or a real outage. In both cases, there isn’t a lot you can do that will work "well" for the user. If it is a bug, then you really have no chance of predicting what will fix it, otherwise you would have already fixed the bug. If it’s an outage you might be able to do retries, but in many cases you have no way of knowing what has gone wrong.

So, one approach is to treat most error conditions as a rare problem that needs fairly radical recovery. One such method is to use a global error handler that is configured during the setup of your client application (you have to explicitly configure networking). This function could update application state to show some kind of top-level modal dialog that describes the problem, possibly allows the user to submit application history (for support viewer) to your servers, and then re-initializes the application in some way.

You can, of course, get pretty creative with the re-initialization. For example, say you write screens so that they will refresh their persistent data whenever it is older than some amount of time, and write it so all entities have a timestamp. You could walk the state and "expire" all of the timestamps, and then close the dialog. Your retry could be set up to check for the expiration, which in turn would trigger loads. If the server is really having problems then the worst case is that the dialog pops back up telling them there is still a problem.

9.10.2. Being a Bit More Pessimistic — Flaky Network Operation

If your users are likely using your software from a phone on a subway then you have a completely different issue.

Fortunately, Fulcro actually makes handling this case relatively easy as well. Here is what you can do:

  1. Write a custom networking implementation for the client that detects the kind of error, and retries recoverable ones until they succeed. Possibly with exponential backoff. (If an infinite loop happens, the user will eventually hit reload.)

  2. Make your server mutations idempotent so that a client can safely re-apply a transaction without causing data corruption.

The default fulcro networking does not do retries because it isn’t safe without the idempotent guarantee.

The optimistic updates of Fulcro and the in-order server execution means that "offline" operation is actually quite tractable. If programmed this way, your error handling becomes isolated almost entirely to the networking layer. Of course, if the user navigates to a screen that needs server data, they will just have to wait. Writing UI code that possibly has lifecycle timers to show progress updates will improve the overall feel, but the correctness will be there with a fairly small number of additions.

However, even with these fancy tricks that make our applications better, there are times when we’d just like to block until something is complete.

9.10.3. Detecting Errors From the Server

The built-in remote and networking in Fulcro has a few hooks for dealing with errors:

  1. A global network error handler for dealing with actual network errors (i.e. you can’t talk to the server).

  2. A global root node value (at :fulcro/server-error) you can query that holds the last network error. (You manually clear this if you wish to use it to track errors over time.)

  3. The ability to run a mutation if a load fails with an application-level error (load option :fallback). See fallbacks.

  4. Mutation fallbacks for responding to full-stack application-level mutation errors.

Global Error Handler

This is only available if you use the built-in remote with the Fulcro client. If you write your own networking then you can handle errors at the network layer any way you want. To install a network error handler on the default remote support simply write a function like this:

;; this function is called on *every* network error, regardless of cause
(defn error-handler "To be used as network-error-callback"
  [state status-code error]
  (log/warn "Global callback:" error " with status code: " status-code))

and install it:

(fc/new-fulcro-client :network-error-callback error-handler)
Server Error Demo

The live example below does various things to demonstrate various ways of reacting to errors. There is a load that fails and uses a fallback to log a message.

The next button tries a mutation that fails (by throwing on the server in a way that propagates the error back to the client). The final one tries a read that will fail, but does nothing with the error, though you’ll still see that the global indicator updates.

Show/Hide Source
(ns book.demos.server-error-handling
  (:require
    [fulcro.client :as fc]
    [fulcro.client.data-fetch :as df]
    [fulcro.logging :as log]
    [fulcro.client.mutations :as m :refer [defmutation]]
    [fulcro.client.primitives :as prim :refer [defsc]]
    [fulcro.server :as server]
    [fulcro.client.dom :as dom]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SERVER:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(server/defmutation error-mutation [params]
  ;; Throw a mutation error for the client to handle
  (action [env] (throw (ex-info "Server error" {:type :fulcro.client.primitives/abort :status 401 :body "Unauthorized User"}))))

(server/defquery-entity :error.child/by-id
  (value [env id params]
    (throw (ex-info "other read error" {:status 403 :body "Not allowed."}))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CLIENT:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; Just send the mutation to the server, which will return an error
(defmutation error-mutation [params]
  (remote [env] true))

;; an :error key is injected into the fallback mutation's params argument
(defmutation disable-button [{:keys [error ::prim/ref] :as params}]
  (action [{:keys [state]}]
    (log/warn "Mutation specific fallback -- disabling button due to error from mutation invoked at " ref)
    (swap! state assoc-in [:error.child/by-id :singleton :ui/button-disabled] true)))

(defmutation log-read-error [{:keys [error]}]
  (action [env] (log/warn "Read specific fallback: " error)))

(defsc Child [this {:keys [fulcro/server-error ui/button-disabled]}]
  ;; you can query for the server-error using a link from any component that composes to root
  {:initial-state (fn [p] {})
   :query         (fn [] [[:fulcro/server-error '_] :ui/button-disabled :fulcro/read-error])
   :ident         (fn [] [:error.child/by-id :singleton])}  ; lambda so we get a *literal* ident
  (dom/div
    ;; declare a tx/fallback in the same transact call as the mutation
    ;; if the mutation fails, the fallback will be called
    (dom/button {:onClick #(df/load this :data nil {:fallback `log-read-error})}
      "Click me to try a read with a fallback (logs to console)")
    (dom/button {:onClick  #(prim/transact! this `[(error-mutation {}) (df/fallback {:action disable-button})])
                 :disabled button-disabled}
      "Click me for error (disables on error)!")
    (dom/button {:onClick #(df/load-field this :fulcro/read-error)}
      "Click me for other error!")
    (dom/div "Server error (root level): " (str server-error))))

(def ui-child (prim/factory Child))

(defsc Root [this {:keys [child]}]
  {:initial-state (fn [params] {:child (prim/get-initial-state Child {})})
   :query         [{:child (prim/get-query Child)}]}
  (dom/div (ui-child child)))

9.10.4. UI Blocking

Fulcro defaults to optimistic updates, which in turn encourages you to write a UI that is very responsive. However, as soon as you start writing remote mutations you start worrying about the fact that your user submitted some data but you to let them go off and do other things (like leave the screen they’re on) before the server has responded. In effect, we’ve told the user "success", but we know we’re kind of lying to them.

Another way of looking at it is: we’re letting them leave the visual context of the information, but we know that if a server error happens then we need to inform them about that error. We’d like to be sure they understand the error by still seeing that context when it arrives.

This is a rather complicated way of saying something like "if their email change didn’t work, then we’d like to show the error next to the email input box".

There is nothing in Fulcro that prevents you from writing a blocking UI. You just have to remember that the UI is a pure rendering of application state: meaning that if you want to block the UI, then you need a way to put a "block the ui" marker in state (that renders in a way that prevents navigation), and remove that marker when the operation is complete.

Fulcro has a number of ways that you can accomplish this, but we’ll cover the simplest and most obvious.

Blocking on Remote Mutations

This technique uses the following pattern:

  1. We use the prim/ptransact! to submit a transaction, which will run each mutation in pessimistic mode (each element runs only after the prior element has completed a round-trip to the server).

  2. The first call in the tx will block the UI, and do the remote operation. We’ll also leverage mutation return values so the server can indicate success to us.

  3. Once the first call finishes, the second call in the tx can choose to unblock the UI, or handle any problem it sees. The mutation return value is merged (and visible) in app state.

Unlike normal mode, pessimistic transactions expect that you might have to nest another one within a mutation in order to retry a prior call. This is a supported use, and you will find the reconciler in the mutation’s env parameter to facilitate it as shown in the example below.

To show how this all works we’ll use an in-browser server emulation and show you a working example.

First, we need something to block our UI (which in the card measures 400x100 px). It is a simple div with some style that will overlay the main UI and prevent further interactions while also showing some kind of feedback message. The CSS sucks, but let’s ignore that for now.

We define it, along with some helper functions that can manipulate its state. It does not have an ident, and we plan to just place it in root at :overlay:

(defn set-overlay-visible* [state tf] (assoc-in state [:overlay :ui/active?] tf))
(defn set-overlay-message* [state message] (assoc-in state [:overlay :ui/message] message))

(defsc BlockingOverlay [this {:keys [ui/active? ui/message]}]
  {:query         [:ui/active? :ui/message]
   :initial-state {:ui/active? false :ui/message "Please wait..."}}
  (dom/div (clj->js {:style {:position        :absolute
                             :display         (if active? "block" "none")
                             :zIndex          65000
                             :width           "400px"
                             :height          "100px"
                             :backgroundColor "rgba(0,0,0,0.5)"}})
    (dom/div (clj->js {:style {:position  :relative
                               :top       "40px"
                               :color     "white"
                               :textAlign "center"}}) message)))

The main UI is just a simple one-field form and submission button. Note, however, that it submits the form with ptransact!, which will force each call to complete before the next one can start. Thus the second call can check the result and run whatever in response to it.

(defsc Root [this {:keys [ui/name overlay]}]
  {:query         [:ui/name {:overlay (prim/get-query BlockingOverlay)}]
   :initial-state {:overlay {} :ui/name "Alicia"}}
  (dom/div {:style (clj->js {:width "400px" :height "100px"})}
    (ui-overlay overlay)
    (dom/p "Name: " (dom/input {:value name}))
    (dom/button {:onClick #(prim/ptransact! this `[(submit-form) (retry-or-hide-overlay)])}
      "Submit")))

Now a bit of information about our "server". It has the following definition of the remote mutation:

(server/defmutation submit-form [params]
  (action [env]
    (if (> 0.5 (rand))
      {:message "Everything went swell!"
       :result  0}
      {:message "There was an error!"
       :result  1})))

As you can see it’s just a stub that randomly responds with success or error. The client mutation looks like this:

(defmutation submit-form [params]
  (action [{:keys [state]}] (swap! state set-overlay-visible* true))
  (remote [{:keys [state ast] :as env}]
    (m/returning ast state MutationStatus)))

It just shows the overlay, and goes remote. Notice the remote part is using returning from the mutations namespace to indicate a merge of the result value of the mutation. For that we’ve defined a singleton component (for its query only):

(defsc MutationStatus [this props]
  {:ident (fn [] [:remote-mutation :status])
   :query [:message :result]})

This means that when this remote mutation is done, we should see a the return value of the server mutation at [:remote-mutation :status] (which is the constant ident of MutationStatus).

Now for our second client mutation. First, we need a function that can look in state and see if the mutation status looks ok:

(defn submit-ok? [env] (= 0 (some-> env :state deref :remote-mutation :status :result)))

Then we can leverage that to make something that reads well:

(defmutation retry-or-hide-overlay [params]
  (action [{:keys [reconciler state] :as env}]
    (if (submit-ok? env)
      (swap! state (fn [s]
                     (-> s
                       (set-overlay-message* "Please wait...") ; reset the overlay message for the next appearance
                       (set-overlay-visible* false))))
      (do
        (swap! state set-overlay-message* (str (-> state deref :remote-mutation :status :message) " (Retrying...)"))
        (prim/ptransact! reconciler `[(submit-form {}) (retry-or-hide-overlay {})])))))

It’s the real work-horse. The optimistic side can assume the result is updated, so it looks for the result code via submit-ok?. If things are OK, then it resets the overlay message and hides it.

If the submission had an error, then it

  • Adds "retrying" to the server message and puts that on the overlay

  • Does a new call to ptransact!.

You can try out the finished product in the example below. Try it a few times so you can see the error-handling in action.

Show/Hide Source
(ns book.server.ui-blocking-example
  (:require [fulcro.client.primitives :as prim :refer [defsc]]
            [fulcro.client.dom :as dom]
            [fulcro.client.cards :refer [defcard-fulcro]]
            [fulcro.client.mutations :as m]
            [fulcro.server :as server]
            [fulcro.client.mutations :as m :refer [defmutation]]
            [book.macros :refer [defexample]]))

;; SERVER

(server/defmutation submit-form [params]
  (action [env]
    (if (> 0.5 (rand))
      {:message "Everything went swell!"
       :result  0}
      {:message "There was an error!"
       :result  1})))

;; CLIENT

(defsc BlockingOverlay [this {:keys [ui/active? ui/message]}]
  {:query         [:ui/active? :ui/message]
   :initial-state {:ui/active? false :ui/message "Please wait..."}}
  (dom/div (clj->js {:style {:position        :absolute
                             :display         (if active? "block" "none")
                             :zIndex          65000
                             :width           "400px"
                             :height          "100px"
                             :backgroundColor "rgba(0,0,0,0.5)"}})
    (dom/div (clj->js {:style {:position  :relative
                               :top       "40px"
                               :color     "white"
                               :textAlign "center"}}) message)))

(def ui-overlay (prim/factory BlockingOverlay))

(defn set-overlay-visible* [state tf] (assoc-in state [:overlay :ui/active?] tf))
(defn set-overlay-message* [state message] (assoc-in state [:overlay :ui/message] message))

(defsc MutationStatus [this props]
  {:ident (fn [] [:remote-mutation :status])
   :query [:message :result]})

(defmutation submit-form [params]
  (action [{:keys [state]}] (swap! state set-overlay-visible* true))
  (remote [{:keys [state ast] :as env}]
    (m/returning ast state MutationStatus)))

(defn submit-ok? [env] (= 0 (some-> env :state deref :remote-mutation :status :result)))

(defmutation retry-or-hide-overlay [params]
  (action [{:keys [reconciler state] :as env}]
    (if (submit-ok? env)
      (swap! state (fn [s]
                     (-> s
                       (set-overlay-message* "Please wait...") ; reset the overlay message for the next appearance
                       (set-overlay-visible* false))))
      (do
        (swap! state set-overlay-message* (str (-> state deref :remote-mutation :status :message) " (Retrying...)"))
        (prim/ptransact! reconciler `[(submit-form {}) (retry-or-hide-overlay {})]))))
  (refresh [env] [:overlay]))                               ; we need this because the mutation runs outside of the context of a component

(defsc Root [this {:keys [ui/name overlay]}]
  {:query         [:ui/name {:overlay (prim/get-query BlockingOverlay)}]
   :initial-state {:overlay {} :ui/name "Alicia"}}
  (dom/div {:style {:width "400px" :height "100px"}}
    (ui-overlay overlay)
    (dom/p "Name: " (dom/input {:value name}))
    (dom/button {:onClick #(prim/ptransact! this `[(submit-form {}) (retry-or-hide-overlay {})])}
      "Submit")))

9.10.5. Fallbacks

If you’re running a mutation is likely to trigger server errors then you can explicitly encode a fallback behavior with the mutation. Fallbacks are triggered the mutation on the server throws an error that is detectable, or if there is a network error.

The requirement for a server mutation to trigger fallbacks is for it to throw an ex-info exception and include {:type :fulcro.client.primitives/abort} in the data. Otherwise the server-side parser will swallow the exception and continue with the transaction.

Defining a fallback for a transaction is done by including a special mutation in the transaction that names the mutation to invoke on error:

(prim/transact! this `[(some/mutation) (fulcro.client.data-fetch/fallback {:action handle-failure})])
(require [fulcro.client.mutations :refer [mutate]]
         [fulcro.client.primitives :as prim)

(defmutation handle-failure [{:keys [error ::prim/ref] :as params}]
  ;; fallback mutations are designed to recover the client-side app state from server failures
  ;; THEY DO NOT CHECK FOR REMOTE. You cannot chain a remote interaction in a fallback.
  (action [{:keys [state]}]
    (swap! state undo-stuff error)))

Assuming that some-mutation is remote, then if the server throws a hard error (e.g. status code not 200) then the fallback action’s mutation symbol (a dispatch key for mutate) is invoked on the client with params that include an :error key that includes the details of the server exception (error type, message, and ex-info’s data). Be sure to only include serializable data in the server exception!

If triggered due to a mutation fallback (not load), then the fallback will also receive the ident of the component that invoked the original transaction in parameters under the key fulcro.client.primitives/ref.

You can have any number of fallbacks in a tx, and they will run in order if the transaction fails.

Note
It is not recommended that you rely on fallbacks for very much. They are provided for cases where you’d like to code instance-targeted recovery, but we believe this to be a rarely useful feature. You’re much better off preventing errors by coding your UI to validate, authorize, and error check things on the client before sending them to the server. The server should still verify sanity for security reasons, but optimistic systems like Fulcro put more burden on the client code in order to provide a better experience under normal operation. See the earlier discussion about error handling.

If you do use fallback then you probably also need to clear the network queue so that additional queued operations don’t continue to fail.

9.10.6. Clearing Network Queue

If the server sends back a failure it may be desirable to clear any pending network requests from the client network queue. For example, if you’re adding an item to a list and get a server error you might have a mutation waiting in your network queue that was some kind of modification to that (now failed) item. Continuing the network processing might just cause more errors.

The FulcroApplication protocol (implemented by your client app) includes the protocol method clear-pending-remote-requests! which will drain all pending network requests.

(fulcro.client/clear-pending-remote-requests! my-app)

A common recovery strategy from errors could be to clean the network queue and run a mutation that resets your application to a known state, possibly loading sane state from the server.

9.10.7. Pessimistic Transaction Fallbacks

The fallback mechanism described for error handling works in ptransact!. Fallbacks are clustered to the remote they follow up until the next remote mutation (with one exception: fallbacks at the beginning of the entire tx are clustered to the first remote mutation):

(ptransact! this `[(L) (df/fallback {...}) (L) (R) (df/fallback {...}) (L2) (R2) (L3) (df/fallback {...}) ])

will associate the first two fallbacks with remote call R, and the last one with R2.

10. Building a Server

In the Getting Started chapter you saw a little on how to build and use Fulcro’s easy server. That server is acually flexible enough for many production needs, but Fulcro also comes with code to help you very quickly get a custom server for your application up and running. In this chapter we’ll give you more detail on these two main approaches to the server-side of Fulcro:

  • How to use the pre-built bits to manually build your own server.

  • More details on the easy server.

10.1. Rolling Your Own Server

If you’re integrating with an existing server then you probably just want to know how to get things working without having to use a Component library, and all of the other stuff that comes along with it.

It turns out that the server API handling is relatively light. Most of the work goes into getting things set up for easy server restart (e.g. making components stop/start) and getting those components into your parsing environment.

If you have an existing server then you’ve mostly figured out all of that stuff already and just want to plug a Fulcro API handler into it.

Here are the basic requirements:

  1. Make sure your Ring stack has transit-response and transit-params. You can see a sample Ring stack in [Fulcro’s source](https://github.com/fulcrologic/fulcro/blob/1.2.0/src/main/fulcro/easy_server.clj#L94)

  2. Check to see if the incoming request is for "/api". If so:

  3. Call [handle-api-request](https://github.com/fulcrologic/fulcro/blob/1.2.0/src/main/fulcro/server.clj#L354). Pass a parser to it (recommend using fulcro.server/fulcro-parser), an environment, and the EDN of the request. It will give back a Ring response.

You’re responsible for creating the parser environment. I’d recommend using the fulcro.server/fulcro-parser because it is already hooked to the server multimethods like defquery-root and defmutation. Those won’t work unless you use it, but any parser that can deal with the query/mutation syntax is technically legal.

Here’s a complete little server with full defaults/dev/prod configuration support (see configuration), and hot code reload using mount instead of component:

(ns demo.server
  (:require
    [fulcro.server :as server]
    [immutant.web :as web]
    [mount.core :refer [defstate]]
    [ring.middleware.content-type :refer [wrap-content-type]]
    [ring.middleware.gzip :refer [wrap-gzip]]
    [ring.middleware.params :refer [wrap-params]]
    [ring.middleware.multipart-params :refer [wrap-multipart-params]]
    [ring.middleware.not-modified :refer [wrap-not-modified]]
    [ring.middleware.resource :refer [wrap-resource]]
    [ring.util.response :refer [response file-response resource-response]]))

;; You'll need defaults.edn and dev.edn in resources/config with at least {} as content.
(defstate config :start (server/load-config {:config-path "config/dev.edn"}))

(def ^:private not-found-handler
  (fn [req]
    {:status  404
     :headers {"Content-Type" "text/plain"}
     :body    "NOPE"}))

(defstate server-parser :start (server/fulcro-parser))

(defn wrap-api [handler uri]
  (fn [request]
    (if (= uri (:uri request))
      (server/handle-api-request
        server-parser
        ;; this map is `env`. Put other defstate things in this map and they'll be in the mutations/query env on server.
        {:config config}
        (:transit-params request))
      (handler request))))

(defstate middleware
  :start
  (-> not-found-handler
    (wrap-api "/api")
    server/wrap-transit-params
    server/wrap-transit-response
    (wrap-resource "public")
    wrap-content-type
    wrap-not-modified
    wrap-gzip))

(defstate http-server
  :start
  (web/run middleware {:host "0.0.0.0"
                       :port (get config :port 3000)}) ; defaults to 3000, but can also come from config file
  :stop
  (web/stop http-server))

and the user namespace (typically in src/dev):

(ns user
  (:require
    [clojure.tools.namespace.repl :as tools-ns :refer [set-refresh-dirs]]
    demo.server
    [mount.core :as mount]))

(set-refresh-dirs "src/dev" "src/main")

(defn go [] (mount/start))

(defn restart []
  (mount/stop)
  (tools-ns/refresh :after 'user/go))

In a REPL, you could start this one up and restart it with:

$ lein repl
user=> (go)
user=> (restart)
...hot code reload/restart...

10.1.1. Configuration

Since you’ll need to configure your web server, it might be useful to note that the configuration used by the easy server is a component you can inject into your own server. A number of add-on components for Fulcro assume a :config keyed component will be in your system, so if you choose to use such components you can create a config component via fulcro.server/new-config.

It supports pulling in values from the system environment, overriding configs with a JVM option, and more. See the section in easy server about configuration, and the docstrings on new-config for more details.

10.2. The "Easy" Server

The pre-built easy server component for Fulcro uses Stuart Sierra’s Component library. The server has no global state except for a debugging atom that holds the entire system, and can therefore be easily restarted with a code refresh to avoid costly restarts of the JVM.

You should have a firm understanding of [Stuart’s component library](https://github.com/stuartsierra/component), since we won’t be covering that in detail here.

10.2.1. Constructing a base server

The base server is trivial to create:

(ns app.system
  (:require
    [fulcro.easy-server :as server]
    [app.api :as api]
    [fulcro.server :as prim]))

(defn make-system []
  (server/make-fulcro-server
    ; where you want to store your override config file
    :config-path "/usr/local/etc/app.edn"

    ; The keyword names of any components you want auto-injected into the query/mutation processing parser env (e.g. databases)
    :parser-injections #{}

    ; Additional components you want added to the server
    :components {}))

10.2.2. Configuring the server [ServerConfig]

Server configuration requires two EDN files:

  • resources/config/defaults.edn: This file should contain a single EDN map that contains defaults for everything that the application wants to configure.

  • /abs/path/of/choice: This file can be named what you want (you supply the name when making the server). It can also contain an empty map, but is meant to be machine-local overrides of the configuration in the defaults. This file is required. We chose to do this because it keeps people from starting the app in an unconfigured production environment. NOTE: If the path is relative it is looked for via CLASSPATH (resource). If it is absolute, the real filesystem is searched.

(defn make-system []
  (server/make-fulcro-server
    :config-path "/usr/local/etc/app.edn"
    ...

The only parameter that the default server looks for is the network port to listen on:

{ :port 3000 }

The configuration component has a number of built-in features:

  • You can override the configuration to use with a JVM option: -Dconfig=filename

  • The defaults.edn is the target of configuration merge. Your config EDN file must be a map, and anything in it will override what is in defaults. The merge is a deep merge.

  • Values can take the form :env/VAR, which will use the string value of that environment variable as the value

  • Values can take the form :env.edn/VAR, which will use read-string to interpret the environment variable as the value

  • Relative paths for the config file can be used, and will search the CLASSPATH resources. This allows you to package your config with you jar.

If you choose not to use components (see mount), then you can use load-config directly:

(defstate config :start (server/load-config {:config-path "config/dev.edn"}))

and config (after start) will simply contain the EDN of the defaults/config file merge.

10.2.3. Pre-installed components

When you start a Fulcro server it comes pre-supplied with injectable components upon which your component can depend and/or inject into the server-side parsing environment.

The most important of these, of course, is the configuration itself. The available components are known by the following keys:

  • :config: The configuration component. The actual EDN value is in the :value field of the component.

  • :handler: The component that handles web traffic. You can inject your own Ring handlers into two different places in the request/response processing: before or after the API handler.

  • :server: The actual web server.

The component library, of course, figures out the dependency order and ensures things are initialized and available where necessary.

10.2.4. Making components available in the processing environment

Any components in the server can be injected into the processing pipeline so they are available when writing your mutations and query procesing. Making them available is as simple as putting their component keyword into the :parser-injections set when building the server:

(defn make-system []
  (server/make-fulcro-server
    :parser-injections #{:config}
    ...))

10.2.5. Adding to the Ring Stack

The easy server has a hook in front of the API processing (pre-hook), and one at the end of the ring stack after API processing and just before the not-found handler (post-hook). You can have a component join into the stack by making it depend on :handler. Here is an example:

(defrecord Authentication [handler]
  c/Lifecycle
  (start [this]
    (log/info "Hooking into pre-processing to add user info")
    (let [old-pre-hook (fulcro.easy-server/get-pre-hook handler)
          new-hook     (fn [ring-handler] (fn [req] ((old-pre-hook ring-handler) (new-behavior req)))]
      (fulcro.easy-server/set-pre-hook! handler new-hook))
    this)
  (stop [this] this))

...

(def server (fulcro.easy-server/make-fulcro-server
               :components { :auth (component/using (map->Authentication {}) [:handler]))}))

The same pattern is used for fallback hooks.

The nesting of these functions can be a bit confusing, especially since none of the types are explicit. Here’s a description of each:

middleware

A function that takes a "default handler" and returns a function of request → response (which is where you can choose to call the default, or do your own thing).

ring-handler

A function from Ring request maps to responses (fn [req] resp)

old-pre-hook

The prior value of the hook. It is middleware.

So in the example above you have complete control over what happens in your new hook. Here are some examples:

Ignore the request altogether, and short-circuit everything with a literal response:

(let [...
      new-hook     (fn [ring-handler] (fn [req] (ring.util.response "Hello world")]
  ...

Ignore the old hook (this is like uninstalling all hooks):

(let [...
      new-hook  (fn [ring-handler] (fn [req] (ring-handler req))]
  ...

Serve index.html from your public resources folder if the uri of the request ends in html (useful for HTML5 routing):

(let [...
      new-hook  (fn [ring-handler]
                  (fn [req]
                    (if (re-find #".*html$" uri)
                      (ring.middleware.resource/resource-request (assoc req :uri "index.html") "public")
                      ((old-pre-hook ring-handler) req)))]
  ...
Note
Remember that if you serve something like index.html for alternate paths you should use absolute paths for the resources (e.g. scripts) in that file. If the requested resource wasn’t an html file, then you’ll also need to set the content type on the response with something like (→ (ring.middleware.resource/resource-request …​) (ring.util.response/content-type "text/html")).

10.2.6. Handling Filesystem Resources

The easy server will serve any files that are placed in resources/public. The easy server does pre-map URI "/" to "/index.html".

10.2.7. Modifying the /api route

The easy server (and client) default to using /api as the URI on which to handle traffic. If you are proxying multiple Fulcro applications to a single server, you may want to place them under different URI paths (e.g. /app-1/api and /app-2/api.

; server API at "/app-1/api":
(def server (fulcro.easy-server/make-fulcro-server :app-name "app-1" ...))

The :app-name option of the easy server will add such a prefix to the API route. If you do that, then the client will also need to have manual configuration of networking to ensure that it tries to contact the correct URI for API calls.

(def client (fc/new-fulcro-client :networking {:remote (fulcro.client.network/fulcro-http-remote {:url "/app1/api"})}))

10.2.8. Adding Non-API Routes

The easy server also supplies a way to add in URI handlers via BIDI:

(defn page-handler [env bidi-match]
  (ring/response ...))

; define routes (see BIDI documentation)
(def my-routes ["/" {"page.html" :page}])

(def server (fulcro.easy-server/make-fulcro-server
               :extra-routes { :routes   my-routes
                               :handlers {:page page-handler}}))

The extra routes are processed right after the pre-hook, but just before the resource (filesystem) serving. Thus, you can respond to any URI that isn’t already handled by your pre-hook.

11. Dynamic Queries

Fulcro fully support dynamic queries: the ability the change the query of a component at runtime. This feature is fully serializable (works with the support viewer and other time-travel features), and is critical for features like code splitting where you may need to compose in a query of an as-yet unloaded component tree of your application.

11.1. Query IDs

For dynamic queries to work right they have to be stored in your application database and every aspect of them must be serializable. Additionally, the UI must be able to look them up at the component level in order to do optimal refresh. The solution to this is query IDs. A query ID is a simple combination of the component’s fully-qualified class name combined with a user-defined qualifier (which defaults to the empty string).

Since this qualifier is needed both in the code that obtains queries (get-query) and in the UI rendering (the factory that draws that component), it is easiest to locate the qualifier in the UI factory itself. This allows you to have instances of a class that can have different queries:

(defsc Thing ...)
(def ui-thing (prim/factory Thing)) ; query ID is based solely on the class itself (with no qualifier)
(def ui-thing-1 (prim/factory Thing {:qualifier :a})) ; query ID is derived from Thing plus the qualifier :a

(defsc Parent [this props]
  {:query (fn [] [{:child (prim/get-query ui-thing-1)}])}
  ...)

In the above example one can now set the query for Thing, or “Thing` with qualifier `:a”.

The following live demo shows dynamic queries in action:

Show/Hide Source
(ns book.queries.dynamic-queries
  (:require
    [fulcro.client.dom :as dom]
    [goog.object]
    [fulcro.client.primitives :as prim :refer [defsc]]
    [fulcro.client.mutations :as m]))

(declare ui-leaf)

; This component allows you to toggle the query between [:x] and [:y]
(defsc Leaf [this {:keys [x y]}]
  {:initial-state (fn [params] {:x 1 :y 42})
   :query         (fn [] [:x])                              ; avoid error checking so we can destructure both :x and :y in props
   :ident         (fn [] [:LEAF :ID])}                      ; there is only one leaf in app state
  (dom/div
    (dom/button {:onClick (fn [] (prim/set-query! this ui-leaf {:query [:x]}))} "Set query to :x")
    (dom/button {:onClick (fn [] (prim/set-query! this ui-leaf {:query [:y]}))} "Set query to :y")
    ; If the query is [:x] then x will be defined, otherwise it will not.
    (dom/button {:onClick (fn [e] (if x
                                    (m/set-value! this :x (inc x))
                                    (m/set-value! this :y (inc y))))}
      (str "Count: " (or x y)))                             ; only one will be defined at a time
    " Leaf"))

(def ui-leaf (prim/factory Leaf {:qualifier :x}))

(defsc Root [this {:keys [root/leaf] :as props}]
  {:initial-state (fn [p] {:root/leaf (prim/get-initial-state Leaf {})})
   :query         (fn [] [{:root/leaf (prim/get-query ui-leaf)}])}
  (dom/div (ui-leaf leaf)))

12. Query Parsing

In the Loading and Incremental Loading sections we showed you the central entry points for responding to server queries: defquery-root and defquery-entity. These are fine for simple examples and for getting into your processing; however, to be truly data-driven you need to change how the server responds based on what the client actually asked for (in detail).

So far, we’ve sort of been spewing entire entities back from the server without taking care to prune them down to the actual query of the client.

Unless you modify the network stack all client communication will be in the form of Query/Mutation expressions, which of course are recursive in nature. There is no built-in recursive processing, since Fulcro does not know anything about your server-side storage; however, there is a parsing mechanism that you can use to build processing to interface with it, and there are a number of libraries that can also help.

12.1. Avoiding Parsing

If you’re lucky, you can make use of a library to do this stuff for you. Here are some options we know about:

  • Pathom

    A really nice library for building recursive Fulcro query parsers. It has a good model for building parsers that can bridge everything from REST services to microservice architectures. In general if you need to interpret your UI queries, this tool can be very useful.

  • Fulcro-SQL

    A library that can run Fulcro graph queries against SQL databases. This library lets you define your joins in relation to the Fulcro join notion. It can walk to-one, to-many, and many-to-many joins in an SQL database in response to a Fulcro join. This allows it to handle many Fulcro queries as graph queries against your SQL database with just a little configuration and invocatin code.

  • Datomic

    If you’re lucky enough to be using Datomic, Fulcro’s graph query syntax will run in their pull API

Of these, Pathom is the most general and allows you to easily process a query in a more abstract way. However, you should know a little bit about parsing the queries yourself.

12.2. Doing the Parsing Yourself

The expression parser needs two things: A function to dispatch reads to, and a function to dispatch mutations. Since we’re talking about query parsing we’ll only be talking about the read dispatch function.

The signature of the read function is (read [env dispatch-key params]) where the env contains the state of your application, a reference to your parser (so you can call it recursively, if you wish), a query root marker, an AST node describing the exact details of the element’s meaning, and anything else you want to put in there if you call the parser recursively.

The most important item in the query processing is the received environment (env). On the server it contains:

  • Any components you’ve asked to be injected. Perhaps database and config components.

  • ast: An AST representation of the item being parsed.

  • query: The subquery (e.g. of a join)

  • parser: The query expression parser itself (which allows you to do recursive calls). If you’re using the built-in parser, this this will be that same parser that is already hooked into your dispatch mechanism (e.g. defquery-root).

  • request: The full incoming Ring request, which will contain things like the headers, cookies, session, user agent info, etc.

The return value of your read must be the value for the dispatch-key. The parser assembles these back together and returns a map containing those keys for all of the items for which you return a non-nil result.

If you understand that, you can probably already write a simple recursive parse of a query. If you need a bit more hand-holding, then read on.

Note
When doing recursive parsing You do not have to use the parser from env. Parsers are cheap. If you want to make one to deal with a particular graph, go for it! The fulcro.client.primitives/parser can make one.

Now, let’s get a feeling for the parser in general. The example below runs a parser on an arbitrary query that you supply, records the calls to the read emitter, and shows the trace of those calls in order.

See the source code comments for a full description of how it works.

Try some queries like these:

  • [:a :b]

  • [:a {:b [:c]}] (note that the AST is recursively built, but only the top keys are actually parsed to trigger reads)

  • [(:a { :x 1 })] (note the value of params)

Show/Hide Source
(ns book.queries.parsing-trace-example
  (:require [fulcro.client.primitives :as prim :refer [defsc]]
            [devcards.util.edn-renderer :refer [html-edn]]
            [cljs.reader :as r]
            [fulcro.client.mutations :as m :refer [defmutation]]
            [fulcro.client.dom :as dom]))

(defn tracer-path
  "The assoc-in path of a field on the ParsingTracer in app state."
  [field] [:widget/by-id :tracer field])

(defn tracing-reader
  "Creates a parser read handler that can record the trace of the parse into the given state-atom at the proper
   tracer path."
  [state-atom]
  (fn [env k params]
    (swap! state-atom update-in (tracer-path :trace)
      conj {:env          (assoc env :parser :function-elided)
            :dispatch-key k
            :params       params})))

(defmutation record-parsing-trace
  "Mutation: Run and record the trace of a query."
  [{:keys [query]}]
  (action [{:keys [state]}]
    (let [parser (prim/parser {:read (tracing-reader state)})] ; make a parser that records calls to read
      (try
        (swap! state assoc-in (tracer-path :trace) [])      ; clear the last trace
        (swap! state assoc-in (tracer-path :error) nil)     ; clear the last error
        (parser {} (r/read-string query))                   ; Record the trace
        (catch js/Error e (swap! state assoc-in (tracer-path :error) e)))))) ; Record and exceptions

(defsc ParsingTracer [this {:keys [trace error query result]}]
  {:query         [:trace :error :query :result]
   :ident         (fn [] [:widget/by-id :tracer])
   :initial-state {:trace [] :error nil :query ""}}
  (dom/div
    (when error
      (dom/div (str error)))
    (dom/input {:type     "text"
                :value    query
                :onChange #(m/set-string! this :query :event %)})
    (dom/button {:onClick #(prim/transact! this `[(record-parsing-trace ~{:query query})])} "Run Parser")
    (dom/h4 "Parsing Trace")
    (html-edn trace)))

(def ui-tracer (prim/factory ParsingTracer))

(defsc Root [this {:keys [ui/tracer]}]
  {:query         [{:ui/tracer (prim/get-query ParsingTracer)}]
   :initial-state {:ui/tracer {}}}
  (ui-tracer tracer))

12.2.1. Injecting some kind of database

In order to play with this on a server, you’ll want to have some kind of state available. The most trivial thing you can do is just create a global top-level atom that holds data. This is sufficient for testing, and we’ll assume we’ve done something like this on our server:

(defonce state (atom {}))

One could also wrap that in a Stuart Sierra component and inject it into the parser (for details on modifying the parser environment, see Making Components Available in the Parsing Environment).

Much of the remainder of this section assumes this.

12.3. Read Dispatching

When building your server there must be a read function that can pull data to fufill what the parser needs to fill in the result of a query. Fulcro supplies this by default and gives you the defquery-* macros as helpers to hook into it, but really it is just a multi-method.

For educational purposes, we’re going to walk you through implementing this read function yourself.

The parser understands the grammar, and is written to work as follows:

  • The parser calls your read with the key that it parsed, along with some other helpful information.

  • Your read function returns a value for that key (possibly calling the parser recursively if it is a join).

  • The parser generates the result map by putting that key/value pair into the result at the correct position (relative to the query).

Note that the parser only processes the query one level deep. Recursion (if you need it) is controlled by you calling the parser again from within the read function.

The example below is similar to the prior one, but it has a read function that just records what keys it was triggered for. Give it an arbitrary legal query, and see what happens.

Some interesting queries:

  • [:a :b :c]

  • [:a {:b [:x :y]} :c]

  • [{:a {:b {:c [:x :y]}}}]

Show/Hide Source
(ns book.queries.parsing-key-trace
  (:require [fulcro.client.primitives :as prim :refer [defsc]]
            [devcards.util.edn-renderer :refer [html-edn]]
            [cljs.reader :as r]
            [fulcro.client.mutations :as m :refer [defmutation]]
            [fulcro.client.dom :as dom]))

(defn tracer-path
  "The assoc-in path of a field on the ParsingTracer in app state."
  [field] [:widget/by-id :tracer field])

(defn tracing-reader
  "Creates a parser read handler that can record just the dispatch keys of a parse."
  [state-atom]
  (fn [env k params]
    (swap! state-atom update-in (tracer-path :trace) conj {:read-called-with-key k})))

(defmutation record-parsing-trace
  "Mutation: Run and record the trace of a query."
  [{:keys [query]}]
  (action [{:keys [state]}]
    (let [parser (prim/parser {:read (tracing-reader state)})] ; make a parser that records calls to read
      (try
        (swap! state assoc-in (tracer-path :trace) [])      ; clear the last trace
        (swap! state assoc-in (tracer-path :error) nil)     ; clear the last error
        (parser {} (r/read-string query))                   ; Record the trace
        (catch js/Error e (swap! state assoc-in (tracer-path :error) e)))))) ; Record and exceptions

(defsc ParsingTracer [this {:keys [trace error query result]}]
  {:query         [:trace :error :query :result]
   :ident         (fn [] [:widget/by-id :tracer])
   :initial-state {:trace [] :error nil :query ""}}
  (dom/div
    (when error
      (dom/div (str error)))
    (dom/input {:type     "text"
                :value    query
                :onChange #(m/set-string! this :query :event %)})
    (dom/button {:onClick #(prim/transact! this `[(record-parsing-trace ~{:query query})])} "Run Parser")
    (dom/h4 "Parsing Trace")
    (html-edn trace)))

(def ui-tracer (prim/factory ParsingTracer))

(defsc Root [this {:keys [ui/tracer]}]
  {:query         [{:ui/tracer (prim/get-query ParsingTracer)}]
   :initial-state {:ui/tracer {}}}
  (ui-tracer tracer))

In the example above you should have seen that only the top-level keys trigger reads.

So, the query:

[:kw {:j [:v]}]

would result in a call to your read function on :kw and :j. Two calls. No automatic recursion. Done. The output value of the parser will be a map (that parse creates) which contains the keys (from the query, copied over by the parser) and values (obtained from your read):

{ :kw value-from-read-for-kw :j value-from-read-for-j }

Note that if your read accidentally returns a scalar for :j then you’ve not done the right thing…​a join like { :j [:k] } expects a result that is a vector of (zero or more) things or a singleton map that contains key :k.

{ :kw 21 :j { :k 42 } }
; OR
{ :kw 21 :j [{ :k 42 } {:k 43}] }

Dealing with recursive queries is a natural fit for a recusive algorithm, and it is perfectly fine to invoke the parser function to descend the query. In fact, the parser is passed as part of your environment.

So, the read function you write will receive three arguments, as described below:

  1. An environment containing:

    • :ast: An abstract syntax tree for the element, which contains:

      • :type: The type of node (e.g. :prop, :join, etc.)

      • :dispatch-key: The keyword portion of the triggering query element (e.g. :people/by-id)

      • :key: The full key of the triggering query element (e.g. [:people/by-id 1])

      • :query: (same as the query in env)

      • :children: If this node has sub-queries, will be AST nodes for those

      • others…​see documentation

    • :parser: The query parser

    • :query: if the element had one E.g. {:people [:user/name]} has :query [:user/name]

    • Components you requested be injected

  2. A dispatch key for the item that triggered the read (same as dispatch key in the AST)

  3. Parameters (which are nil if not supplied in the query)

It must return a value that has the shape implied by the grammar element being read.

Note
The following examples run various parsers against arbitrary queries. The ParseRunner component source looks like this:
(ns book.queries.parse-runner
  (:require [fulcro.client.primitives :as prim :refer [defsc]]
            [devcards.util.edn-renderer :refer [html-edn]]
            [cljs.reader :as r]
            [fulcro.client.mutations :as m :refer [defmutation]]
            [fulcro.client.dom :as dom]))

(defn parse-runner-path
  ([] [:widget/by-id :parse-runner])
  ([field] [:widget/by-id :parse-runner field]))

(defmutation run-query [{:keys [query database parser]}]
  (action [{:keys [state]}]
    (try
      (let [query  (r/read-string query)
            result (parser {:state (atom database)} query)]
        (swap! state update-in (parse-runner-path) assoc
          :error ""
          :result result))
      (catch js/Error e (swap! state assoc-in (parse-runner-path :error) e)))))

(defsc ParseRunner [this {:keys [ui/query error result]} {:keys [parser database]}]
  {:query         [:ui/query :error :result]
   :initial-state (fn [{:keys [query] :or {query ""}}] {:ui/query query :error "" :result {}})
   :ident         (fn [] [:widget/by-id :parse-runner])}
  (dom/div
    (dom/input {:type     "text"
                :value    query
                :onChange (fn [evt] (m/set-string! this :ui/query :event evt))})
    (dom/button {:onClick #(prim/transact! this `[(run-query ~{:query query :database database :parser parser})])} "Run Parser")
    (when error
      (dom/div (str error)))
    (dom/div
      (dom/h4 "Query Result")
      (html-edn result))
    (dom/div
      (dom/h4 "Database")
      (html-edn database))))

(def ui-parse-runner (prim/factory ParseRunner))

12.3.1. Reading a keyword

If the parser encounters a keyword :kw, your function will be called with:

(your-read
  { :dispatch-key :kw :parser (fn ...) } ;; the environment: parser, etc.
  :kw                                   ;; the keyword
  nil) ;; no parameters

your read function should return some value that makes sense for that spot in the grammar. There are no real restrictions on what that data value has to be in this case. You are reading a simple property. There is no further shape implied by the grammar. It could be a string, number, Entity Object, JS Date, nil, etc.

Due to additional features of the parser, your return value must be wrapped in a map with the key :value. If you fail to do this, you will get nothing in the result.

Thus, a very simple read for props (keywords) could be:

(defn read [env key params] { :value 42 })

below is an example that implements exactly this read and plugs it into a parser like this:

(defn read-42 [env key params] {:value 42})
(def parser-42 (prim/parser {:read read-42}))

The UI just passes your query off to the parser and shows the results.

Thus, the example returns the value 42 no matter what it is asked for. Run any query you want in it, and check out the answer.

Some examples to try:

  • [:a :b :c]

  • [:what-is-6-x-7]

  • [{:a {:b {:c {:d [:e]}}}}] (yes, there is only one answer)

Show/Hide Source
(ns book.queries.naive-read
  (:require [fulcro.client.primitives :as prim :refer [defsc]]
            [book.queries.parse-runner :refer [ParseRunner ui-parse-runner]]
            [fulcro.client.dom :as dom]))

(defn read-42 [env key params] {:value 42})
(def parser-42 (prim/parser {:read read-42}))

(defsc Root [this {:keys [parse-runner]}]
  {:initial-state {:parse-runner {}}
   :query         [{:parse-runner (prim/get-query ParseRunner)}]}
  (dom/div
    (ui-parse-runner (prim/computed parse-runner {:parser parser-42 :database {}}))))

So now you have a read function that returns the meaning of life the universe and everything in a single line of code! But now it is obvious that we need to build an even bigger machine to understand the question.

If your server state is just a flat set of scalar values with unique keyword identities, then a better read is similarly trivial:

(defn property-read [{:keys [state]} key params] {:value (get @state key :not-found)})
(def property-parser (prim/parser {:read property-read}))

It just assumes the property will be in the top-level of some injected state atom. Let’s try that out. The database we’re emulating is shown at the bottom of the example. Run some queries and see what you get. Some suggestions:

  • [:a :b :c]

  • [:what-is-6-x-7]

  • [{:a {:b {:c {:d [:e]}}}}] (yes, there is only one answer)

Show/Hide Source
(ns book.queries.simple-property-read
  (:require [fulcro.client.primitives :as prim :refer [defsc]]
            [book.queries.parse-runner :refer [ParseRunner ui-parse-runner]]
            [fulcro.client.dom :as dom]))

(defn property-read [{:keys [state]} key params] {:value (get @state key :not-found)})
(def property-parser (prim/parser {:read property-read}))

(defsc Root [this {:keys [parse-runner]}]
  {:initial-state {:parse-runner {}}
   :query         [{:parse-runner (prim/get-query ParseRunner)}]}
  (dom/div
    (ui-parse-runner (prim/computed parse-runner {:parser property-parser :database {:a 1 :b 2 :c 99}}))))

The result of those nested queries (the last suggestion above) is supposed to be a nested map. So, obviously we have more work to do.

12.3.2. Reading a join

Your state probably has some more structure to it than just a flat bag of properties. Joins are naturally recursive in syntax, and those that are accustomed to writing parsers probably already see the solution.

First, let’s clarify what the read function will receive for a join. When parsing:

{ :j [:a :b :c] }

your read function will be called with:

(your-read { :state state :parser (fn ...) :query [:a :b :c] } ; the environment
           :j                                                 ; keyword of the join
           nil) ; no parameters

But just to prove a point about the separation of database format and query structure we’ll implement this next example with a basic recursive parse, but use more flat data (the following is live code):

(def flat-app-state {:a 1 :user/name "Sam" :c 99})

(defn flat-state-read [{:keys [state parser query] :as env} key params]
  (if (= :user key)
    {:value (parser env query)}                             ; recursive call. query is now [:user/name]
    {:value (get @state key)}))                             ; gets called for :user/name :a and :c

(def my-parser (prim/parser {:read flat-state-read}))

The important bit is the then part of the if. Return a value that is the recursive parse of the sub-query. Otherwise, we just look up the keyword in the state (which as you can see is a very flat map).

Try running the query [:a {:user [:user/name]} :c]:

Show/Hide Source
(ns book.queries.parsing-simple-join
  (:require [fulcro.client.primitives :as prim :refer [defsc]]
            [book.queries.parse-runner :refer [ParseRunner ui-parse-runner]]
            [fulcro.client.dom :as dom]))

(def flat-app-state {:a 1 :user/name "Sam" :c 99})

(defn flat-state-read [{:keys [state parser query] :as env} key params]
  (if (= :user key)
    {:value (parser env query)}                             ; recursive call. query is now [:user/name]
    {:value (get @state key)}))                             ; gets called for :user/name :a and :c

(def my-parser (prim/parser {:read flat-state-read}))

(defsc Root [this {:keys [parse-runner]}]
  {:initial-state (fn [params] {:parse-runner (prim/get-initial-state ParseRunner {:query "[:a {:user [:user/name]} :c]"})})
   :query         [{:parse-runner (prim/get-query ParseRunner)}]}
  (dom/div
    (ui-parse-runner (prim/computed parse-runner {:parser my-parser :database flat-app-state}))))

The first (possibly surprising thing) is that your result includes a nested object. The parser creates the result, and the recursion naturally nested the result correctly.

Next you should remember that a join implies there could be one OR many results. The singleton case is fine (e.g. putting a single map there). If there are multiple results it should be a vector.

In this case we’re just showing calling the parser recursively. Notice that it in turn will call your read function again. In a real application your data will not be this flat so you will almost certainly not do things in quite this way.

Let’s put a little better state in our application and write a more realistic parser.

12.3.3. A Non-trivial, recursive example

Let’s start with the following hand-normalized application state:

(def app-state (atom {
                      :window/size  [1920 1200]
                      :friends      [[:people/by-id 1] [:people/by-id 3]]
                      :people/by-id {
                                     1 {:id 1 :name "Sally" :age 22 :married false}
                                     2 {:id 2 :name "Joe" :age 22 :married false}
                                     3 {:id 3 :name "Paul" :age 22 :married true}
                                     4 {:id 4 :name "Mary" :age 22 :married false}}}))

Our friend db→tree could handle queries against this database, but let’s implement it by hand.

Say we want to run this query:

(def query [:window/size {:friends [:name :married]}])

From the earlier discussion you see that we’ll have to handle the top level keys one at a time.

For this query there are only two keys to handle: :friends and :window-size. So, let’s write a case for each:

(defn read [{:keys [state query]} key params]
  (case key
    :window/size {:value (get @state :window/size)}
    :friends (let [friend-ids (get @state :friends)
                   get-friend (fn [id] (select-keys (get-in @state id) query))
                   friends (mapv get-friend friend-ids)]
               {:value friends})
    nil))

The default case is nil, which means if we supply an errant key in the query no exception will happen.

You can try it out below:

Show/Hide Source
(ns book.queries.parsing-recursion-one
  (:require [fulcro.client.primitives :as prim :refer [defsc]]
            [book.queries.parse-runner :refer [ParseRunner ui-parse-runner]]
            [fulcro.client.dom :as dom]))

(def database {:window/size  [1920 1200]
               :friends      #{[:people/by-id 1] [:people/by-id 3]}
               :people/by-id {
                              1 {:id 1 :name "Sally" :age 22 :married false}
                              2 {:id 2 :name "Joe" :age 22 :married false}
                              3 {:id 3 :name "Paul" :age 22 :married true}
                              4 {:id 4 :name "Mary" :age 22 :married false}}})

(defn read [{:keys [state query]} key params]
  (case key
    :window/size {:value (get @state :window/size)}
    :friends (let [friend-ids (get @state :friends)
                   get-friend (fn [id] (select-keys (get-in @state id) query))
                   friends    (mapv get-friend friend-ids)]
               {:value friends})
    nil))

(def parser (prim/parser {:read read}))
(def query "[:window/size {:friends [:name :married]}]")

(defsc Root [this {:keys [parse-runner]}]
  {:initial-state (fn [params] {:parse-runner (prim/get-initial-state ParseRunner {:query query})})
   :query         [{:parse-runner (prim/get-query ParseRunner)}]}
  (dom/div
    (ui-parse-runner (prim/computed parse-runner {:parser parser :database database}))))

Those of you paying close attention will notice that we have yet to need recursion. We’ve also done something a bit naive: select-keys assumes that query contains only keys! What if query followed an ident link to :married-to:

[:window/size {:friends [:name :age {:married-to [:name]}]}]

and the database was:

{:window/size  [1920 1200]
               :friends      [[:people/by-id 1] [:people/by-id 3]]
               :people/by-id {
                              1 {:id 1 :name "Sally" :age 22 :married false}
                              2 {:id 2 :name "Joe" :age 33 :married false}
                              3 {:id 3 :name "Paul" :age 45 :married true :married-to [:people/by-id 1]}
                              4 {:id 4 :name "Mary" :age 19 :married false}}}

Now things get interesting, and I’m sure more than one reader will have an opinion on how to proceed. My aim is to show that the parser can be called recursively to handle these things, not to find the perfect structure for the parser in general, so I’m going to do something simple.

The primary trick I’m going to exploit is the fact that env is just a map, and that we can add stuff to it. When we are in the context of a person, we’ll add :person to the environment, and pass that to a recursive call to parser.

The example below (with source) shows the result:

Show/Hide Source
(ns book.queries.parsing-recursion-two
  (:require [fulcro.client.primitives :as prim :refer [defsc]]
            [book.queries.parse-runner :refer [ParseRunner ui-parse-runner]]
            [fulcro.client.dom :as dom]))

(def database {:window/size  [1920 1200]
               :friends      [[:people/by-id 1] [:people/by-id 3]]
               :people/by-id {
                              1 {:id 1 :name "Sally" :age 22 :married false}
                              2 {:id 2 :name "Joe" :age 33 :married false}
                              3 {:id 3 :name "Paul" :age 45 :married true :married-to [:people/by-id 1]}
                              4 {:id 4 :name "Mary" :age 19 :married false}}})

; we're going to add person to the env as we go
(defn read [{:keys [parser ast state query person] :as env} dispatch-key params]
  (letfn [(parse-friend
            ; given a person-id, parse the current query by placing that person's data in the
            ; environment, and call the parser recursively.
            [person-id]
            (if-let [person (get-in @state person-id)]
              (parser (assoc env :person person) query)
              nil))]
    (case dispatch-key
      ; These trust that a person has been found and placed in the env
      :name {:value (get person dispatch-key)}
      :id {:value (get person dispatch-key)}
      :age {:value (get person dispatch-key)}
      :married {:value (get person (:key ast))}
      ; a to-one join
      :married-to (if-let [pid (get person dispatch-key)]
                    {:value (parse-friend pid)}
                    nil)
      ; these assume we're asking for the top-level state
      :window/size {:value (get @state :window/size)}
      ; a to-many join
      :friends (let [friend-ids (get @state :friends)]
                 {:value (mapv parse-friend friend-ids)})
      nil)))

(def parser (prim/parser {:read read}))
(def query "[:window/size {:friends [:name :age {:married-to [:name]}]}]")

(defsc Root [this {:keys [parse-runner]}]
  {:initial-state (fn [params] {:parse-runner (prim/get-initial-state ParseRunner {:query query})})
   :query         [{:parse-runner (prim/get-query ParseRunner)}]}
  (dom/div
    (ui-parse-runner (prim/computed parse-runner {:parser parser :database database}))))

It can be a little bit of work to build these parsers for the queries (which is why libraries exist so you don’t have to); however, we hope you can see that it is actually pretty tractable to build them once you understand the basics.

Now we’ll move on to another thing that servers typically need in their queries: parameters!

12.4. Parameters

The Fulcro query grammar accept parameters on most elements. These are intended to be combined with dynamic queries that will allow your UI to have some control over what you want to read from the application state (think filtering, pagination, sorting, and such).

As you might expect, the parameters on a particular expression in the query are just passed into your read function as the third argument. You are responsible for both defining and interpreting them. They have no rules other than that they are maps.

To read the property :load/start-time with a parameter indicating a particular time unit you might use a query like:

[(:load/start-time {:units :seconds})]

this will invoke read with:

(your-read env :load/start-time { :units :seconds})

the implication is clear. The code is up to you. Let’s add some quick support for this in our read so you can try it out.

The parser below understands the following queries:

[(:load/start-time {:units :seconds})]

[(:load/start-time {:units :minutes})]

[(:load/start-time {:units :ms})]
Show/Hide Source
(ns book.queries.parsing-parameters
  (:require [fulcro.client.primitives :as prim :refer [defsc]]
            [book.queries.parse-runner :refer [ParseRunner ui-parse-runner]]
            [fulcro.client.dom :as dom]))

(def database {
               :load/start-time 40000.0                     ;stored in ms
               })

(defn convert [ms units]
  (case units
    :minutes (/ ms 60000.0)
    :seconds (/ ms 1000.0)
    :ms ms))

(defn read [{:keys [state]} key params]
  (case key
    :load/start-time {:value (convert (get @state key) (or (:units params) :ms))}
    nil))

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


(defsc Root [this {:keys [parse-runner]}]
  {:initial-state (fn [params] {:parse-runner (prim/get-initial-state ParseRunner {:query "[(:load/start-time {:units :seconds})]"})})
   :query         [{:parse-runner (prim/get-query ParseRunner)}]}
  (dom/div
    (ui-parse-runner (prim/computed parse-runner {:parser parser :database database}))))

12.5. Using a Completely Custom Parser

Most of this book has assumed you’ll be using Fulcro’s predefined server parser. Note that you can still do that and switch out to an alternate (custom) parser at any phase of parsing. You can even install a custom parser in your server (though then the macros for defining mutations and query handlers won’t work for you). Parsers can be constructed using the prim/parser function.

12.6. Parsing on the Client

Fulcro allows you to augment the built-in query parser for local reads on the client. It uses the exact same techniques discussed above, and it is similar in that you must be able to start at the root of the query (even though you may only want to augment something rather deep in the query tree).

Actually, there are two cases that such a custom read must be able to do:

  • Handle the path from root to the point of interest (it can hand off uninteresting side branches to db→tree.

  • Handle (or safely ignore) ident-based queries for any "virtual" entities that are purely parser-generated.

The first is a limitation of how queries are processed. Fulcro normally runs the query through db→tree, which attempts to fill the entire result. If you supply a :read-local function during client construction, then your :read-local will get first shot at each element of the query that was submitted. Note that the query can start at an ident. If your read-local function returns nil for an element, then the normal Fulcro processing takes place on that sub-query. If your read-local returns a value, then that is all of the processing that is done for the sub-tree rooted at that key. Thus, custom client parsing always requires you to process sub-trees of the query, not just individual elements. Of course, you can use db→tree at any time to "finish out" some subquery against real data.

This has the advantage of letting you dynamically fill queries without having to have a concrete representation of the graph in your database. This can be helpful if you have some rapidly changing data (e.g. updated by a web worker) and some views of that data that would otherwise be hard to keep up-to-date.

It is also useful if you’re trying to port from Om Next.

13. UI Routing

UI Routing is a very important task in Fulcro. It is the primary means by which you keep your application running quickly. You see, in Fulcro your query is run from root. If your entire application’s query runs on every render frame things can get slow indeed.

The solution is easy: use union queries with to-one relations to ensure only the portions of your query that are active on the UI are processed.

Unfortunately many people find hand-writing union components a litle challenging. Fulcro provides a nice pre-written facility that can write much of the code for you, making the process more conceptual as UI Routing.

13.1. A Basic Router

A basic router looks like this:

(defsc Index [this {:keys [db/id router/page]}]
  {:query         [:db/id :router/page]
   :ident         (fn [] [page id])
   :initial-state {:db/id 1 :router/page :PAGE/index}}
  ...)

(defsc Settings [this {:keys [db/id router/page]}]
  {:query         [:db/id :router/page]
   :ident         (fn [] [page id])
   :initial-state {:db/id 1 :router/page :PAGE/settings}}
  ...)

(defrouter RootRouter :root/router
  ; OR (fn [t p] [(:router/page p) (:db/id p)])
  [:router/page :db/id]
  :PAGE/index Index
  :PAGE/settings Settings)

The important points are:

  1. The ident generator for the components and router must all work the same. The router uses the first element of the ident to pick the screen.

  2. The list of screens to route to in the router are keyed by that first element of the ident.

  3. The components that act as screens should:

    1. Have initial state that will work with ident

    2. Use an ident function that returns the same thing that the router will extract.

The router itself then works like any other Fulcro component. You make a factory for it, join it into your query, and render it by passing the queried props to it.

Warning
The defrouter macro allows you to specify a vector for the ident argument where both keys are looked up in the component’s props; However, the defsc macro assumes that a vector shorthand for idents contains a table name and something to look up. This is the most convenient behavior for both, but since it does not match use this rule: If you’re using defsc to make a screen component that will be used with a router then you always want the lambda form, not the vector template in that defsc.
Show/Hide Source
(ns book.simple-router-1
  (:require [fulcro.client.routing :as r :refer-macros [defrouter]]
            [fulcro.client.dom :as dom]
            [fulcro.client :as fc]
            [fulcro.client.data-fetch :as df]
            [fulcro.client.primitives :as prim :refer [defsc]]
            [fulcro.client.mutations :as m]))

(defsc Index [this {:keys [db/id router/page]}]
  {:query         [:db/id :router/page]
   :ident         (fn [] [page id])                         ; IMPORTANT! Look up both things, don't use the shorthand for idents on screens!
   :initial-state {:db/id 1 :router/page :PAGE/index}}
  (dom/div nil "Index Page"))

(defsc Settings [this {:keys [db/id router/page]}]
  {:query         [:db/id :router/page]
   :ident         (fn [] [page id])
   :initial-state {:db/id 1 :router/page :PAGE/settings}}
  (dom/div "Settings Page"))

(defrouter RootRouter :root/router
  ; OR (fn [t p] [(:router/page p) (:db/id p)])
  [:router/page :db/id]
  :PAGE/index Index
  :PAGE/settings Settings)

(def ui-root-router (prim/factory RootRouter))

(defsc Root [this {:keys [router]}]
  {:initial-state (fn [p] {:router (prim/get-initial-state RootRouter {})})
   :query         [{:router (prim/get-query RootRouter)}]}
  (dom/div
    (dom/a {:onClick #(prim/transact! this
                        `[(r/set-route {:router :root/router
                                        :target [:PAGE/index 1]})])} "Index") " | "
    (dom/a {:onClick #(prim/transact! this
                        `[(r/set-route {:router :root/router
                                        :target [:PAGE/settings 1]})])} "Settings")
    (ui-root-router router)))

Be sure to look at the database view in the example above. Notice that all that has to happen is a change of a single ident. This as the effect of switching the rendering, and choosing the subquery for the remainder of the visible UI.

It is very easy from here to compose together as many of these as you’d like in order to build a more complicated UI. For example, the settings could have several subscreens as in this example:

Show/Hide Source
(ns book.simple-router-2
  (:require [fulcro.client.routing :as r :refer-macros [defrouter]]
            [fulcro.client.dom :as dom]
            [fulcro.client :as fc]
            [fulcro.client.primitives :as prim :refer [defsc]]
            [fulcro.client.mutations :as m]))

(defsc Index [this {:keys [router/page db/id]}]
  {:query         [:db/id :router/page]
   :ident         (fn [] [page id])
   :initial-state {:db/id 1 :router/page :PAGE/index}}
  (dom/div "Index Page"))

(defsc EmailSettings [this {:keys [db/id router/page]}]
  {:query         [:db/id :router/page]
   :ident         (fn [] [page id])
   :initial-state {:db/id 1 :router/page :PAGE/email}}
  (dom/div "Email Settings Page"))

(defsc ColorSettings [this {:keys [db/id router/page]}]
  {:query         [:db/id :router/page]
   :ident         (fn [] [page id])
   :initial-state {:db/id 1 :router/page :PAGE/color}}
  (dom/div "Color Settings"))

(defrouter SettingsRouter :settings/router
  [:router/page :db/id]
  :PAGE/email EmailSettings
  :PAGE/color ColorSettings)

(def ui-settings-router (prim/factory SettingsRouter))

(defsc Settings [this {:keys [router/page db/id subpage]}]
  {:query         [:db/id :router/page {:subpage (prim/get-query SettingsRouter)}]
   :ident         (fn [] [page id])
   :initial-state (fn [p]
                    {:db/id       1
                     :router/page :PAGE/settings
                     :subpage     (prim/get-initial-state SettingsRouter {})})}
  (dom/div
    (dom/a {:onClick #(prim/transact! this
                        `[(r/set-route {:router :settings/router
                                        :target [:PAGE/email 1]})])} "Email") " | "
    (dom/a {:onClick #(prim/transact! this
                        `[(r/set-route {:router :settings/router
                                        :target [:PAGE/color 1]})])} "Colors")
    (js/console.log :p (prim/props this))
    (ui-settings-router subpage)))

(defrouter RootRouter :root/router
  ; OR (fn [t p] [(:router/page p) (:db/id p)])
  [:router/page :db/id]
  :PAGE/index Index
  :PAGE/settings Settings)

(def ui-root-router (prim/factory RootRouter))

(defsc Root [this {:keys [router]}]
  {:initial-state (fn [p] {:router (prim/get-initial-state RootRouter {})})
   :query         [{:router (prim/get-query RootRouter)}]}
  (dom/div
    (dom/a {:onClick #(prim/transact! this
                        `[(r/set-route {:router :root/router
                                        :target [:PAGE/index 1]})])} "Index") " | "
    (dom/a {:onClick #(prim/transact! this
                        `[(r/set-route {:router :root/router
                                        :target [:PAGE/settings 1]})])} "Settings")
    (ui-root-router router)))

This allows you to build up a tree of routers that keeps your query minimal, and allows for very nice dynamic structuring of the applicataion at runtime.

If you have screens that could have different instances (for example, different reports), then each report could have an ID, and routing would involve selecting the screen’s table, as well as a distinct ID.

The problem, of course, is that managing all of these routers in your application logic becomes somewhat of a chore. Also, it is common to want to mix UI routing with HTML5 history, where only a single "route" is spelled out, but you may need to logically "switch" any number of these UI routers to reach the indicated screen.

For example, one could imagine wanting to go to /settings/colors as a URI for the previous example. That single URI as a concept is a single route to a screen, but the mutation you’d trigger would be to set-route on two different routers:

(prim/transact! this `[(r/set-route ...) (r/set-route ...)])

13.2. Routing Tree Support

Fulcro includes some routing tree primitives to do the mapping from single conceptual "routes" like /settings/colors to a set of instructions that you need to send to your UI routers. There is an additional concern as well: route parameters. It is quite common to want to interpret URIs like /user/436 as a route that populates a given screen with some data.

Thus, the tree support is based on the concept of a Route Handler and Route Parameters.

Defining routes requires just a few steps:

  1. Define routers as shown in the prior section, giving each router a distinct ID.

  2. Give each routable screen (i.e. URI) in your tree a handler name. The example below shows two routers with 5 conceptual target screen. The screens have handler names :main, :login, etc.

                  (top-router w/id :top-router)
                     ----------------------
                    /     /     |          \
               :main  :login  :new-user    (report router w/id :report-router)
                                                 |
                                  (reports shared content screen)
                                                 |
                                                / \
                                          :status  :graph
  3. Define your routing tree. This is a data structure that gives instructions to one-or-more routers that are necessary to display the screen with a given handler name. In the above example you need to tell both the top router and report router to change what they are showing in order to get a :status or :graph onto the screen.

    (def routing-tree
      "A map of route handling instructions. A given route has a handler name (e.g. `:main`) which is
      thought of as the target of a routing operation (i.e. interpretation of a URI). It also has a vector
      of `router-instruction`s, which say
    
      1. which router should be changed
      2. What component instance that router should point to (by ident)
    
      The routing tree for the diagram above is therefore:
      "
      (r/routing-tree
        (r/make-route :main [(r/router-instruction :top-router [:main :top])])
        (r/make-route :login [(r/router-instruction :top-router [:login :top])])
        (r/make-route :new-user [(r/router-instruction :top-router [:new-user :top])])
        (r/make-route :graph [(r/router-instruction :top-router [:report :top])
                              (r/router-instruction :report-router [:graphing-report :param/report-id])])
        (r/make-route :status [(r/router-instruction :top-router [:report :top])
                               (r/router-instruction :report-router [:status-report :param/report-id])])))
  4. Compose the application as normal, placing the routers as shown in the prior section.

13.3. Using the Routing Tree

The routing namespace includes a Fulcro mutation for triggering the routing tree handler. It takes a :handler (e.g. :main) and an optional :route-params argument:

; assumes you've aliased fulcro.client.routing to r
(prim/transact! `[(r/route-to {:handler :main})])

Running this mutation will run all of the router-instruction, and will also do route parameter substitution on the resulting idents.

13.3.1. Route Parameters

Anything you pass in the :route-params map will get automatically plugged into the parameter placeholders in your routing tree instructions. By default anything that looks like an integer (only digits) will be coerced to an integer. Anything that contains only letters will map to a keyword.

If the default coercion isn’t sufficient then you can customize it using parameter coercion.

It is a very common task to need to convert incoming strings (e.g. from a URL) to elements of an ident. If you’d like to use this support in your own code then use (r/set-ident-route-params ident params) which supports the coercion and replacement:

(r/set-ident-route-params [:param/table :param/id] {:table "a" :id "1"})
; => [:a 1]

Note that the params argument is just what you’d get from a URL when using something like bidi (everything will come in as strings).

Parameter Coercion

The default coercion converts integer-looking things to integers, and string-looking things to keywords.

There is a multimethod r/coerce-param that dispatches on :param/X and replaces the value with whatever you return. You customize coercion simply by adding your own coercion for a parameter by name:

(defmethod r/coerce-param :param/NAME
  [k incoming-string-value]
  (transform-it incoming-string-value))

Of course, be sure that your namespace with the defmethod is loaded so that your methods get installed.

13.4. Examining Routes in UI and Mutations

Your UI will often want to rely on knowing the "current" route of a given router in order to give user navigation feedback. You cannot embed your router in the query, because that would often make the query have a circular reference and blow the stack.

The only real bit of information in a router that is useful is the current route The current-route function can be used in a mutation or component (by querying for the router table) to check the route:

(ns x
  (:require [fulcro.client.routing :as r]
            [fulcro.client.primitives :as prim]))

(r/defrouter SomeRouter :top-router
  (ident [this props] ...)
  :home-page HomePage
  :about-page AboutPage
  ...)

(defmutation do-something-with-routes [params]
  (action [{:keys [state]}]
    (let [current (r/current-route state :top-router)] ; current will be an ident of a screen of :top-router
    ...)))

(defsc NavBar [this props]
  {:query (fn [] [ [r/routers-table '_] ])
   :initial-state (fn [p] {})}
  (let [current (current-route props :top-router)] ; current will be an ident of a screen of :top-router
     ...))

13.5. A Complete UI Routing Example

The following shows the example routing tree in a complete running demo:

Show/Hide Source
(ns book.ui-routing
  (:require [fulcro.client.routing :as r :refer-macros [defrouter]]
            [fulcro.client.dom :as dom]
            [fulcro.client :as fc]
            [fulcro.client.data-fetch :as df]
            [fulcro.client.primitives :as prim :refer [defsc]]
            [fulcro.client.mutations :as m]))

(defsc Main [this {:keys [label]}]
  {:initial-state {:page :main :label "MAIN"}
   :query         [:page :label]}
  (dom/div {:style {:backgroundColor "red"}}
    label))

(defsc Login [this {:keys [label]}]
  {:initial-state {:page :login :label "LOGIN"}
   :query         [:page :label]}
  (dom/div {:style {:backgroundColor "green"}}
    label))

(defsc NewUser [this {:keys [label]}]
  {:initial-state {:page :new-user :label "New User"}
   :query         [:page :label]}
  (dom/div {:style {:backgroundColor "skyblue"}}
    label))

(defsc StatusReport [this {:keys [id]}]
  {:initial-state {:id :a :page :status-report}
   :query         [:id :page :label]}
  (dom/div {:style {:backgroundColor "yellow"}}
    (dom/div (str "Status " id))))

(defsc GraphingReport [this {:keys [id]}]
  {:initial-state {:id :a :page :graphing-report}
   :query         [:id :page :label]}                       ; make sure you query for everything need by the router's ident function!
  (dom/div {:style {:backgroundColor "orange"}}
    (dom/div (str "Graph " id))))

(defrouter ReportRouter :report-router
  ; This router expects numerous possible status and graph reports. The :id in the props of the report will determine
  ; which specific data set is used for the screen (though the UI of the screen will be either StatusReport or GraphingReport
  ; IMPORTANT: Make sure your components (e.g. StatusReport) query for what ident needs (i.e. in this example
  ; :page and :id at a minimum)
  [:page :id]
  :status-report StatusReport
  :graphing-report GraphingReport)

(def ui-report-router (prim/factory ReportRouter))

; BIG GOTCHA: Make sure you query for the prop (in this case :page) that the union needs in order to decide. It won't pull it itself!
(defsc ReportsMain [this {:keys [report-router]}]
  ; nest the router under any arbitrary key, just be consistent in your query and props extraction.
  {:initial-state (fn [params] {:page :report :report-router (prim/get-initial-state ReportRouter {})})
   :query         [:page {:report-router (prim/get-query ReportRouter)}]}
  (dom/div {:style {:backgroundColor "grey"}}
    ; Screen-specific content to be shown "around" or "above" the subscreen
    "REPORT MAIN SCREEN"
    ; Render the sub-router. You can also def a factory for the router (e.g. ui-report-router)
    (ui-report-router report-router)))

(defrouter TopRouter :top-router
  (fn [this props] [(:page props) :top])
  :main Main
  :login Login
  :new-user NewUser
  :report ReportsMain)

(def ui-top (prim/factory TopRouter))

(def routing-tree
  "A map of route handling instructions. The top key is the handler name of the route which can be
  thought of as the terminal leaf in the UI graph of the screen that should be \"foremost\".

  The value is a vector of routing-instructions to tell the UI routers which ident
  of the route that should be made visible.

  A value in this ident using the `param` namespace will be replaced with the incoming route parameter
  (without the namespace). E.g. the incoming route-param :report-id will replace :param/report-id"
  (r/routing-tree
    (r/make-route :main [(r/router-instruction :top-router [:main :top])])
    (r/make-route :login [(r/router-instruction :top-router [:login :top])])
    (r/make-route :new-user [(r/router-instruction :top-router [:new-user :top])])
    (r/make-route :graph [(r/router-instruction :top-router [:report :top])
                          (r/router-instruction :report-router [:graphing-report :param/report-id])])
    (r/make-route :status [(r/router-instruction :top-router [:report :top])
                           (r/router-instruction :report-router [:status-report :param/report-id])])))

(defsc Root [this {:keys [top-router]}]
  ; r/routing-tree-key implies the alias of fulcro.client.routing as r.
  {:initial-state (fn [params] (merge routing-tree
                                 {:top-router (prim/get-initial-state TopRouter {})}))
   :query         [{:top-router (prim/get-query TopRouter)}]}
  (dom/div
    ; Sample nav mutations
    (dom/a {:onClick #(prim/transact! this `[(r/route-to {:handler :main})])} "Main") " | "
    (dom/a {:onClick #(prim/transact! this `[(r/route-to {:handler :new-user})])} "New User") " | "
    (dom/a {:onClick #(prim/transact! this `[(r/route-to {:handler :login})])} "Login") " | "
    (dom/a {:onClick #(prim/transact! this `[(r/route-to {:handler :status :route-params {:report-id :a}})])} "Status A") " | "
    (dom/a {:onClick #(prim/transact! this `[(r/route-to {:handler :graph :route-params {:report-id :a}})])} "Graph A")
    (ui-top top-router)))

13.6. Combining Routing with Data Management

Of course you can compose this with other mutations into a single transaction. This is common when you’re trying to switch to a screen whose data might not yet exist:

(prim/transact! `[(ensure-report-loaded {:report-id :a}) (r/route-to {:graph :a})])

here we’re assuming that ensure-report-loaded is a mutation that ensures that there is at least placeholder data in place (or the UI rendering might look a bit odd or otherwise fail from lack of data). It may also do things like trigger background loads that will fufill the graph’s needs, something like this:

(defmutation ensure-report-loaded [{:keys [report-id]}]
  (action [{:keys [state] :as env}]
    (let [when-loaded (get-in @state [:reports/by-id report-id :load-time-ms] 0)
          is-missing? (= 0 when-loaded)
          now-ms (.getTime (js/Date.))
          age-ms (- now-ms when-loaded)
          should-be-loaded? (or (too-old? age-ms) is-missing?)]
      ; if missing, put placeholder
      ; if too old, add remote load to Fulcro queue (see data-fetch for remote-load and load-action)
      (when is-missing? (swap! state add-report-placeholder report-id))
      (when should-be-loaded? (df/load-action env [:reports/by-id report-id] StatusReport)))})
  (remote [env] (df/load-action env)))

Additional mutations might do things like garbage collect old data that is not in the view. You may also need to trigger renders of things like your main screen with follow-on reads (e.g. of a keyword on the root component of your UI). Of course, combining such things into functions adds a nice touch:

(defn show-report!
  [component report-id]
  (prim/transact! component `[(app/clear-old-reports)
                              (app/ensure-report-loaded {:report-id ~report-id})
                              (r/route-to {:graph ~report-id})
                              :top-level-key]))

which can then be used more cleanly in the UI:

(dom/a {:onClick #(show-report! this :a)} "Report A")

13.7. Mutations with Routing

In some cases you will find it most convenient to do your routing within a mutation itself. This will let you check state, trigger loads, etc. If you trigger loads, then you can also easily defer the routing until the load completes. Of course, in that case you may want to do something in the state to cause your UI to indicate the routing is in progress.

There is nothing special about this technique. There are several functions in the routing namespace that can be used easily within your own mutations:

  • update-routing-links - For standard union-based defrouter (does not support dynamic code loading routers): Takes the state map and a route match (map with :handler and :route-params) and returns a new state map with the routes updated.

  • route-to-impl! - For all kinds of routers (including dynamic): Takes the mutation env and a bidi-style match {:handler/:params}. Works with dynamic routes. Does swaps against app state, but is safe to use within a mutation.

  • set-route - Changes the current route on a specific defrouter instance. Takes a state map, router ID, and a target ident. Used if not using routing trees or dynamic routers.

13.8. HTML5 Routing

Hooking HTML5 or hash-based routing up to this is relatively simple using, for example, pushy and bidi.

We do not provide direct support for this, since your application will need to make a number of decisions that really are local to the specific app:

  • How to map URIs to your leaf screens. If you use bidi then bidi-match will return exactly what you need from a URI route match (e.g. {:handler :x :route-params {:p v}}).

  • How to grab the URI bits you need. For example, pushy lets you hook up to HTML5 history events.

  • If a routing decision should be deferred/reversed? E.g. navigation should be denied until a form is saved.

  • How you want to update the URI on routing. You can define your own additional mutation to do this (e.g. via pushy/set-token!) and possibly compose it into a new mutations with route-to. The function r/update-routing-links can be used for such a composition:

; in some defmutation
(swap! state (fn [m]
                (-> m
                    (r/update-routing-links { :handler :h :route-params p })
                    (app/your-state-updates)))
(pushy/set-token! your-uri-interpretation-of-h)

See the fulcro-template on github. It supports HTML5 routing with a demo tree.

14. Forms Overview

On the surface forms are trivial: you have DOM input fields, users put stuff in them, and you submit that to a server. For really simple forms you already have sufficient tools and you can simply code them however you see fit.

The next most critical thing you’ll want is some help with managing the meta-state that goes with most form interactions:

  • When is the content of a field valid?

  • When should you show a validation error message? E.g. you should not tell a user that they made a mistake on a field they have yet to touch.

  • How do you "reset" the form if the user changes their mind or cancels an edit?

  • How do you ensure that just the data that has changed is sent to the server?

These more advanced interactions require that you track a few things:

  • The validation rules

  • Which fields are "complete" (ready for validation)?

  • What was the state of the form fields before the user started interacting with it?

  • How do you transition states (e.g. indicate that the updated form is now the new "real state"?

You will also commonly need a way to deal with the fact that a form may cross several entities in your database, generating a more global top-level form concern: are all of the entities in this form valid?

Again, you can certainly code all of this by hand, but Fulcro includes two different namespaces of helpers that can make dealing with these aspects of forms a little easier. The reason there are two is that the older version was not easy to change without breaking existing code, so new functions were written in a new namespace as an alternative.

The form state support concentrates just on providing utilities to manage the data, and has validation that is based on Clojure Spec but is completely pluggable. The full form management support is the older version that attempts to isolate your components a bit more from the event and state management, but at the expense of some added complexity.

Both are fully supported, though the form state support is considered the cleaner implementation.

15. Form State Support (version 2.1.4+)

The namespace fulcro.ui.form-state (aliased to fs in this chapter) includes functions and mutations for working with your entity as a form. This support brings functions for dealing with common state storage and form transitions with minimal opinion or additional complexity.

Your UI is still built and rendered identically to what you’re already used to. The form state support simply adds some additional state tracking that can help you manage things like field validation and minimal delta submissions to the server.

15.1. Defining the Form Component

A component that wishes to act as a form must have an ident and a query. There are two additional steps you must do to prepare your component to work with form state management:

  1. Add a form configuration join to your query.

  2. Declare which of your props/joins are part of the form.

So, a minimal form-state-compatible component looks like this:

(defsc NameForm [this props]
  {:query [:id :name fs/form-config-join] ; fs is fulcro.ui.form-state
   :ident [:name-form/by-id :id]
   :form-fields #{:name}} ; MUST be a set of keywords from your query
  ...)

Technically this adds a protocol to the generated component. If you’re using defui, it looks like this:

(defui NameForm
  static fs/IFormFields
  (form-field [this] #{:name})
  ...)

next, you’ll want to populate your state with some data. Of course during this step you’ll need to populate that form configuration data.

15.2. Form Configuration

Form state is stored in a form configuration entity in your app state database. This configuration entity includes:

  • The "pristine" state of your entity.

  • Which properties (and joins) of your entity are part of the form.

  • Which properties are "complete" (ready for validation).

  • A map of which parts of the form come from which declared component.

The form state is normalized into your state database. There are two ways of adding this configuration:

  1. Add it to a tree of initial (or incoming) state, and merge that (which will normalize it all).

  2. Add it directly to your state database.

both methods are supported:

15.2.1. Initializing a Tree of Data

This case occurs when you have either some initial state or a function on the client side that generate a new entity (i.e. with a tempid) and you want to immediately use it with a form. Forms can be nested into a group, and the functions automatically support initializing the configuration recursively for a given form set.

Say you have a person with multiple (normalized) phone numbers. You want to make a new person and set them up with an initial phone number to fill out. The tree for that data might look like this:

(def person-tree
  { :db/id (prim/tempid)
    :person/name "Joe"
    :person/phone-numbers [{:db/id (prim/tempid) :phone/number "555-1212"}]})

with components like:

(defsc PhoneForm [this props]
  {:query [:db/id :phone/number fs/form-config-join]
   :ident [:phone/by-id :db/id]
   :form-fields #{:phone/number}}
  ...)

(defsc PersonForm [this props]
  {:query [:db/id :person/name {:person/phone-numbers (prim/get-query PhoneForm)}
           fs/form-config-join]
   :ident [:person/by-id :db/id]
   :form-fields #{:person/name :person/phone-numbers}}
  ...)

Normally you might throw this into your application state with something like:

(prim/merge-component! reconciler Person person-tree)

which will insert and normalize the person and phone number.

You can first augment the form with form configuration like so:

(def person-tree-with-form-support (fs/add-form-config Person person-tree))
(prim/merge-component! reconciler Person person-tree-with-form-support)
Important
add-form-config will look at all of the form (and subforms reachable from that form), but it will only add config to the ones that are missing it. This means the current form state is not reset by this call.

15.2.2. Initializing in a Mutation

The other very common case is this: You’ve loaded something from the server, and you’d like to use it as the basis for form fields. In this case the data is already normalized in your state database, and you’ll need to work on it via a mutation.

The add-form-config* function is your helper for that. The common pattern for using it is:

(defmutation use-person-as-form [{:keys [person-id]}]
  (action [{:keys [state]}]
    (swap! state (fn [s]
                   (-> s
                     (fs/add-form-config* Person [:person/by-id person-id]) ; this will get phone as well
                     (assoc-in [:component :person-editor :person-to-edit] [:person/by-id person-id]) ; hook it into an editor?
                     ...))))) ; other setup code
Important
add-form-config* will look at all of the form (and subforms reachable from that form), but it will only add config to the ones that are missing it. This means the current form state is not reset by this call.

15.3. Validation

Validation is completely customizable. There is built-in support for working with Clojure Spec as your validation layer. In order for this to be effective you should be sure to namespace all of your properties in a globally-unique way, and then simply write normal specs for them. The section on custom validators describes the other supported mechanism for validation.

The central function for using specs is fs/get-spec-validity, which can be used on an entire form or a single field. This function returns one of #{:valid, :invalid, :unchecked}. Initially, a form’s fields are marked as incomplete. When in this state the validity will always be :unchecked.

Some additional helpers are useful for concise UI code:

  • (invalid-spec? form field) - Field is optional. Returns true if the form (field) is complete and invalid.

  • (valid-spec? form field) - Field is optional. Returns true if the form (field) is complete and valid.

  • (checked? form field) - Field is optional. Returns true if the form (field) is complete. This function works no matter what validator you’re using.

  • (dirty? form field) - Field is optional. Returns true if the pristine copy of the form (field) doesn’t match the current entity.

(defsc PersonForm [this {:keys [person/name] :as props}]
   ...
   (dom/input { :value name :onBlur #(prim/transact! this `[(fs/mark-complete ...)]) ...})
   (when (fs/invalid-spec? props :person/name)
      (dom/span "Invalid username!")
   ...)

and the error message won’t show until the user has "blurred" out of that field, and then only if the field’s value does not match the spec for :person/name.

As you can see, the idea of "complete" is important to validation.

15.3.1. Completing Fields

Initially the form config will not consider any of the form fields to be complete. The idea of field "completion" is so that you can prevent validation on a field until you feel it is a good time. No one wants to see error messages about fields that they have yet to interact with!

However, depending on what you are editing, you may have different ideas about when fields should be considered complete. For example, if you just loaded a saved entity from a server, then all of the fields are probably complete by definition, meaning that you need a way to mark all fields (recursively) complete.

When you first add form config to an entity, all fields are "incomplete". You can iteratively mark fields complete as the user interacts with them, or trigger a completion "event" all at once. There is a support function and a mutation for this.

The mark-complete* is meant to be used from within mutations against the app state database. It requires the state map (not atom), the entity’s ident, and which field you want to mark complete. If you omit the field, then it marks everything (recursively) complete form that form down.

So, in our earlier example of loading a person for editing, we’d augment that mutation like so:

(defmutation use-person-as-form [{:keys [person-id]}]
  (action [{:keys [state]}]
    (swap! state (fn [s]
                   (-> s
                     (fs/add-form-config* Person [:person/by-id person-id]) ; this will get phone as well
                     (fs/mark-complete* [:person/by-id person-id]) ; MUST come after config is added!
                     (assoc-in [:component :person-editor :person-to-edit] [:person/by-id person-id]))))))

this will cause all validations to immediately apply in the UI.

The mark-complete! mutation can be used for the exact same purpose from the UI. Typically, it is used in things like :onBlur handlers to indicate that a field is ready for validation. It takes an :entity-ident and :field, but the :entity-ident is optional if the transaction is invoked on the form component of that field:

(defsc PersonForm [this {:keys [db/id person/name]}]
  ... ; as before
  (dom/input {:value name
              :onBlur #(prim/transact! this `[(fs/mark-complete! {:entity-ident [:person/by-id ~id]})])})
  ...)

The inverse operations are clear-complete! and clear-complete*.

15.3.2. Using non-spec Validators

You may not wish to use the longer names of properties that are required in order to get stable Clojure Spec support simply for form validation. In this case you’d still like to use the idea of field completion and validation, but you’ll want to supply the mechanism whereby validity is determined.

The form traversal code for validation is already in the form state code, and a helper function is provided so you can leverage it to create your own form validation system. It is quite simple:

  1. Write a function (fn [form field] …​) that returns true if the given field (a keyword) is valid on the given form (a map of the props for the form that contains that field).

  2. Generate a validator with fs/make-validator

The returned validator works identically to get-spec-validity, but it uses your custom function instead of specs to determine validity.

For example, you might want to make a simple new user form validation that looks something like this:

(defn new-user-form-valid [form field]
  (let [v (get form field)]
    (case field
      :username (and (string? v) (seq (str/trim v))) ; not empty
      :password (> (count v) 8)                      ; longer than 8
      :password-2 (= v (:password form)))))          ; passwords match

(def validator (fs/make-validator new-user-form-valid))

(defsc NewUser [this {:keys [username password password-2] :as props}]
   ...
   (dom/input {:name "password-2" :value password-2 :onBlur #(prim/transact! this `[(fs/mark-complete ...)]) ...})
   (when (= :invalid (validator props :password-2))
      (dom/span "Passwords do not match!")
   ...)

As before: you won’t see the error message on an invalid entry until your code has marked the field complete. This moves a decent amount of clutter out of the primary UI code and into the form support itself.

15.4. Submitting Data

Once your form is valid and the user indicates a desire to save your interest of course is to send that data to the server. The dirty-fields function should be used from the UI in order to calculate this and pass it as a parameter to a mutation. The mutation can then update the local pristine state of form config and indicate a remote operation.

The dirty-fields function returns a map from ident to fields that have changed. If the ident includes a temporary ID, then all fields for that form will be included. If the ID is not a temp id, then it will only include those fields that differ from the pristine copy of the original. This will include subform references as to-one or to-many idents (to indicate the addition or removal of subform instances).

You can ask dirty-fields to either send the explicit new values (only), or a before/after picture of each field. The latter is particularly useful for easily deriving the addition/removal of references, but is also quite useful if you would like to do optimistic concurrency (e.g. not apply a change to a server where the old value wasn’t still in the database).

(defmutation submit-person [{:keys [id]}]
  (action [{:keys [state]}]
    (swap! state fs/entity->pristine* [:person/by-id id])) ; updates form state to think the form is now in pristine shape
  (remote [env] true)) ; diff goes over the network as a parameter from the UI layer

(defsc Person [this props]
   ... ; as before
   (dom/button {:onClick #(prim/transact! this `[(submit-person ~{:id id :diff (fs/dirty-fields props true)})])} "Submit")
   ...)

If you’d like to wait until the server indicates everything is ok, then you can use ptransact! and returning to get back some submission information, and move the entity→pristine* step to a later mutation:

(defmutation submit-person [{:keys [id]}]
  (remote [{:keys [ast state]] (fulcro.client.mutations/returning ast @state SubmissionStatus))

(defmutation finish-person-submission [{:keys [id]}]
  (action [{:keys [state]}]
     (if (= :ok (get-in @state [:submission-status/by-id id :status]))
       (swap! state fs/entity->pristine* [:person/by-id id])
       ...)))

...

(ptransact! this `[(submit-person ~{:id id :diff (fs/dirty-fields props true)})
                   (finish-person-submission ~{:id id})])

See pessimistic transactions for more details.

15.5. Form State Demos

The following two fully-functional demos show you complete code for two scenarios.

15.5.1. Selecting an Entity for Edit

A very common use case is the scenario where the entities are already loaded and are displayed in the UI. The user clicks on an entry, and you take them to a form where they can edit the item.

This demo lists some phone numbers. Clicking on one:

  • Adds form configuration to the entity

  • Switches the UI to the form editor

  • Switches back to the (updated) list on save

Show/Hide Source
(ns book.forms.form-state-demo-1
  (:require [fulcro.ui.elements :as ele]
            [fulcro.client.mutations :as m :refer [defmutation]]
            [fulcro.ui.form-state :as fs]
            [fulcro.ui.bootstrap3 :as bs]
            [fulcro.client.primitives :as prim :refer [defui defsc]]
            [fulcro.client.dom :as dom]
            [clojure.spec.alpha :as s]
            [garden.core :as g]))

(declare Root PhoneForm)

(defn render-field
  "A helper function for rendering just the fields."
  [component field renderer]
  (let [form         (prim/props component)
        entity-ident (prim/get-ident component form)
        id           (str (first entity-ident) "-" (second entity-ident))
        is-dirty?    (fs/dirty? form field)
        clean?       (not is-dirty?)
        validity     (fs/get-spec-validity form field)
        is-invalid?  (= :invalid validity)
        value        (get form field "")]
    (renderer {:dirty?   is-dirty?
               :ident    entity-ident
               :id       id
               :clean?   clean?
               :validity validity
               :invalid? is-invalid?
               :value    value})))

(defn input-with-label
  "A non-library helper function, written by you to help lay out your form."
  ([component field field-label validation-string input-element]
   (render-field component field
     (fn [{:keys [invalid? id dirty?]}]
       (bs/labeled-input {:error           (when invalid? validation-string)
                          :id              id
                          :warning         (when dirty? "(unsaved)")
                          :input-generator input-element} field-label))))
  ([component field field-label validation-string]
   (render-field component field
     (fn [{:keys [invalid? id dirty? value invalid ident]}]
       (bs/labeled-input {:value    value
                          :id       id
                          :error    (when invalid? validation-string)
                          :warning  (when dirty? "(unsaved)")
                          :onBlur   #(prim/transact! component `[(fs/mark-complete! {:entity-ident ~ident
                                                                                     :field        ~field})])
                          :onChange #(m/set-string! component field :event %)} field-label)))))

(s/def ::phone-number #(re-matches #"\(?[0-9]{3}[-.)]? *[0-9]{3}-?[0-9]{4}" %))

(defmutation abort-phone-edit [{:keys [id]}]
  (action [{:keys [state]}]
    (swap! state (fn [s]
                   (-> s
                     ; stop editing
                     (dissoc :root/phone)
                     ; revert to the pristine state
                     (fs/pristine->entity* [:phone/by-id id])))))
  (refresh [env] [:root/phone]))

(defmutation submit-phone [{:keys [id delta]}]
  (action [{:keys [state]}]
    (js/console.log delta)
    (swap! state (fn [s]
                   (-> s
                     ; stop editing
                     (dissoc :root/phone)
                     ; update the pristine state
                     (fs/entity->pristine* [:phone/by-id id])))))
  (remote [env] true)
  (refresh [env] [:root/phone [:phone/by-id id]]))

(defsc PhoneForm [this {:keys [:db/id ::phone-type root/dropdown] :as props}]
  {:query       [:db/id ::phone-type ::phone-number
                 {[:root/dropdown '_] (prim/get-query bs/Dropdown)} ;reusable dropdown
                 fs/form-config-join]
   :form-fields #{::phone-number ::phone-type}
   :ident       [:phone/by-id :db/id]}
  (dom/div :.form
    (input-with-label this ::phone-number "Phone:" "10-digit phone number is required.")
    (input-with-label this ::phone-type "Type:" ""
      (fn [attrs]
        (bs/ui-dropdown dropdown
          :value phone-type
          :onSelect (fn [v]
                      (m/set-value! this ::phone-type v)))))
    (bs/button {:onClick #(prim/transact! this `[(abort-phone-edit {:id ~id})])} "Cancel")
    (bs/button {:disabled (or (not (fs/checked? props)) (fs/invalid-spec? props))
                :onClick  #(prim/transact! this `[(submit-phone {:id ~id :delta ~(fs/dirty-fields props true)})])} "Commit!")))

(def ui-phone-form (prim/factory PhoneForm {:keyfn :db/id}))

(defsc PhoneNumber [this {:keys [:db/id ::phone-type ::phone-number]} {:keys [onSelect]}]
  {:query         [:db/id ::phone-number ::phone-type]
   :initial-state {:db/id :param/id ::phone-number :param/number ::phone-type :param/type}
   :ident         [:phone/by-id :db/id]}
  (dom/li
    (dom/a {:onClick (fn [] (onSelect id))}
      (str phone-number " (" (phone-type {:home "Home" :work "Work" nil "Unknown"}) ")"))))

(def ui-phone-number (prim/factory PhoneNumber {:keyfn :db/id}))

(defsc PhoneBook [this {:keys [:db/id ::phone-numbers]} {:keys [onSelect]}]
  {:query         [:db/id {::phone-numbers (prim/get-query PhoneNumber)}]
   :initial-state {:db/id          :main-phone-book
                   ::phone-numbers [{:id 1 :number "541-555-1212" :type :home}
                                    {:id 2 :number "541-555-5533" :type :work}]}
   :ident         [:phonebook/by-id :db/id]}
  (dom/div
    (dom/h4 "Phone Book (click a number to edit)")
    (dom/ul
      (map (fn [n] (ui-phone-number (prim/computed n {:onSelect onSelect}))) phone-numbers))))

(def ui-phone-book (prim/factory PhoneBook {:keyfn :db/id}))

(defmutation edit-phone-number [{:keys [id]}]
  (action [{:keys [state]}]
    (let [phone-type (get-in @state [:phone/by-id id ::phone-type])]
      (swap! state (fn [s]
                     (-> s
                       ; make sure the form config is with the entity
                       (fs/add-form-config* PhoneForm [:phone/by-id id])
                       ; since we're editing an existing thing, we should start it out complete (validations apply)
                       (fs/mark-complete* [:phone/by-id id])
                       (bs/set-dropdown-item-active* :phone-type phone-type)
                       ; tell the root UI that we're editing a phone number by linking it in
                       (assoc :root/phone [:phone/by-id id])))))))

(defsc Root [this {:keys [:root/phone :root/phonebook]}]
  {:query         [{:root/dropdown (prim/get-query bs/Dropdown)}
                   {:root/phonebook (prim/get-query PhoneBook)}
                   {:root/phone (prim/get-query PhoneForm)}]
   :initial-state (fn [params]
                    {:root/dropdown  (bs/dropdown :phone-type "Type" [(bs/dropdown-item :work "Work")
                                                                      (bs/dropdown-item :home "Home")])
                     :root/phonebook (prim/get-initial-state PhoneBook {})})}
  (ele/ui-iframe {:frameBorder 0 :width 500 :height 200}
    (dom/div
      (dom/link {:rel "stylesheet" :href "bootstrap-3.3.7/css/bootstrap.min.css"})
      (if (contains? phone ::phone-number)
        (ui-phone-form phone)
        (ui-phone-book (prim/computed phonebook {:onSelect (fn [id] (prim/transact! this `[(edit-phone-number {:id ~id})]))}))))))

15.5.2. Loading or Creating Something New

This example shows the case where a graph of entities (a person and multiple phone numbers) are to be created in a UI, or are to be loaded from a server. This is a full-stack example, though it doesn’t actually persist the data (it just prints what the server receives in the Javascript console).

There are two buttons. One will load an existing entity into the editor, and of course submissions will send a minimal delta. The other button will create a new person, and submissions will send all fields.

The load case, as you can see in the code, is very similar to the prior example, but just includes some extra code to show you how to put it together with a load interaction.

Show/Hide Source
(ns book.forms.form-state-demo-2
  (:require [devcards.core]
            [fulcro.ui.elements :as ele]
            [fulcro.server :as server]
            [fulcro.client.mutations :as m :refer [defmutation]]
            [fulcro.ui.bootstrap3 :as bs]
            [fulcro.client.primitives :as prim :refer [defsc]]
            [fulcro.client.dom :as dom]
            [fulcro.ui.form-state :as fs]
            [clojure.string :as str]
            [cljs.spec.alpha :as s]
            [fulcro.client.data-fetch :as df]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Server Code
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

; a simple query for any person, that will return valid-looking data
(server/defquery-entity :person/by-id
  (value [env id params]
    {:db/id          id
     ::person-name   (str "User " id)
     ::person-age    56
     ::phone-numbers [{:db/id 1 ::phone-number "555-111-1212" ::phone-type :work}
                      {:db/id 2 ::phone-number "555-333-4444" ::phone-type :home}]}))

(defonce id (atom 1000))
(defn next-id [] (swap! id inc))

; Server submission...just prints delta for demo, and remaps tempids (forms with tempids are always considered dirty)
(server/defmutation submit-person [params]
  (action [env]
    (js/console.log "Server received form submission with content: ")
    (cljs.pprint/pprint params)
    (let [ids    (map (fn [[k v]] (second k)) (:diff params))
          remaps (into {} (keep (fn [v] (when (prim/tempid? v) [v (next-id)])) ids))]
      {:tempids remaps})))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Client Code
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(s/def ::person-name (s/and string? #(seq (str/trim %))))
(s/def ::person-age #(s/int-in-range? 1 120 %))

(defn render-field [component field renderer]
  (let [form         (prim/props component)
        entity-ident (prim/get-ident component form)
        id           (str (first entity-ident) "-" (second entity-ident))
        is-dirty?    (fs/dirty? form field)
        clean?       (not is-dirty?)
        validity     (fs/get-spec-validity form field)
        is-invalid?  (= :invalid validity)
        value        (get form field "")]
    (renderer {:dirty?   is-dirty?
               :ident    entity-ident
               :id       id
               :clean?   clean?
               :validity validity
               :invalid? is-invalid?
               :value    value})))

(def integer-fields #{::person-age})

(defn input-with-label
  "A non-library helper function, written by you to help lay out your form."
  ([component field field-label validation-string input-element]
   (render-field component field
     (fn [{:keys [invalid? id dirty?]}]
       (bs/labeled-input {:error           (when invalid? validation-string)
                          :id              id
                          :warning         (when dirty? "(unsaved)")
                          :input-generator input-element} field-label))))
  ([component field field-label validation-string]
   (render-field component field
     (fn [{:keys [invalid? id dirty? value invalid ident]}]
       (bs/labeled-input {:value    value
                          :id       id
                          :error    (when invalid? validation-string)
                          :warning  (when dirty? "(unsaved)")
                          :onBlur   #(prim/transact! component `[(fs/mark-complete! {:entity-ident ~ident
                                                                                     :field        ~field})
                                                                 :root/person])
                          :onChange (if (integer-fields field)
                                      #(m/set-integer! component field :event %)
                                      #(m/set-string! component field :event %))} field-label)))))

(s/def ::phone-number #(re-matches #"\(?[0-9]{3}[-.)]? *[0-9]{3}-?[0-9]{4}" %))

(defsc PhoneForm [this {:keys [::phone-type ui/dropdown] :as props}]
  {:query       [:db/id ::phone-number ::phone-type
                 {:ui/dropdown (prim/get-query bs/Dropdown)}
                 fs/form-config-join]
   :form-fields #{::phone-number ::phone-type}
   :ident       [:phone/by-id :db/id]}
  (dom/div :.form
    (input-with-label this ::phone-number "Phone:" "10-digit phone number is required.")
    (input-with-label this ::phone-type "Type:" ""
      (fn [attrs]
        (bs/ui-dropdown dropdown
          :value phone-type
          :onSelect (fn [v]
                      (m/set-value! this ::phone-type v)
                      (prim/transact! this `[(fs/mark-complete! {:field ::phone-type})
                                             :root/person])))))))

(def ui-phone-form (prim/factory PhoneForm {:keyfn :db/id}))

(defn add-phone-dropdown*
  "Add a phone type dropdown to a phone entity"
  [state-map phone-id default-type]
  (let [dropdown-id (random-uuid)
        dropdown    (bs/dropdown dropdown-id "Type" [(bs/dropdown-item :work "Work") (bs/dropdown-item :home "Home")])]
    (-> state-map
      (prim/merge-component bs/Dropdown dropdown)           ; we're being a bit wasteful here and adding a new dropdown to state every time
      (bs/set-dropdown-item-active* dropdown-id default-type)
      (assoc-in [:phone/by-id phone-id :ui/dropdown] (bs/dropdown-ident dropdown-id)))))

(defn add-phone*
  "Add the given phone info to a person."
  [state-map phone-id person-id type number]
  (let [phone-ident      [:phone/by-id phone-id]
        new-phone-entity {:db/id phone-id ::phone-type type ::phone-number number}]
    (-> state-map
      (update-in [:person/by-id person-id ::phone-numbers] (fnil conj []) phone-ident)
      (assoc-in phone-ident new-phone-entity)
      (add-phone-dropdown* phone-id type))))

(defmutation add-phone
  "Mutation: Add a phone number to a person, and initialize it as a working form."
  [{:keys [person-id]}]
  (action [{:keys [state]}]
    (let [phone-id (prim/tempid)]
      (swap! state (fn [s]
                     (-> s
                       (add-phone* phone-id person-id :home "")
                       (fs/add-form-config* PhoneForm [:phone/by-id phone-id])))))))

(defsc PersonForm [this {:keys [:db/id ::phone-numbers]}]
  {:query       [:db/id ::person-name ::person-age
                 {::phone-numbers (prim/get-query PhoneForm)}
                 fs/form-config-join]
   :form-fields #{::person-name ::person-age ::phone-numbers} ; ::phone-numbers here becomes a subform because it is a join in the query.
   :ident       [:person/by-id :db/id]}
  (dom/div :.form
    (input-with-label this