1. About This Book

This is a stand-alone developer’s guide for version 3 of Fulcro. It is intended to be 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, the docstrings/clojure docs, and even a series of YouTube videos,

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.

Of course fixes to this guide are also appreciated.

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

Example 1. Sample Example

(ns book.example-1
  (:require
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.mutations :refer [defmutation]]
    [com.fulcrologic.fulcro.dom :as dom]))

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

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

Each example includes a "Focus Inspector" button that will cause the Fulcro Inspect development tools to focus on that example. See Install Fulcro Inspect for details on how to set that up and use it.

All of the full stack examples use a mock server embedded in the browser to simulate any network 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 repository then you won’t see the live code! Use http://book.fulcrologic.com/fulcro3 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. 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 [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
            [com.fulcrologic.fulcro.application :as app]
            [com.fulcrologic.fulcro.data-fetch :as df]
            [com.fulcrologic.fulcro.dom :as dom]
            [com.fulcrologic.fulcro.algorithms.lookup :as ah]
            [com.fulcrologic.fulcro.algorithms.merge :as merge]
            [com.fulcrologic.fulcro.algorithms.data-targeting :as targeting]
            [com.fulcrologic.fulcro.algorithms.denormalize :as fdn]
            [com.fulcrologic.fulcro.algorithms.normalize :as fnorm]
            [com.fulcrologic.fulcro.algorithms.react-interop :as interop]
            [com.fulcrologic.fulcro.algorithms.tempid :as tempid]
            [com.fulcrologic.fulcro.algorithms.form-state :as fs]
            [com.fulcrologic.fulcro.networking.http-remote :refer [fulcro-http-remote]]
            [com.fulcrologic.fulcro.ui-state-machines :as uism]
            [com.fulcrologic.fulcro.routing.dynamic-router :as dr]
            [com.fulcrologic.fulcro.routing.legacy-ui-routers :as r]
            [com.fulcrologic.fulcro.mutations :as m :refer [defmutation]]))

others will be identified as they are used.

2. Fulcro From 10,000 Feet

You may be reading this guide hoping for a "quick feel" for why Fulcro might be a good fit for a project. The Getting Started chapter walks you through the evolution of the model in a way that is intended to lead to the best understanding, but it is a lot of minutiae to absorb just to get the "big picture" and there is some chance you’ll misunderstand where that is going and stop before you get to the good stuff. This book is also a rather large work intended to serve as a complete reference guide, so reading it cover to cover may not be all that desirable for your purposes.

If you really want to learn to use Fulcro, feel free to skip this chapter and read Getting Started; however, if you’d rather learn the central ideas and approach of Fulcro this is a good place to start.

At its core Fulcro and its companion library pathom are a full-stack application programming system centered around graph-based UI and data management.

The core ideas are as follows:

  • Graphs and Graph Queries are a great way to generalize data models.

  • UI trees are directed graphs that can easily be "fed" from graph queries.

  • User-driven operations are modeled at transactions whose values are simply data (that look like calls).

  • Arbitrary graphs of data from the server need to be normalized (as in database normalization):

    • UI trees often repeat parts of the graph.

    • Local Manipulation of data obtained from a graph needs to be de-duped.

  • Composition is King. Seamless composition is a key component of software sustainability.

2.1. But Does it Have X?

People have a number of initial "comparative" questions when looking at Fulcro when they’ve used other libraries. If you are familiar with other CLJS or JS UI libraries then you may have one of these questions. The most common ones are:

Can I use "hiccup" notation for the DOM?

Yes, you can use Sablono. It should work fine, but it is cljs-only. If you want support, isomorphic server-side rendering of components, etc.: then use what Fulcro provides.

Why don’t you use Hiccup by default?

The honest answer is "because". As in: because that’s the way it was written in Om Next, the predecessor of Fulcro. I assume David Nolen had his reasons for preferring functions/macros over hiccup and I respected his choice (even though I did not personally know why it was). Why is Fulcro still using functions? Well, several reasons: Certainly the fact that I already have the code and there are apps based on it is sufficient, it is no more verbose (if you leverage :refer in :require), it tests to be as fast as raw React when used optimally, and works with the same API on the client and server for supporting isomorphic rendering (Hiccup is CLJ only, you have to use Sablono for cljs with React). Finally, I will note that this question ranks near the top of my list of "Questions that Don’t Matter". If you came to Fulcro looking for DOM notation: move along.

Where are the "reducers"?

Re-frame and Redux are very popular choices for front-end work, and for good reason. Fulcro is basically a graph-centric full-stack CQRS-style library. Mutations are the "commands" that are queued (like events in a reducer system), but Fulcro adds a some very important things: graph data support, auto-normalization of central database, and a centralized transaction processing system.

Fulcro 3’s UI State Machines are usable in exactly the same way as reducer/event systems, but have some distinct differences (dare I say improvements?): They support an actor model where instances of UI components are assigned at runtime to play "roles" in the logical flow; they are true state machines (not just switch/case statements); they use of Fulcro’s database auto-normalization, graph query abilities, and abstract mutation system; and their current "state" is stored and normalized into the same central application database where Fulcro’s Chrome extension makes them easy to debug.

Can I Subscribe to XYZ?

Subscriptions are not a central concern of Fulcro itself, but Fulcro’s architecture mixed with the websocket remote make building something that has a "Meteor JS" experience pretty easy to do. Almost all of the real difficulty in making a subscription system has to do with server-side management. You could use a "custom" websocket remote to adapt to any existing GraphQL infrastructure that supplies such automatic updates.

What about Server-Side Rendering?

Fulcro is written in CLJC. The intention is to be able to set up and "run" an application in a headless JS OR Java VM environment and either actively control a DOM or just render "frames" as strings. The server-side implementation of the DOM methods let you render Fulcro UI in a Java VM without needing to resort to a JS engine at all.

2.2. Server Schema vs. UI Graph

Fulcro makes no assumptions nor does it have any requirements about back-end databases. You don’t even have to have a back-end: You can easily simulate one with the browser (like this book does), wrap browser local storage, or simply not use one at all.

If you are using a server database, then normalization and schema on the server is done according to whatever you’ve designed. Pathom makes it easy to "reshape" any particular schema or storage technology into a graph that fits your external API and UI needs.

This is an important point: it does not matter how your back-end database structures the data, as long as you know how to get the data you need when given a "context" that makes sense.

2.3. Queries (EQL)

Fulcro uses EQL (EDN Query Language), a graph query language similar to Datomic’s pull query notation.

A query is a vector (with nesting) that lists the things you want:

[:person/name {:person/address [:address/street]}]

The above query (when run against a person) asks for that person’s name, and then indicates a "join" by using a map whose key is the join property on person, and whose value is a subquery of things to pull from there.

Such a query returns a map that mirrors the query structure:

{:person/name "Joe"
 :person/address {:address/street "111 Main St."}}

and could potentially return to-many results (if the address field were to-many in the database of the server):

{:person/name "Joe"
 :person/address [{:address/street "111 Main St."} {:address/street "345 Center Ave."}]}

For those coming from SQL this roughly equates to a LEFT JOIN from person to address (where the behind-the-scenes join key might be converted from :person/address to something like address.person_id). You could implement this with something crude like:

SELECT person.id, person.name, address.id, address.street FROM person, address WHERE address.id = person.address_id

and walk the table result with a reduce to convert the rows into the graph, but fortunately, the Pathom library does most of that "lifting" for you and also includes features that can be used to tune for optimal performance (e.g. fixing n+1 query problems, branch caching, parallel operation, etc.).

2.4. Fulcro Client Database

Fulcro uses a single, central, normalized graph database. Any time data is created or read into a Fulcro application it can be auto-normalized into this database, and queries from the UI components can easily be fulfilled from it.

The format of this database is trivial (and very fast as a result):

  • Nodes of the graph have some distinct ID (for their type, at least)

  • Nodes are stored in "tables".

  • Edges are represented as vectors: [TABLE ID]

We use the term *ident* to mean "A tuple of table and ID that uniquely identify a graph node in the database."

The graph of data like this:

{:person/id 1 :person/name "Joe" :person/address {:address/id 42 :address/street "111 Main St."}}

can be turned into this:

;; TABLE     ID   ENTITY                                           POINTER TO ADDRESS TABLE
{ :PERSON  { 1    {:person/id 1 :person/name "Joe" :person/address [:ADDRESS 42]}}
  :ADDRESS { 42   {:address/id 42 :address/street "111 Main St."}}}

This format supports any arbitrary graph, to-many (a vector of idents) and to-one (single ident) relations, any possible property value type (though you should generally make sure they’re serializable for network transfer), and is simple to manipulate (i.e. an assoc-in with a path length of 2 or 3 gets you to anything).

This leads to the following useful results:

  • Everything is de-duped for you. No need to worry that composing in some "alternate view" of pre-existing data will cause two out of sync copies to exist.

  • Caching is a centralized concern. Your normalized client database is the only possible place for the data, and each node of the graph has exactly one place to live (even if a query has two views of of the same thing on different branches of some complex graph query result, which is common when composing related components onto a UI).

  • Subgraph reasoning is trivial: You can (re)load any portion of the graph at any time.

    • A UI query, server load, or transfer of some portion of the graph can be started from any node.

    • Manipulation of some node affects all paths through that node.

  • Meteor-style subscription models need not be an internal part of the library. If you have a database that can tell you when data changes then you can either re-run your graph query to pull those changes or merge in new graph of data that arrives from some arbitrary websocket push.

You may be asking "by what magic can Fulcro do this?". The answer is simple: by co-locating the node/edge descriptions with the components of your UI!

2.5. The Glue: UI Components

We also are writing SPAs where what we care about is the UI we end up with. Pathom can shape our server graph to match whatever we need on the client, and this frees us to design our UI as we see fit.

Components can therefore represent the desired UI nodes of a graph, and the EQL joins can represent the edges. Auto-normalization just requires one more detail: a way to manufacture the correct ident for a given node in order to make the edge.

This is functional programming, so of course the answer is a function:

(defn person-ident [props] [:person/id (:person/id props)])

The convention for our table names (for ease of use with Pathom) is to use the name of the ID field of the entity (e.g. :person/id) as the table name for those types of entities.

Thus, the "ident" of this person:

(person-ident {:person/id 22 ...})
; => [:person/id 22]

Of course, this function needs to be associated with the Person component for Fulcro to know how to use it. That looks like this:

(defsc Person [this props]
 {:query [:person/id :person/name]  ; (1)
  :ident (fn [] [:person/id (:person/id props)])} ; (2)
 (dom/div                 ; (3)
   (:person/name props)))
  1. The co-located query: What should we ask the server for when we’re asking for person data?

  2. The ident: Given the props for some particular person, what is the 2-tuple that describes the table and ID for it?

  3. What does this component look like when rendered?

2.5.1. Putting Things in the Database

Of course, the most interesting aspect of the operation now is "how do I get data into the system?".

Well, the query and normalization constructs are on the component, so you simply use the component with various APIs.

For example, to put some arbitrary graph of data you generated by hand into the database you could use:

(merge/merge-component! app Person {:person/id 11 ...})

which inserts this:

{:person/id {11 {:person/id 11 ...}}}

To put that same data into the database and add an "edge" somewhere else in the graph you could do:

(merge/merge-component! app Person {:person/id 11 ...} :append [:list/id :friends :list/people])

which would add this to the graph:

;;                  append implies to-many. i.e. a vector of idents
{:list/id {:friends {:list/people [... [:person/id 11]] :list/id :friends ...}}
 :person/id {11 {:person/id 11 ...}}}
Loading

Loading is just as trivial and uses the same mechanisms. Loading a particular person from the server might look like this:

(df/load app [:person/id 22] Person)
Nested Things

Of course components are typically nested, and all of this composes with elegance. Say your person had some addresses that you modelled on a sub-component. If you compose those together everything else just falls into place:

(defsc Address [this props]
  {:query [:address/id :address/street]
   :ident :address/id} ; shorthand notation for the most common case: the table name and id key are the same keyword
  ...)

(defsc Person [this props]
  {:query [:person/id :person/name {:person/address (comp/get-query Address)}]
   :ident :person/id}
  ...)

Then using load or merge against such a component

(merge-component! app Person {:person/id 1 :person/address {:address/id 42 ...}})

will merge and normalize the entire (sub)graph.

2.6. The Operational Model

During operation you will need to make things happen.

2.6.1. Mutations

Mutations are a first-class higher-order citizen of Fulcro, and encode all of the possible interactions that your UI might need to have with the local graph database and any remote servers. Mutations are similar to specific "handler cases" of reducers in Redux, but are more akin to the "request" of a CQRS system in that they are serialized into a network-compatible form so that a single operation can be abstractly communicated to both the local logic and remote server(s) without having to deal with the low-level details.

The UI-layer need know nothing about these details because it simply submits transactions that look like this:

(comp/transact! this [(add-person {:name name})])

add-person in this case is not a function, but instead a mutation. If you call add-person as a function, it simply returns the expression as namespace-resolved CLJ(S) data (a list containing a fully-qualified symbol and the (evaluated) parameters):

user=> (def name "Tony")
user=> (add-person {:name name})
;;=> (ns.of.mutation/add-person {:name "Tony"})

This is an important point. You can also write these with Clojure quoting (which makes it even more obvious that you’re not calling something in the UI):

(comp/transact! this `[(add-person ~{:name name})])

The core point is that the UI submits an abstract bit of data to Fulcro’s transaction system. The UI doesn’t usually directly manipulate anything to do with the database, networking, or any other async APIs.

Mutations are written as separate code units using defmutation, which has a notation somewhere between defn and defrecord with a protocol:

(defmutation add-person [params]
  (action [env] ...)
  (remote [env] ...)
  (rest [env] ...)
  (ok-action [env] ...)
  (error-action [env] ...))

where each "section" represents how that particular mutation interacts with the world around it. The action section describes what happens locally and optimistically to the client database before any networking. This is the main place where you manipulate the graph database (via swap!/merge/etc. on it).

The remote sections (which is any section not ending in -action) describe how that mutation should interact with some particular server (you name them, and remote is the default name). Remotes are defined at a top-level of the application so that networking code is not mixed into the mutations at all. Remotes usually just return true to indicate that the mutation should be forwarded on to the server it represents (unchanged), but they can also make decisions based on state and even "rewrite" the transaction that will flow over the network.

Other *-action sections have to do with handling network results as they occur.

Thus a simple UI call can turn into a combination of optimistic actions, remote interactions, etc.

Mutations are allowed to submit new transactions themselves, so sections like ok-action can check results of remote interactions and then decide what to do, if anything, next.

Mutations, as in GraphQL, can return graph results. Fulcro, of course, can auto-merge those into the client database.

2.6.2. UI Refresh

Fulcro manages UI updates by targeting refreshes according to what actually changes (the rendering optimizations are even pluggable in Fulcro 3), and it turns out the the normalized database makes this all very fast to calculate.

Here’s a quick overview of how (one method) of these optimizations work:

  • Each component has a shouldComponentUpdate that checks to see if the props have actually changed. If a component is asked to "double render" or render without changed props, then it will short-circuit (e.g. it is a PureComponent). Checking this fact in CLJS is very very fast, since we use immutable data structures where reference comparisons are enough to ensure two things are identical.

  • Fulcro keeps indexes of mounted components by their idents. Thus, at any given time it knows exactly which on-screen components "represent" something like [:person/id 1]. It keeps these up to date via hooks on React’s lifecycle.

  • At certain points in processing (e.g. after any optimistic action in a mutation or the arrival of a network result) a refresh is scheduled via js/requestAnimationFrame.

  • AFTER every render, Fulcro records a snapshot of what the state database looked like. This is cheap because of immutable data and structural sharing (e.g. ns time-scale…​copy a single 64-bit reference).

  • The ident-optimized render does the following:

  • It reads all of the "mounted component" idents from its internal indexes. This is the list of "data that matters" to things on the screen.

  • If finds the "component to refresh" by checking to see if the table entries at these idents are identical (reference compare) to the last rendered version. It can then translate a "change table entry" into a set of components using the indexes. Typically only 1-2 components will be found. This stage typically takes under a millisecond.

  • For all components that need a refresh: It runs those component’s queries to get new props and "tunnels" those new props to the components (via a hidden property in component-local state (e.g. React setState)).

So, a form with 50 fields on-screen will do roughly 50 64-bit compares (to find stale data), run 1-2 queries against the state database targeted at the affected components, and call React setState a couple of times.

The alternate renderer (keyframe) just does a root refresh every time and relies on shouldComponentUpdate. It is actually quite fast as well, since the query engine in Fulcro is pretty highly tuned to be fast, and CLJS implementations of shouldComponentUpdate are equally impressive.

Fulcro 2 and earlier leveraged "follow-on reads". A concept invented by Om Next where you named the data that changed during a mutation (e.g. :person/name). It would then leverage indexes to find which components queried that property, then look up which on-screen elements had that component type. It’s still a potential "alternate" optimization strategy, but it suffered from a few problems:

  1. It was a manual process, so it was easy to forget to "tell it" what changed.

  2. It was harder to understand (fully automatic is hard to beat), especially to new users.

All of the required indexes still exist, so it would not be hard to "replicate" this style of refresh optimization. Another possible future expansion would be to re-purpose "follow-on reads" as "refresh only this" (tell the underlying scanner not to scan anything else). This could be useful when doing rapid UI interactions where you want to eliminate as much processing as possible in-between frames.

2.7. Additional Items of Interest

The prior sections describe the core points of Fulcro’s architecture. Data flows in and out via graph queries and mutations that can easily manipulate a local normalized graph database in a unified way. Remote interactions are managed by mutation declarations and all side-effecting and asynchrony is isolated from the UI layer by a CQRS-style transactional semantic. Fulcro leverages the idents, immutable data structures, co-located queries, and other tricks to optimize refresh.

Here are some additional interesting things that fall out of this model "for free":

  • Initial application state can also be co-located as a superb optimization for "getting things going".

  • The queries can be used to "traverse" the declarative UI graph. Some places where this has been used to good effect:

    • Error checking: The defsc macro leverages to co-located query to check things like you props destructuring and initial state to warn you when you’ve got a typo or have forgotten something.

    • Nested forms can "discover" children using the query and leverage that to calculate diffs for server interactions.

    • UI screen routers can discover nested routers allowing UI sub-module composition for things like HTML 5 routing without explicit declaration.

  • Debugging is a relatively simple matter of looking at a component’s current state, which is always in a well-defined location (and easily visible in Fulcro Inspect, a Chrome extension).

  • History traversal is trivial. All application state is just a single immutable map. Saving snapshots over time is all it takes to be able to "view" the app as it existed at some prior point in time.

  • Snapshotting an application state for re-use during development. Fulcro Inspect can take a picture of app state and save it in browser local storage for later restoration. This allows you to "jump to" some application state that you use a lot during development (e.g. jump to screen 2, with a partially filled form read for submit).

Finally, a more recent addition to the library has proven to be extremely useful: UI State Machines.

Most UIs end up with a mess of logic spread out in ways that are hard to reason about. Look no further than any "login" module of an application. Are you logged in? Are we checking the server to see if our session is valid? Should I show a "wait" indicator?

We’ve known for years that state machines are a great way to manage this kind of complexity, but they are traditionally a bit of work to integrate into any given "UI library".

Fulcro’s central normalized database and mutation model made an internal version of state machines a very good match. Not only is it easy to define them, the model makes them reusable: that is to say you can indicate which UI components serve roles within the machine, leading to things like CRUD state machines can manage the interactions (edit, list, save, reset, cancel, etc.) for most of the entities in a form system.

3. 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.

Important
This document assumes you’re working with Fulcro 3.x and above. Please see older version of the Developer’s Guide if you’re working with an older version.

3.1. Install Fulcro Inspect

You should use Chrome for development because we have a developer tool called Fulcro Inspect that is available from the Chrome store for free. There are also devtools that are installed via preloads which will autoformat EDN that is sent via console.log. When properly configured, these tools will let you view the internal state of your Fulcro application, the local and remote transaction history, save "snapshots" of state so you can quickly retry a UI flow by hitting a "reset" button, "scroll" over state history and watch the UI change, run queries (with autocomplete if you’re using Pathom) against your EQL server, and even (via binaryage devtools) source-level debug your Fulcro application in CLJS with stack frame analysis and proper clojure names for things! These tools will save you countless hours of frustration.

Fulcro Inspect is a free extension for Chrome, and Binaryage devtools is a library that can be injected via a preload (which shadow-cljs does for you if it is in your dependencies).

All of the examples in the book have a "Focus Inspector" button. If you open Chrome devtools, choose the Fulcro Inspect tab, and then press that button you will focus the inspector on that example so you can see its database. The first example in this book looks like this when you do that:

Example 2. Using Inspect on the Book
inspect open

3.2. Configure Chrome Development Settings

You should open the Chrome developer tools (e.g. console and such). Edit the developer tool settings and change:

  • Under "Console": "Enable Custom Formatters"

  • Under "Network": "Disable Cache (while devtools is open)"

and always keep devtools open when you’re working on your apps. This ensures you’ll not be confused by caching issues and will be able to see clojure data structures as Clojure, and not low-level JS objects.

3.3. Install Supporting Tools

You’ll normally want to build a real application based on the Fulcro template. It contains a lot of boilerplate on things like server configuration, CSRF, testing, better error message formatting, and so on. This can save you quite a bit of setup and development time.

For this chapter we’re going to start from literally nothing. These instructions should work with any UNIX-like (e.g. OSX or Linux) system. I do not use Windows, so your mileage may vary if you’re on that platform.

  • Install a Java SE Development Kit (JDK). You’ll have an easier time if you use an older version: 8. OpenJDK or the official one is fine.

  • Install Clojure CLI Tools

  • Install Node and npm: The shadow-cljs compiler uses node for all js dependencies.

  • Optional, but recommended: Install IntelliJ CE and the Cursive plugin. There are free versions of both for non-commercial use. Any programming editor will do, but if you’re doing anything large I recommend this (or Emacs/Spacemacs if (and only if) you already use it).

The following commands should work from your command line (the $ is the command prompt):

$ clj
Clojure 1.10.0
user=>

(hit CTRL-D or CTRL-C to exit)

$ java -version
java version "1.8.0_162"
Java(TM) SE Runtime Environment (build 1.8.0_162-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.162-b12, mixed mode)

your version may be different, but it should be 1.8.x.

$ npm list
/your/directory
└── (empty)

If any of these fail, diagnose your installation of those tools before continuing.

3.4. Create Your Project

Create a directory and set up a basic node project:

$ mkdir app
$ cd app
$ mkdir -p src/main src/dev resources/public
$ npm init
... answer the questions or just take defaults ...
$ npm install shadow-cljs react react-dom --save

3.4.1. Clojure Dependencies

Create a deps.edn file with this content:

{:paths   ["src/main" "resources"]
 :deps    {org.clojure/clojure    {:mvn/version "1.10.1"}
           com.fulcrologic/fulcro {:mvn/version "3.0.0-alpha-18"}}

 :aliases {:dev {:extra-paths ["src/dev"]
                 :extra-deps  {org.clojure/clojurescript   {:mvn/version "1.10.520"}
                               thheller/shadow-cljs        {:mvn/version "2.8.40"}
                               binaryage/devtools          {:mvn/version "0.9.10"}}}}}

3.4.2. Shadow-cljs Compiler Configuration

And a shadow-cljs.edn file that looks like this:

{:deps     {:aliases [:dev]}
 :dev-http {8000 "classpath:public"}
 :builds   {:main {:target     :browser
                   :output-dir "resources/public/js/main"
                   :asset-path "/js/main"
                   :modules    {:main {:init-fn app.client/init
                                       :entries [app.client]}}
                   :devtools   {:after-load app.client/refresh
                                :preloads   [com.fulcrologic.fulcro.inspect.preload]}}}}

The init-fn and after-load options give you a place to put code that will set up an application on load, and trigger UI refreshes on hot code reload. We’ll put those functions in the source in a moment.

The top-level dev-http server will cause shadow to start a dev web server on port 8080 that serves the files from our resources/public directory (resources is on our classpath via :paths in deps.edn).

The :modules section configures what code gets pulled into a given build. Every build has at least one module, and shadow will follow requires in the code so you typically only need your entry-point namespace in :entries.

See the Shadow-cljs User’s Guide for more information.

3.4.3. HTML File

In resources/public/index.html add this content:

<html>
 <meta charset="utf-8">
  <body>
    <div id="app"></div>
    <script src="/js/main/main.js"></script>
  </body>
</html>

We really only need to load our one (generated) JS file and supply a div with an ID. Our React application will be rendered on that div. You may, of course, include other content around that: CSS, other divs, etc.

3.4.4. Application Source

Our base source path is src/main for production code, and we want a namespace within the app package. Our directory structure must match this in CLJ(S), so we create src/main/app/client.cljs:

(ns app.client
  (:require
    [com.fulcrologic.fulcro.application :as app]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.dom :as dom]))

(defonce app (app/fulcro-app))

(defsc Root [this props]
  (dom/div "TODO"))

(defn ^:export init
  "Shadow-cljs sets this up to be our entry-point function. See shadow-cljs.edn `:init-fn` in the modules of the main build."
  []
  (app/mount! app Root "app")
  (js/console.log "Loaded"))

(defn ^:export refresh
  "During development, shadow-cljs will call this on every hot reload of source. See shadow-cljs.edn"
  []
  ;; re-mounting will cause forced UI refresh, update internals, etc.
  (app/mount! app Root "app")
  (js/console.log "Hot reload"))

3.4.5. Summary

Your project source tree should now look like this:

$ tree -I node_modules .
.
├── deps.edn
├── package.json
├── resources
│   └── public
│       ├── index.html
├── shadow-cljs.edn
└── src
    └── main
        └── app
            └── client.cljs

That’s it! You have a complete Fulcro application!

3.4.6. Build It!

You can start the compiler server with:

$ npx shadow-cljs server

and navigate to the URL it prints out for the compiler server (usually http://localhost:9630). You can then use the "Builds" menu to turn on/off different client builds and see the progress live as it happens!

It also has hot-code (and CSS) reload built in, so there is no need for any additional tools!

We configured the shadow-cljs server to also start a development mode HTTP server to serve our HTML file and javascript. So, if you didn’t make any typos then your new app should display "TODO" at http://localhost:8000.

3.4.7. Using the REPL

Shadow-cljs creates an network REPL for your clojurescript program. You can actually use a limited REPL via the shadow-cljs control web page (the one at port 9630). If you look at the command-line startup output you’ll also see it report a port number on which you can connect a more advanced REPL (you can nail that port to a constant using the setting :nrepl {:port 9000} at the top-level of shadow-cljs.edn).

If you’re using IntelliJ, edit your run configurations and add a "Clojure → Remote REPL". Give it localhost for the host, and the port reported by shadow’s startup message.

Once you connect to the network REPL you’ll have to select the build you want to work against (and you must already have that application running in a browser). Just run:

user=> (shadow/repl :main)

to cause it to change over to a CLJS REPL connected to your running application.

Now running (js/alert "Hi") should cause the browser to throw up an alert on your application’s tab.

3.5. Basic UI Components

Fulcro supplies a defsc macro to build React components. This macro emits class-based React components (hook support is coming soon, but does not work with defsc). These components are augmented with Fulcro’s data management and render refresh.

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

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 {:className "a" :id "id" :style {:color "red"}}
    (dom/p "Hello")))

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

You can tighten your DOM code up even more if you also :refer the common tags in your namespace declaration:

(ns app.client
  (:require
    [com.fulcrologic.fulcro.dom :as dom :refer [div p]]))

(defsc ComponentName [this props]
  { ...options... }
  (div {:className "a" :id "id" :style {:color "red"}}
    (p "Hello")))

React lifecycle methods are all supported, but we’ll talk about those later.

3.5.1. The render method.

The body of defsc is the render for the component and can do whatever work you need, but it should be a "pure" function of the parameters and return a react element (see React Components, Elements, and Instances). As of React 16 you can return a fragment or sequence of elements as well (though each must have a unique :key).

The DOM element functions allow class names and IDs to be written in various forms for convenience. The following are all equivalent:

(dom/div {:id "id" :className "x y z" :style {:color "red"}} ...)
(dom/div :.x#id {:className "y z" :style {:color "red"}} ...)
(dom/div :.x.y.z#id {:style {:color "red"}} ...)
(dom/div :.x#id {:classes ["y" "z"] :style {:color "red"}} ...)
Note
The keyword notation requires each class to be preceeded by a ., and will give a compile error if you forget to do that.

Children simply placed after the props as nested children.

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

... the actual code is the 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 functions to operate 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, two namespaces are required in order to get the optimal (inlined) client performance.

Note
Regarding performance: The dom macros/functions in Fulcro can be nearly as fast as calling raw React createElement directly (the macros, in fact, directly turn into low-level js/createElement calls with compile-time conversion of the cljs props to js in the optimal case), even when using the convenience features; however, the macros cannot make them that fast if there is ambiguity about the props vs. children. For example, (div :.a (f)) is ambiguous at compile time (the expression after :.a could be a function call returning a props map or a nested React element), so it will be forced to add code that checks for a cljs map, and if it finds one, code to convert it to a js object. Technically you can force the highest level of runtime performance by always including a props map, even if you don’t need one: (div :.a {} (f)). This latter example has no ambiguity and the macro can at compile time convert the props to a javascript object for passing directly to React createElement. The speed difference can be about 3-fold, but even then it is usually so small that it won’t matter, so you may want to use tighter notation unless you measure a real performance problem.

3.5.2. Props

React components receive their data through props and state (which is local mutable state on the component). In Fulcro you will usually use props for most things. The data passed to a component can be accessed (as a CLJS map) by calling comp/props on this, or by destructuring in the second argument of defsc.

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

(defsc Person [this {:person/keys [name 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 (comp/factory Person))

Now we can compose people into our root:

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

3.5.3. 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.

3.5.4. Organizing Source

At this point it is a good idea to adopt some code organization. The application itself might be needed from any number of namespaces, so having it mixed in with things can easily lead to circular requires, which are not allowed in Clojure(script).

To avoid this, we’ll create src/main/app/application.cljs and put this content there:

(ns app.application
  (:require
    [com.fulcrologic.fulcro.application :as app]))

(defonce app (app/fulcro-app))

You probably want to move your actual UI tree to a new namespace for similar reasons. Create a new file src/main/app/ui.cljs with this content:

(ns app.ui
  (:require
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.dom :as dom]))

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

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

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

and now you can trim your client.cljs down to just the initialization code:

(ns app.client
  (:require
    [app.application :refer [app]]
    [app.ui :as ui]
    [com.fulcrologic.fulcro.application :as app]))

(defn ^:export init []
  (app/mount! app ui/Root "app")
  (js/console.log "Loaded"))

(defn ^:export refresh []
  ;; re-mounting will cause forced UI refresh
  (app/mount! app ui/Root "app")
  (js/console.log "Hot reload"))

Splitting up your source will also typically help with overall incremental compilation speed.

3.5.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 UI content with this:

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

;; The keyfn generates a react key for each element based on props. See React documentation on keys.
(def ui-person (comp/factory Person {:keyfn :person/name}))

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

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

(defsc Root [this {:keys [ui/react-key]}]
  (let [ui-data {:friends {:list/label "Friends" :list/people
                                              [{:person/name "Sally" :person/age 32}
                                               {:person/name "Joe" :person/age 22}]}
                 :enemies {:list/label "Enemies" :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           { :list/people [PERSON ...]
;  ==to-one list=>      ==to-many people==>
  :enemies           { :list/people [PERSON ...] }
data tree

3.6. 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?

3.6.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, data management instances or modules peppered through the code, etc.

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

3.6.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 into the initial nodes and edges of the local client database.

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 (comp/get-initial-state Component).

It looks like this:

(ns app.ui
  (:require
    [com.fulcrologic.fulcro.dom :as dom]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]))

(defsc Person [this {:person/keys [name 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 (comp/factory Person {:keyfn :person/name}))

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

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

; Root's initial state becomes the entire app's initial state!
(defsc Root [this {:keys [friends enemies]}]
  {:initial-state (fn [params] {:friends (comp/get-initial-state PersonList {:label "Friends"})
                                :enemies (comp/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=> (com.fulcrologic.fulcro.components/get-initial-state app.ui/Root {})
{:friends {:list/label "Friends",
           :list/people [{:person/name "Sally", :person/age 32} {:person/name "Joe", :person/age 22}]},
 :enemies {:list/label "Enemies",
           :list/people [{:person/name "Fred", :person/age 11} {:person/name "Bobby", :person/age 55}]}}
Note
The REPL shown here is a CLJS REPL. Shadow-cljs makes a REPL port available when it starts. If you connect to it (as a Remote REPL in IntelliJ) you can interact with a live browser session by first typing (shadow/repl :main) (where :main is the name of the build). This requires a browser be actively running the build output.

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 want to see your current application state, you can do so through the app itself:

dev:cljs.user=> (com.fulcrologic.fulcro.application/current-state app.application/app)

but you’ll usually look at this state via the Fulcro Inspect Chrome development tool.

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

3.6.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: in the options map.

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 name (keyword) for that nested bit of state.

So, a data tree like this:

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

would have a query that looks like this:

[{:friends  ; JOIN
    [ :list/label
      {: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. People, in turn, is a join that has nested properties name and age.

  • A vector always means "get this stuff at the current node". Note that this is a relative (to the current node) statement.

  • :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 is a subquery, and therefore 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 singular, 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 :list/people pointed to a vector of Person data. Beware that you don’t confuse yourself with naming (e.g. friends is plural, but points to a single UI element that represents a single list, which in turn owns the items of that 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). Furthermore, it enables the full potential of Pathom when you get to the server interactions.

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

dev:cljs.user=> (fdn/db->tree [{:friends [:list/label]}] (comp/get-initial-state app.ui/Root {}) {})
{:friends {: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. The get-query function actually 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 {:person/keys [name 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 [list/label list/people]}]
  {:query [:list/label {:list/people (comp/get-query Person)}]
   :initial-state
          (fn [{:keys [label]}]
            {:list/label  label
             :list/people (if (= label "Friends")
                            [(comp/get-initial-state Person {:name "Sally" :age 32})
                             (comp/get-initial-state Person {:name "Joe" :age 22})]
                            [(comp/get-initial-state Person {:name "Fred" :age 11})
                             (comp/get-initial-state Person {:name "Bobby" :age 55})])})}
  (dom/div
    (dom/h4 label)
    (dom/ul
      (map ui-person people))))

again, nothing else changes.

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

Finally, we compose to Root:

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

This all looks like a minor (and somewhat wordy) addition; 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 local 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 its 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.

3.7. Passing Callbacks and Other Parent-computed Data

The queries on components 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:

3.7.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 (comp/factory Person {:keyfn :person/name}))

(defsc PersonList [this {:list/keys [label people]}]
  {:query [:list/label {:list/people (comp/get-query Person)}]
   :initial-state
          (fn [{:keys [label]}]
            {:list/label  label
             :list/people (if (= label "Friends")
                            [(comp/get-initial-state Person {:name "Sally" :age 32})
                             (comp/get-initial-state Person {:name "Joe" :age 22})]
                            [(comp/get-initial-state Person {:name "Fred" :age 11})
                             (comp/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.

3.7.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 {:person/keys [name age]} {:keys [onDelete]}] ; (2)
  {: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")))) ; (2)

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

(defsc PersonList [this {:list/keys [label people]}] ;
  {:query [:list/label {:list/people (comp/get-query Person)}]
   :initial-state
          (fn [{:keys [label]}]
            {:list/label  label
             :list/people (if (= label "Friends")
                                   [(comp/get-initial-state Person {:name "Sally" :age 32})
                                    (comp/get-initial-state Person {:name "Joe" :age 22})]
                                   [(comp/get-initial-state Person {:name "Fred" :age 11})
                                    (comp/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 (comp/computed p {:onDelete delete-person}))) people))))) ; (1)
  1. The comp/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 (comp/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.

Note
You can create a comp/computed-factory instead of a regular factory for ui-person. The computed factory accepts props and computed as two arguments instead of having to use a nested notation.

3.8. Updating the Data Tree

Now the real fun begins: Making things dynamic.

Most operations that make changes to the client database should happen through the Fulcro transaction system. It is actually possible to do more advanced low-level things, but that is beyond our current scope.

3.8.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 themselves are just data and can be 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.

(comp/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:

(comp/transact! this `[(ops/delete-person {:list-name ~name :person ~person})])
Important
When you define mutations they are actually set up to return themselves as data instead if called like functions. This allows you to avoid the need to quote in most circumstances.

The defmutation macro returns a function-like object that just returns itself as data when called "normally":

user=> (app.mutations/delete-person {:name "Joe"})
(app.mutations/delete-person {:name "Joe"})

So, here are the rules of using mutations in transact!:

  1. If you can require the mutation namespace without causing circular references, then you can just "call" the mutation within the transaction as-if it were a function (avoiding quoting). The result will still just embed the mutations as raw data.

  2. If you cannot require the namespace, then you must quote it, ensure the fully-qualified namespace is correct, and unquote any data from the surrounding scope.

With the require:

(ns app.ui
  (:require
    [app.mutations :as api]))

  ...
  (comp/transact! this [(api/delete-person {:list-name name ...})])

Without the require:

(ns app.ui
  (:require
    ...))

  ...
  (comp/transact! this `[(app.mutations/delete-person {:list-name ~name ...})])

Both uses generate the exact same runtime result.

3.8.2. Handling Mutations

Mutations can be defined wherever you want, but of course you need to make sure that namespace is required by files that your program already uses or they won’t end up being available at runtime.

Something like src/main/app/mutations.cljs is fine, but you might also find it useful to place your mutations in something topical so you can write the server mutations in the CLJ version of the file, and the client ones in the cljs. Mutations are always namespaced.

A mutation definition 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 some of the names we use for the items 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.mutations
  (:require [com.fulcrologic.fulcro.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 :list/people]
                     [:enemies :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 app.ui in the following ways:

  1. Add a require for app.mutations

  2. Change the callback to run the correct transaction

(ns app.ui
  (:require
    ; ADD THIS:
    [app.mutations :as api])) ; (1)

...

(defsc PersonList [this {:keys [list/label list/people]}]
  ...
  (let [delete-person (fn [name] (comp/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.mutations/delete-person, but the first layer of evaluation (calling a mutation as a function just returns the call itself) will rewrite it to (app.mutations/delete-person …​). 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 a grand unification when it comes to writing full-stack applications.

3.8.3. Hold on – Something Still 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.

3.9. 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 { :friends  { :id :friends
                           :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 :list/people is a problem. There is no schema so there is no way to know which table to look in for "1" and "2"!

The solution is rather easy: code the foreign reference to include the name of the table: [:Person 1].

To-one relations are represented as a single one of these, and to-many relations as a vector of these (to preserve order).

In Fulcro [TABLE ID] is known as an ident.

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

  1. Properties should be namespaced: :person/name, :account/email, etc.

  2. Entities are usually identified by a type-centric ID: :person/id, :account/id, etc.

  3. Table names in the database are usually the same as the ID key of the entities within it (to facilitate some nice support in Pathom).

Using these conventions the prior example would have looked like this:

{:list/id { :friends  { :list/id :friends
                        :list/label "Friends"
                        :list/people [[:person/id 1] [:person/id 2]] }}
 :person/id { 1 {:person/id 1 :person/name "Joe" }
              2 {:person/id 2 :person/name "Sally"}}}

3.9.1. Automatic Normalization

Fortunately, you don’t have to hand-normalize your data. The components have everything they need to do the normalization 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, since you can’t easily normalize things that don’t have them):

The program will now look like this:

(ns app.ui
  (:require
    [app.mutations :as api]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.dom :as dom]))

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

(def ui-person (comp/factory Person {:keyfn :person/id}))

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

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

(defsc Root [this {:keys [friends enemies]}]
  {:query         [{:friends (comp/get-query PersonList)}
                   {:enemies (comp/get-query PersonList)}]
   :initial-state (fn [params] {:friends (comp/get-initial-state PersonList {:id :friends :label "Friends"})
                                :enemies (comp/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 props from the defsc argument list is "in scope" for ident.

  2. We will be using IDs now, so we need to add them to the query (and props destructuring).

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

  4. The callback will now be able to delete people by their ID (see below)

  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 (NOTE: It is much easier to look at this using Fulcro Inspect in your developer tools tab):

dev:cljs.user=> (com.fulcrologic.fulcro.application/current-state app.application/app)
{:friends [:list/id :friends]
 :enemies [:list/id :enemies]
 :person/id {1 {:person/id 1, :person/name "Sally", :person/age 32}
             2 {:person/id 2, :person/name "Joe", :person/age 22}
             3 {:person/id 3, :person/name "Fred", :person/age 11}
             4 {:person/id 4, :person/name "Bobby", :person/age 55}}
 :list/id {:friends {:list/id :friends, :list/label "Friends", :list/people [[:person/id 1] [:person/id 2]]}
           :enemies {:list/id :enemies, :list/label "Enemies", :list/people [[:person/id 3] [:person/id 4]]}}}

Note that fdn/db→tree understands this normalized form, and can convert it (via a query) to the proper data tree. So, try this at the REPL:

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

3.9.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. Fulcro’s com.fulcrologic.fulcro.algorithms/merge namespace includes tools for merging and managing normalized data and includes some helpers for dealing with lists of idents. Our mutation can now become:

(ns app.mutations
  (:require
    [com.fulcrologic.fulcro.mutations :as m :refer [defmutation]]
    [com.fulcrologic.fulcro.algorithms.merge :as merge]))

(defmutation delete-person
  "Mutation: Delete the person with `:person/id` from the list with `:list/id`"
  [{list-id   :list/id
    person-id :person/id}]
  (action [{:keys [state]}]
    (swap! state merge/remove-ident* [:person/id person-id] [:list/id list-id :list/people])))

Mutation "helpers" in fulcro are functions that work against a plain map (normalized app database) so that they can be used easily in swap!. By convention these functions have a * suffix, and often have mutation versions that do not have the suffix.

The remove-ident* mutation helper does just that: It removes an ident from a list of idents. The arguments are the ident to remove and the path to the list of idents that you want to remove it from.

Of course, you’ll have to change the mutation usage in the application to look like this now:

(defsc Person [this {:person/keys [id name age] :as props} {:keys [onDelete]}]  (1)
    ...
    (dom/h5 (str name " (age: " age ")") (dom/button {:onClick #(onDelete id)} "X")))) ; (2)

...

(defsc PersonList [this {:list/keys [id label people] :as props}] (1)
  ...
  (let [delete-person (fn [person-id] (comp/transact! this [(api/delete-person {:list/id id :person/id person-id})]))] ; (2)
  1. Destructure ID from the props.

  2. Use IDs in the mutation and callback.

If we were to now refactor the UI and 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.

3.9.3. How Automatic Normalization Works (optional)

It is good to know how an arbitrary tree of data (the one in initial app state) 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 comp/get-query), the get-query function adds metadata to each component’s query fragment that names which component that query fragment came from.

For example, try this at the REPL:

dev:cljs.user=> (meta (com.fulcrologic.fulcro.components/get-query app.ui/PersonList))
{:component app.ui/PersonList}

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

So, Fulcro includes a function called com.fulcrologic.fulcro.algorithms.normalize/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 appropriate table (by calling your ident function), and replaces the data in the tree with its 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.).

3.10. 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.

    • Fulcro and React manage the UI to do a minimal refresh.

3.11. 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.

3.11.1. Fulcro Inspect

We’ve mentioned this before, but now that you know about the centralized database, we want to mention it again: Use it! 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.

3.11.2. Workspaces

The Workspaces library allows you to start up a development environment where you can code components, full-stack elements, or even entire applications in a flexible environment. Remember, we can actually split off chunks of the application because they are all fed data via a normalized database and coupled only to their parent.

Setting up an alternate build for creating sections of your application in isolation can be a real productivity boost.

3.12. 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.

3.12.1. The Communication

Fulcro uses transit as the over-the-wire protocol for network requests. There is only one API endpoint, and an EQL parser on the server will process the requests and return responses. The overall network interaction story is essentially just EQL requests and responses.

We recommend using Pathom to process the EQL on the server, and we’ll set up a simple Pathom parser for our application’s server.

Add this to your deps:

com.wsscode/pathom     {:mvn/version "2.2.15"}

and create the file src/main/app/parser.clj with this content:

(ns app.parser
  (:require
    [com.wsscode.pathom.core :as p]
    [com.wsscode.pathom.connect :as pc]
    [taoensso.timbre :as log]))

(def resolvers [])

(def pathom-parser
  (p/parser {::p/env     {::p/reader                 [p/map-reader
                                                      pc/reader2
                                                      pc/ident-reader
                                                      pc/index-reader]
                          ::pc/mutation-join-globals [:tempids]}
             ::p/mutate  pc/mutate
             ::p/plugins [(pc/connect-plugin {::pc/register resolvers})
                          p/error-handler-plugin]}))

(defn api-parser [query]
  (log/info "Process" query)
  (pathom-parser {} query))

3.12.2. Setting up a Server

To run a server you’ll first need something that implements the core HTTP stuff, and some bits of glue. The standard for this in Clojure is Ring, and a common easy-to-use HTTP server is http-kit.

Open your deps.edn file and add dependencies so it looks like this:

{:paths   ["src/main" "resources"]
 :deps    {org.clojure/clojure    {:mvn/version "1.10.1"}
           com.fulcrologic/fulcro {:mvn/version "3.0.0-alpha-18"}
           com.wsscode/pathom     {:mvn/version "2.2.15"}
           ring/ring-core         {:mvn/version "1.6.3"}
           http-kit               {:mvn/version "2.3.0"}}

 :aliases {:dev {:extra-paths ["src/dev"]
                 :extra-deps  {org.clojure/clojurescript {:mvn/version "1.10.520"}
                               thheller/shadow-cljs      {:mvn/version "2.8.40"}
                               binaryage/devtools        {:mvn/version "0.9.10"}}}}}

and then add a src/main/app/server.clj namespace:

(ns app.server
  (:require
    [app.parser :refer [api-parser]]
    [org.httpkit.server :as http]
    [com.fulcrologic.fulcro.server.api-middleware :as server]
    [ring.middleware.content-type :refer [wrap-content-type]]
    [ring.middleware.resource :refer [wrap-resource]]))

(def ^:private not-found-handler
  (fn [req]
    {:status  404
     :headers {"Content-Type" "text/plain"}
     :body    "Not Found"}))

(def middleware
  (-> not-found-handler                                     ; (1)
    (server/wrap-api {:uri    "/api"
                      :parser api-parser})                  ; (2)
    (server/wrap-transit-params)
    (server/wrap-transit-response)
    (wrap-resource "public")                                ; (3)
    wrap-content-type))

(defonce stop-fn (atom nil))

(defn start []
  (reset! stop-fn (http/run-server middleware {:port 3000})))

(defn stop []
  (when @stop-fn
    (@stop-fn)
    (reset! stop-fn nil)))
  1. The middleware stack ends at a not found handler

  2. The wrap-api middleware from Fulcro, along with transit in/out encode/decode.

  3. Resource serving (to get our index.html file) and set the content type correctly.

3.12.3. Running the Server

The Clojure REPL will automatically start in the user namespace. During development we can leverage that to make our lives a little easier. One thing we’ll commonly want to do it refresh our server when we make code changes.

So, we should add a little more to our project. First, code reloading tools in deps.edn:

{:paths   ["src/main" "resources"]
 :deps    {org.clojure/clojure    {:mvn/version "1.10.1"}
           com.fulcrologic/fulcro {:mvn/version "3.0.0-alpha-18"}
           com.wsscode/pathom     {:mvn/version "2.2.15"}
           ring/ring-core         {:mvn/version "1.6.3"}
           http-kit               {:mvn/version "2.3.0"}}

 :aliases {:dev {:extra-paths ["src/dev"]
                 :extra-deps  {org.clojure/clojurescript   {:mvn/version "1.10.520"}
                               thheller/shadow-cljs        {:mvn/version "2.8.40"}
                               binaryage/devtools          {:mvn/version "0.9.10"}
                               org.clojure/tools.namespace {:mvn/version "0.2.11"}}}}} ; (1)
  1. Tools for reloading namespaces at runtime.

and we only want our "development" mode to see our user.clj file, so we’ll put it in the alternate dev source path src/dev/user.clj:

Important
This file goes in src/dev, not src/main.
(ns user
  (:require
    [app.server :as server]
    [clojure.tools.namespace.repl :as tools-ns :refer [set-refresh-dirs refresh]]))

;; Ensure we only refresh the source we care about. This is important
;; because `resources` is on our classpath and we don't want to
;; accidentally pull source from there when cljs builds cache files there.
(set-refresh-dirs "src/dev" "src/main")

(defn start []
  (server/start))

(defn restart
  "Stop the server, reload all source code, then restart the server.

  See documentation of tools.namespace.repl for more information."
  []
  (server/stop)
  (refresh :after 'user/start))

;; These are here so we can run them from the editor with kb shortcuts.  See IntelliJ's "Send Top Form To REPL" in
;; keymap settings.
(comment
  (start)
  (restart))
Important
Make sure the dev alias is enabled for Clojure when you run your REPL. In IntelliJ this is in the "Clojure Deps" tab, under "aliases". From the command line include -A:dev.

If you start the server with (start) you should be able to load http://localhost:3000/index.html. Note you must specify the path because our server is quite literal at the moment. It has no idea what "/" means.

3.12.4. 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. The additions we’ve made make this possible by just running:

user=> (restart)

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=> (start)

3.13. Parsing Queries

Before we can obtain data from the server we have to understand a little bit about writing resolvers for Pathom. Pathom is basically a library for building EQL parsers. The two critical things you define are called resolvers and mutations. The latter is pretty much identical to what you’ve already done in Fulcro, and at the moment we’re primarily interested in satisfying some reads. So let’s talk about resolvers.

Resolvers are defined to expect some particular inputs, and declare what they can produce. They are always called with some context (stored in an env parameter) and some optional inputs that match the expected inputs. The context includes the portion of the query that is currently being parsed along with a number of other things. The idea is that "given a context and some input" you should be able to write some code that can resolve the desired outputs.

In this chapter the following requires with aliases are assumed:

...
  (:require
    [com.wsscode.pathom.core :as p]
    [com.wsscode.pathom.connect :as pc]))

Here is a sample resolver:

(pc/defresolver person-resolver [env input]
  {::pc/input  #{:person/id}
   ::pc/output [:person/name]}
  (let [name (get-name-from-database (:person/id input))]
    {:person/name name}))

it declares "Given a :person/id as input, I can produce their name". The inputs are written as a set, and the output is written as an EQL query. The body of the function will find the required input in the input parameter, and must return an EQL response that matches the shape of the declared ::pc/output.

Furthermore, the output of a resolver augments the known context that Pathom is working with, and can be used to chain lookups across resolvers to fulfill a complete query.

For the sake of our examples we’re going to hand-generate a server-side database in simple maps so that you can concentrate on the code of interest instead of boilerplate database initialization and queries.

Add a src/main/app/resolvers.clj file with this content:

(ns app.resolvers
  (:require
    [com.wsscode.pathom.core :as p]
    [com.wsscode.pathom.connect :as pc]))

(def people-table
  {1 {:person/id 1 :person/name "Sally" :person/age 32}
   2 {:person/id 2 :person/name "Joe" :person/age 22}
   3 {:person/id 3 :person/name "Fred" :person/age 11}
   4 {:person/id 4 :person/name "Bobby" :person/age 55}})

(def list-table
  {:friends {:list/id     :friends
             :list/label  "Friends"
             :list/people [1 2]}
   :enemies {:list/id     :enemies
             :list/label  "Enemies"
             :list/people [4 3]}})

;; Given :person/id, this can generate the details of a person
(pc/defresolver person-resolver [env {:person/keys [id]}]
  {::pc/input  #{:person/id}
   ::pc/output [:person/name :person/age]}
  (get people-table id))

;; Given a :list/id, this can generate a list label and the people
;; in that list (but just with their IDs)
(pc/defresolver list-resolver [env {:list/keys [id]}]
  {::pc/input  #{:list/id}
   ::pc/output [:list/label {:list/people [:person/id]}]}
  (when-let [list (get list-table id)]
    (assoc list
      :list/people (mapv (fn [id] {:person/id id}) (:list/people list)))))

(def resolvers [person-resolver list-resolver])

and then add these resolvers into the parser.clj file:

(ns app.parser
  (:require
    [app.resolvers]
    [com.wsscode.pathom.core :as p]
    [com.wsscode.pathom.connect :as pc]))

(def resolvers [app.resolvers/resolvers])

... as before

These two resolvers already give you a lot of power. We’ve been following a convention for some time of giving idents the "table name" of the ID field. This is because Pathom can treat an ident as "context" for determining which resolver to run.

It turns out that EQL will allow an ident to be used as a join key to establish a particular graph context (i.e. pretend we’re on the node with the given ident): :

[{[:person/id 3] [:person/name]}]

If you load the parser namespace you can try these out in a CLJ REPL (e.g. clj -A:dev at the command line):

(require 'app.parser)

(app.parser/api-parser [{[:list/id :friends] [:list/id]}])
=> {[:list/id :friends] {:list/id :friends}}

(app.parser/api-parser [{[:person/id 1] [:person/name]}])
=> {[:person/id 1] {:person/name "Sally"}}

(app.parser/api-parser [{[:list/id :friends] [:list/id {:list/people [:person/name]}]}])
=> {[:list/id :friends] {:list/id :friends, :list/people [{:person/name "Sally"} {:person/name "Joe"}]}}

Pathom’s magic is that it can traverse the graph based on the declared inputs and outputs. The fact that the list resolver says it outputs :person/id means that Pathom can "connect the dots" to fill in details of that person.

Those two resolvers make it possible for us to execute all of the arbitrary queries we’d need to feed data to our application at any level!

It is also possible to define resolvers that don’t require any inputs at all. These "global resolvers" are the same as "root queries" in GraphQL. We could use one of these for our application, so add this to your resolvers.clj file:

...

(pc/defresolver friends-resolver [env input]
  {::pc/output [{:friends [:list/id]}]}
  {:friends {:list/id :friends}})

(pc/defresolver enemies-resolver [env input]
  {::pc/output [{:enemies [:list/id]}]}
  {:enemies {:list/id :enemies}})

;; Make sure you add the two resolvers into the list
(def resolvers [person-resolver list-resolver friends-resolver enemies-resolver])

and now if you reload the code in your REPL you should be able to run a query like this:

(app.parser/api-parser [{:friends [:list/id {:list/people [:person/name]}]}])
=> {:friends {:list/id :friends, :list/people [{:person/name "Sally"} {:person/name "Joe"}]}}

3.13.1. Loading Data

At this point we’ve actually got everything we need on the server to handle incoming load requests from the client.

In your clojure REPL, use (restart) to stop/reload/start the server. If everything went OK you’ve got a server that can satisfy client queries.

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 lists of 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 most important load scenarios can be done using the com.fulcrologic.fulcro.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 (root prop).

  • Load a tree and normalize it into tables.

  • Target a loaded tree to "start" at some specific edge in the graph.

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

First, get rid of the application’s initial state

(ns app.ui
  (:require
    [app.mutations :as api]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.dom :as dom]))

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

(def ui-person (comp/factory Person {:keyfn :person/id}))

(defsc PersonList [this {:list/keys [id label people] :as props}]
  {:query [:list/id :list/label {:list/people (comp/get-query Person)}]
   :ident (fn [] [:list/id (:list/id props)])}
  (let [delete-person (fn [person-id] (comp/transact! this [(api/delete-person {:list/id id :person/id person-id})]))] ; (2)
    (dom/div
      (dom/ul
        (map #(ui-person (comp/computed % {:onDelete delete-person})) people)))))

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

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

We also added a little "guard code" to test if data was missing so we don’t try to render nil props.

If you now reload your page you should see two headings, but no people.

Loading something into the DB root

Our application has two root-level things that we’d like to load. 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, and load our friends and enemies.

Open the app.application namespace and add remote support:

(ns app.application
  (:require
    [com.fulcrologic.fulcro.application :as app]
    [com.fulcrologic.fulcro.networking.http-remote :as http]))

(defonce app (app/fulcro-app
               {:remotes {:remote (http/fulcro-http-remote {})}}))

and then go to client.cljs and add our initial load during application startup:

(ns app.client
  (:require
    [app.application :refer [app]]
    [app.ui :as ui]
    [com.fulcrologic.fulcro.application :as app]
    [com.fulcrologic.fulcro.data-fetch :as df]))

(defn ^:export init []
  (app/mount! app ui/Root "app")
  (df/load! app :friends ui/PersonList)
  (df/load! app :enemies ui/PersonList)
  (js/console.log "Loaded"))

...

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.

Important
Make sure your application is running from your server (port 3000), and not the dev server.

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

Loading a specific entity and its 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 would be 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/id 3] Person)
Adding Edges

There are a number of ways to "add edges" to the UI data graph when loading. The most common is to use the :target option of load!. This option indicates that the incoming data should be "joined" into the graph as a given edge (the default is to link it to the root node).

So, you could add an arbitrary person the the friends list like this:

(df/load! this [:person/id 3] Person {:target (targeting/append-to [:list/id :friends :list/people])})
Note
Targets are just graph database paths. The append-to modifier comes from the com.fulcrologic.fulcro.algorithms.data-targeting namespace and will append the edge to a to-many relation if (and only if) it isn’t already there. Remember, any edge is reachable in our database by a path that is at most three elements long (table, id, field).
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), server query parameters, error handling, and more. Read the corresponding chapters in this guide for more details.

3.13.2. Handling Mutations on The Server

Mutations can be handled on the server using Pathom’s defmutation macro.

This has a nearly-identical syntax to the client Fulcro macro of the same name.

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. The Pathom defmutation actually lets you override what symbol it responds to, so this is not absolutely necessary.

So, let’s add an implementation for our server-side delete-person.

First, we need to modify our database tables so we can change them. Modify the resolvers.clj to be this:

(ns app.resolvers
  (:require
    [com.wsscode.pathom.core :as p]
    [com.wsscode.pathom.connect :as pc]))

(def people-table
  ;; changed to an atom so we can update thes "databases"
  (atom
    {1 {:person/id 1 :person/name "Sally" :person/age 32}
     2 {:person/id 2 :person/name "Joe" :person/age 22}
     3 {:person/id 3 :person/name "Fred" :person/age 11}
     4 {:person/id 4 :person/name "Bobby" :person/age 55}}))

(def list-table
  (atom
    {:friends {:list/id     :friends
               :list/label  "Friends"
               :list/people [1 2]}
     :enemies {:list/id     :enemies
               :list/label  "Enemies"
               :list/people [4 3]}}))

;; Given :person/id, this can generate the details of a person
(pc/defresolver person-resolver [env {:person/keys [id]}]
  {::pc/input  #{:person/id}
   ::pc/output [:person/name :person/age]}
  (get @people-table id))

;; Given a :list/id, this can generate a list label and the people
;; in that list (but just with their IDs)
(pc/defresolver list-resolver [env {:list/keys [id]}]
  {::pc/input  #{:list/id}
   ::pc/output [:list/label {:list/people [:person/id]}]}
  (when-let [list (get @list-table id)]
    (assoc list
      :list/people (mapv (fn [id] {:person/id id}) (:list/people list)))))

(pc/defresolver friends-resolver [env input]
  {::pc/output [{:friends [:list/id]}]}
  {:friends {:list/id :friends}})

(pc/defresolver enemies-resolver [env input]
  {::pc/output [{:enemies [:list/id]}]}
  {:enemies {:list/id :enemies}})

(def resolvers [person-resolver list-resolver friends-resolver enemies-resolver])

Then create src/main/app/mutations.clj and add this code to it:

(ns app.mutations
  (:require
    [app.resolvers :refer [list-table]]
    [com.wsscode.pathom.connect :as pc]
    [taoensso.timbre :as log]))

(pc/defmutation delete-person [env {list-id   :list/id
                                    person-id :person/id}]
  ;; optional, this is how you override what symbol it responds to.  Defaults to current ns.
  {::pc/sym `delete-person}
  (log/info "Deleting person" person-id "from list" list-id)
  (swap! list-table update list-id update :list/people (fn [old-list] (filterv #(not= person-id %) old-list))))

(def mutations [delete-person])

and then modify parser.clj to require and include the mutations in the overall list of pathom resolvers:

(ns app.parser
  (:require
    [app.resolvers]
    [app.mutations] ; <----- add this
    [com.wsscode.pathom.core :as p]
    [com.wsscode.pathom.connect :as pc]
    [taoensso.timbre :as log]))

(def resolvers [app.resolvers/resolvers app.mutations/mutations]) <---- add to this

;; no other changes

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`"
  [{list-id :list/id
    person-id :person/id}]
  (action [{:keys [state]}] ...)
  (remote [env] true)) ; This one line is it!!!

The syntax for the addition is:

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

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.

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.

3.14. Wrapping Up

At this point you’ve gotten more detailed view of what a Fulcro application looks like. The remaining chapters of this book go into considerably more detail about each important part of the API.

4. Components and 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 com.fulcrologic.fulcro.dom namespace.

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

...

(dom/div :.some-class
  (dom/ul {:style {:color :red}}
    (dom/li ...)))
Important
If you’re writing your UI in CLJC files 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 [com.fulcrologic.fulcro.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

There is also 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 #js notation.

Remember that this (nested) call of functions results in a representation (React Elements) 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 ...))))

but remember that a function has no component hooks, so it cannot do things like short-circuit rendering when props have not changed.

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:

  • Attributes follow the react naming conventions for [Tags and Attributes](https://facebook.github.io/react/docs/tags-and-attributes.html)

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

  • Any time an element includes a collection of children 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.1.2. Converting HTML to Fulcro

Below is a simple little live application that can convert valid HTML into the Clojure(script) you’d use within a Fulcro component. Note that stray whitespace will be converted to (harmless) things like " \n" that can be easily removed, and the output indentation isn’t ideal; Still, it turns the task into one of simple formatting:

Example 3. HTML Converter

(ns book.html-converter
  (:require
    #?(:cljs [com.fulcrologic.fulcro.dom :as dom]
       :clj  [com.fulcrologic.fulcro.dom-server :as dom])
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.mutations :as m :refer [defmutation]]
    [com.fulcrologic.fulcro.dom.html-entities :as ent]
    [taoensso.timbre :as log]
    [camel-snake-kebab.core :as csk]
    [hickory.core :as hc]
    [clojure.set :as set]
    [clojure.pprint :refer [pprint]]
    [clojure.string :as str]))

(def attr-renames {:class        :className
                   :for          :htmlFor
                   :tabindex     :tabIndex
                   :viewbox      :viewBox
                   :spellcheck   :spellcheck
                   :autocorrect  :autoCorrect
                   :autocomplete :autoComplete})

(defn fix-style [style]
  (try
    (let [lines     (str/split style #";")
          style-map (into {} (map (fn [line]
                                    (let [[k v] (str/split line #":")]
                                      [(csk/->camelCase (keyword k)) (str/trim v)])) lines))]

      style-map)
    (catch #?(:cljs :default :clj Exception) e
      style)))

(defn classes->keyword [className]
  (when (seq (str/trim className))
    (let [classes (keep (fn [e] (when (seq e) e)) (str/split className #"  *"))
          kw      (keyword (str "." (str/join "." classes)))]
      kw)))

(defn- chars->entity [ns chars]
  (if (= \# (first chars))
    (apply str "&" (conj chars ";"))                        ; skip it. needs (parse int, convert base, format to 4-digit code)
    (if (seq ns)
      (symbol ns (apply str chars))
      (symbol (apply str chars)))))

(defn- parse-entity [stream result {:keys [entity-ns] :as options}]
  (loop [s stream chars []]
    (let [c (first s)]
      (case c
        (\; nil) [(rest s) (if (seq chars)
                             (conj result (chars->entity entity-ns chars))
                             result)]
        (recur (rest s) (conj chars c))))))

(defn- html-string->react-string [html-str {:keys [ignore-entities?] :as options}]
  (if ignore-entities?
    html-str
    (loop [s html-str result []]
      (let [c (first s)
            [new-stream new-result] (case c
                                      nil [nil result]
                                      \& (parse-entity (rest s) result options)
                                      [(rest s) (conj result c)])]
        (if new-stream
          (recur new-stream new-result)
          (let [segments (partition-by char? new-result)
                result   (mapv (fn [s]
                                 (if (char? (first s))
                                   (apply str s)
                                   (first s))) segments)]
            result))))))

(defn element->call
  ([elem]
   (element->call elem {}))
  ([elem {:keys [ns-alias keep-empty-attrs?] :as options}]
   (cond
     (and (string? elem)
       (let [elem (str/trim elem)]
         (or
           (= "" elem)
           (and
             (str/starts-with? elem "<!--")
             (str/ends-with? elem "-->"))
           (re-matches #"^[ \n]*$" elem)))) nil
     (string? elem) (html-string->react-string (str/trim elem) options)
     (vector? elem) (let [tag               (name (first elem))
                          raw-props         (second elem)
                          classkey          (when (contains? raw-props :class)
                                              (classes->keyword (:class raw-props)))
                          attrs             (cond-> (set/rename-keys raw-props attr-renames)
                                              (contains? raw-props :class) (dissoc :className)
                                              (contains? raw-props :style) (update :style fix-style))
                          children          (keep (fn [c] (element->call c options)) (drop 2 elem))
                          expanded-children (reduce
                                              (fn [acc c]
                                                (if (vector? c)
                                                  (into [] (concat acc c))
                                                  (conj acc c)))
                                              []
                                              children)]
                      (concat (list) (keep identity
                                       [(if (seq ns-alias)
                                          (symbol ns-alias tag)
                                          (symbol tag))
                                        (when classkey classkey)
                                        (if keep-empty-attrs?
                                          attrs
                                          (when (seq attrs) attrs))]) expanded-children))
     :otherwise "")))

(defn html->clj-dom
  "Convert an HTML fragment (containing just one tag) into a corresponding Dom cljs.

  Options is a map that can contain:

  - `ns-alias`: The primary DOM namespace alias to use.  If not set, the calls will not be namespaced.
  - `keep-empty-attrs?`: Boolean (default false). Output (dom/p {} ...) vs (dom/p ...).
  - `entity-ns`: String (defaults to \"ent\"). When named HTML entities are found they are converted to the Fulcro
    HTML entity ns symbols that stand for the correct unicode (e.g. \"&quot;\" -> `ent/quot`). This is the ns alias
    for those.
  - `ignore-entities?`: Boolean (default false). If true, entities in strings will not be touched.
  "
  ([html-fragment options]
   (let [hiccup-list (map hc/as-hiccup (hc/parse-fragment html-fragment))
         options     (cond-> options
                       (not (contains? options :entity-ns)) (assoc :entity-ns "ent"))]
     (let [result (keep (fn [e] (element->call e options)) hiccup-list)]
       (if (< 1 (count result))
         (vec result)
         (first result)))))
  ([html-fragment]
   (html->clj-dom html-fragment {:ns-alias "dom"})))

(defmutation convert [p]
  (action [{:keys [state]}]
    (let [html (get-in @state [:top :conv :html])
          cljs (html->clj-dom html)]
      (swap! state assoc-in [:top :conv :cljs] {:code cljs}))))

(defsc HTMLConverter [this {:keys [html cljs]}]
  {:initial-state (fn [params] {:html "<div id=\"3\" class=\"b\"><p>Paragraph</p></div>" :cljs {:code (list)}})
   :query         [:cljs :html]
   :ident         (fn [] [:top :conv])}
  (dom/div {:className ""}
    (dom/textarea {:cols     80 :rows 10
                   :onChange (fn [evt] (m/set-string! this :html :event evt))
                   :value    html})
    (dom/button :.c-button {:onClick (fn [evt]
                                       (comp/transact! this `[(convert {})]))} "Convert")
    (dom/pre {} (with-out-str (pprint (:code cljs))))))

(def ui-html-convert (comp/factory HTMLConverter))

(defsc Root [this {:keys [converter]}]
  {:initial-state {:converter {}}
   :query         [{:converter (comp/get-query HTMLConverter)}]}
  (ui-html-convert converter))

4.2. The defsc Macro

Fulcro’s defsc is the main macro you’ll use to create components. It is sanity-checked for the most common elements: ident (optional), query, props destructuring, 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.2.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-extensible-arg>]
 { ...options... }
 (dom/div {:onClick (:onClick computed)} (:db/id props)))

The last parameter is used by libraries to augment defsc with any additional data they might want to make convenient. See the fulcro-garden-css library for an example.

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 "extra map" without having to write a separate let:

(defsc DestructuredExample [this
                            {:keys [db/id] :as props}
                            {:keys [onClick] :as computed :or {onClick identity}}]
  {:query         [:db/id]
   :initial-state {:db/id 22}}
  (dom/div
    (str "Component: " id)))

4.2.2. Options – Lambda vs. Template

The core options (:query, :ident, :initial-state) of defsc are special. They 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 ident can use this and props from the defsc argument list. The other two are primarily used "statically" and have no sane this or props.

4.2.3. Ident Generation

If you include :ident, it can take three possible forms: a template, lambda, or keyword.

Keyword Idents

The keyword option is a popular option if you’re following the recommended naming conventions. It means that the table name and the ID key of the entity are the same.

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.

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.

So, the following two forms are identical:

(defsc Person [_ _]
 {:ident :person/id
  ...

;; OR

(defsc Person [_ _]
 {:ident [:person/id :person/id]
  ...
Lambda Idents

The above options are great for the common cases, 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 causes an ident lamda to "close over" the defsc argument list, which 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.2.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 can only include things that are in the query.

  • The ident’s id propery is checked to make sure it is in the query (if ident is in template mode)

  • The initial app state can only contains things that are also 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 or wildcards. It is also useful when you want to disable the sanity checks for any reason.

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

4.2.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 (comp/get-query Job)}]
  :initial-state {:person/job {:job/name "Welder"}}
  ...)

means (in simplified terms):

(defsc Person [this props]
  {:initial-state (fn [params] {:person/job (comp/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 (comp/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 [(comp/get-initial-state Job {:job/name "Welder"})
                         (comp/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.2.6. Pre-Merge

The :pre-merge option offers a hook to manipulate data entering your Fulcro app at a component level. The option looks like this:

(defsc Countdown [this props]
  {:pre-merge (fn [env] ...)}
  ...)

The :pre-merge lambda receives a single map containing the following keys:

  • :data-tree - the new data tree entering the database (e.g. from a load or explicit merge-component!)

  • :current-normalized - the current entity value (normalized form in the db)

  • :state-map - the current normalized client database (as a map, not an atom)

  • :query - the query being used to for this request (user may have modified the original using :focus, :without or :update-query during the load call

and returns the data that should actually be merged into app state. This feature requires a complete understanding of normalization and full stack operation, and is covered in a later chapter.

4.2.7. Component Constructor

It is sometimes useful to be able to run some code once on component construction. In React this is particularly useful when you want a function for saving off :ref or as other callbacks (so the function isn’t changing all the time), or copy something from props into state.

Use :initLocalState to do these operations before you return a value.

4.2.8. React Lifecycle Methods

The options of defsc allow for React Lifecycle methods to be defined (as lambdas). The first argument of all non-static lifecycle methods is this. The React documentation describes any further arguments. Where props or state are expected you will be given Fulcro’s version of those (cljs data instead of raw js). Where the current props are not an argument you can call (comp/props this) to get them, and you can obtain computed using comp/get-computed.

The signatures are:

(defsc Component [this props]
    :initLocalState            (fn [this props] ...)
    :shouldComponentUpdate     (fn [this next-props next-state] ...)
    :componentWillReceiveProps (fn [this next-props] ...)
    :componentWillUpdate       (fn [this next-props next-state] ...)
    :componentDidUpdate        (fn [this prev-props prev-state] ...)
    :componentWillMount        (fn [this] ...)
    :componentDidMount         (fn [this] ...)
    :componentWillUnmount      (fn [this] ...)

    ;; Replacements for deprecated methods in React 16.3+
    :UNSAFE_componentWillReceiveProps (fn [this next-props] ...)
    :UNSAFE_componentWillUpdate       (fn [this next-props next-state] ...)
    :UNSAFE_componentWillMount        (fn [this] ...)

    ;; ADDED for React 16:
    :componentDidCatch         (fn [this error info] ...)
    :getSnapshotBeforeUpdate   (fn [this prevProps prevState] ...)
    :getDerivedStateFromProps  (fn [props state] ...) ; static

    ;; ADDED for React 16.6:
    :getDerivedStateFromError  (fn [error] ...)  ; static. **NOTE**: Sets low-level state, Use get-react-state.

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

Component Local State and Errors

Fulcro wraps React’s state such that you can easily store cljs data in the component (e.g. arguments to methods like componentDidUpdate prev-state will be a cljs map, NOT the raw JS one that React uses).

The same is true for the return value of initLocalState: You return a CLJS map, and it is safely stored in react state but otherwise behaves just like the native.

The advantage of this is twofold:

  1. Convenience: You get to use immutable data for component-local state

  2. Speed: Fulcro is able to very quickly compare and short-circuit React rendering because immutable state is very fast to compare (it can mostly be a single reference compare).

This is accomplished by storing the "Fulcro" version of component state under the fulcro$state key in the low-level js state. Normally, this is of no concern to you at all, but React 16.7 added a static lifecycle method called getDerivedStateFromError whose return value is meant to be a js map that gets merged to the low-level state, and there is no internal hook or hack (yet found) to merge that into the correct Fulcro location.

Thus, if you provide a getDerivedStateFromError your return value (a CLJS map) will overwrite your current component local state.

If you’re not using component local state for anything else, then this is probably just fine, but if you have important information in component-local state you may want to choose an alternative.

One workaround is to set state in componentDidCatch. This is deprecated by React because it happens in a different phase of the React processing, so you should read up on that in the React docs to see if it is acceptable for your case.

Another (trivial but possibly annoying) workaround is to write a wrapper component that does nothing but deal with errors, so that the state is never used for anything but error handling.

4.2.9. 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 mis-spelling a property).

For example, try:

  • Mismatching the name of a prop in a query 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 joined child (instead of a map or vector of maps)

  • Forget to query for the ID field of a component that is used in an ident

In some cases the sanity checking is too aggressive or may mis-detect a problem. To get around it simply use the lambda style.

4.3. The Component Registry

You may find it useful to note that any Fulcro component that is loaded in your application will appear in a global component registry. You can look up such components using a symbol or keyword that matches the fully namespace-qualified name of the component:

(comp/registry-key->class `app.ui/Root)

;; OR

(comp/registry-key->class :app.ui/Root)

This comes in handy for things like code splitting (where the dynamically loaded code will appear in the registry), tracking classes (by name) in app state (which cannot appear there as "code").

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 com.fulcrologic.fulcro.components/factory:

(def ui-component (comp/factory MyComponent {:keyfn f}))

The :keyfn option is a function against props to generate a React key. This should be supplied on any component that will appear as a to-many child to ensure React rendering can properly diff.

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 [{:ui/child (comp/get-query Child)}], and then when you destructure in render: (let [{:ui/keys [child]} (comp/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- eliminates such collisions.

4.5. Render and Props

Properties are always passed to a component factory as the first argument and are not optional:

(ui-child child-props)

The properties are available as the second argument to defsc for the render body, but can also be accessed by calling com.fulcrologic.fulcro.components/props on this. The latter approach is useful in functions that receive the instance as an argument.

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. Such components do not take advantage of any other Fulcro advantages, such as render optimizations.

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 may want to 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 {:list/keys [items]}]
  {:query [{:list/items (comp/get-query CheckboxItem)}]}
  (let [all-checked? (every item-checked? items)]
    (dom/div
      "All: " (dom/input {:checked all-checked? ...})
    (dom/ul ...))))

There is a problem, though: the rendering refresh algorithms of Fulcro tries to avoid rendering things that don’t actually change. In this case the thing that is changing is a child (it is being toggled). Technically this changes the parent’s props (because the child flows through it) but some of Fulcro’s rendering optimizations might not see it that way (nothing the parent directly queried for changed).

If you’re using the default ident-optimized render, then you’ll want to add a refresh option to the transaction that changes the child:

(comp/transact! this [(toggle-checkbox {...})] {:refresh [:list/items]})

The :refresh option indicates that the property (or ident) listed changed in a way that any component querying directly for it should be refreshed even if the data that the component itself uses did not change.

Note
The ident of this is always included in the refresh list for you; therefore you can avoid having to deal with this additional parameter if you run the toggle against the parent (e.g. via a callback sent to the child).
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 "remove" 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. Parents should not pass callbacks through the props because the parent is where they are created, 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 by wrapping them in com.fulcrologic.fulcro.components/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 (comp/get-computed this :onDelete)]
    ...))

(defsc Parent [this {:keys [x child]}]
  {:query  [:x {:child (comp/get-query Child)}]}
  (let [onDelete (fn [id] (comp/transact! ...))
        child-props-with-callbacks (comp/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 subsequent use.

For convenience there is a function that can generate a factory that accepts computed data as a second argument to the factory so you can avoid calling computed:

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

(def ui-child (comp/computed-factory Child))

(defsc Parent [this {:keys [x child]}]
  {:query  [:x {:child (comp/get-query Child)}]}
  (let [onDelete (fn [id] (comp/transact! ...))]
    (ui-child child {:onDelete onDelete})))

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

If children are passed to your factory they will be available in (comp/children this). Basically, the child or children can simply be dropped into the place where they should be rendered.

(defsc Parent [this props]
  {:query [:x]}
  (apply dom/div :.ui.grid
    (comp/children this)))

(def ui-parent (comp/factory Parent))

(defsc Component [this {:keys [parent-props child-props other-child-props]}]
  {:query [{:parent-props (get-query X)}
           {:child-props (get-query Y)}
           {:other-child-props (get-query Z)}]
   ...}
  (ui-parent parent-props
    (ui-child child-props)
    (ui-child other-child-props)))

The main trick is making sure you get the data feed right. In this case the data is all obtained by the component rendering this custer, even though the UI tree has a slightly different shape. Technically they are all "data children" of Component. Often, the Parent in this situation is something like a modal or layout component that may or may not even need a query.

React 16 Fragments and Returning Multiple Children

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’s components namespaces include a fragment function to wrap elements in a fragment:

(defsc X [this props]
  (comp/fragment ; in the comp namespace, because it isn't DOM specific. Could be used with native.
    (dom/p "a")
    (dom/p "b")))

or

(defsc X [this props]
  [(dom/p {:key "a"} "a") (dom/p {:key "b"} "b")])

(def ui-x (comp/factory X))

allows your component to return multiple elements that are spliced into the parent without any "wrapping" elements in the DOM. If you return a vector then you also need to supply React keys.

(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 a 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. 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 (comp/get-initial-state Collapse {:id 1 :start-open false})})
   :query [{:collapse-1 (comp/get-query Collapse)}
           {:child (comp/get-query SomeChild)}]}
  (dom/div
    (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. Fulcro is designed to be fast enough that 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. See the optimization guidelines if you have problems.

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.
Note
Most HTML inputs deal in strings (even date and numeric controls). Be careful when using the value from change events in your database, because you may change your data to a string by mistake. A nice way to handle this is to wrap inputs with your own components that do data transforms in-between :value and :onChange.

See React Forms for more details.

4.6.1. Advanced Details of Wrapped Inputs

React is actually designed for you to do controlled components via component-local state, and in reality it doesn’t deal well with a behind-the-scenes persistence mechanism tracking the state. Fulcro actually has a wrapper around inputs to deal with these complications for you.

If you use any kind of library that provides input-like controls you may experience similar kinds of issues. You have two easy options to make them behave correctly: Use component-local state with them, or similarly wrap them using Fulcro’s pre-built wrapper code. There is a function in the dom namespace called wrap-form-element that can be used to create a factory for input-like controls that misbehave.

4.7. Using defsc for Rendering Optimization

Normally you’ll define components that have both a query and ident; however, it is perfectly legal to define components that are completely controlled by their parent and have no identity of their own. This is useful when you have a component that has a rather large UI, but for which the query does not need further breakdown. Using defsc instead of defn to render subsections of a component means that you get shouldComponentUpdate optimizations that will short-circuit automatically when the props sent to those components have not changed.

A simple example:

(defsc TopBar [this {:keys [title]}]
  {}
  (dom/div
   ...
   (dom/h3 title)))

(def ui-top-bar (comp/factory TopBar))

instead of

(defn ui-top-bar [{:keys [title]}]
  (dom/div
    ...
    (dom/h3 title)))

4.8. React Lifecycle Examples

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.8.1. Focusing an input

Focus is a stateful browser mechanism, and React cannot infer the rendering of "focus" from pure data. As such, when you need to deal with UI focus it generally involves some interpretation, and possibly component local state. The most common 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.


(ns book.ui.focus-example
  (:require [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
            [com.fulcrologic.fulcro.dom :as dom]
            [com.fulcrologic.fulcro.mutations :as m]
            [goog.object :as gobj]))

(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]
   :initLocalState     (fn [this _]
                         {:save-ref (fn [r] (gobj/set this "input-ref" r))})
   :componentDidUpdate (fn [this prev-props _]
                         (when (and (not (:editing? prev-props)) (:editing? (comp/props this)))
                           (when-let [input-field (gobj/get this "input-ref")]
                             (.focus input-field))))}
  (let [save-ref (comp/get-state this :save-ref)]
    (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      save-ref})
      ; do an explicit focus
      (dom/button {:onClick (fn []
                              (when-let [input-field (gobj/get this "input-ref")]
                                (.focus input-field)
                                (.setSelectionRange input-field 0 (.. input-field -value -length))))}
        "Highlight All"))))

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

(defsc Root [this {:keys [field] :as props}]
  {:query         [{:field (comp/get-query ClickToEditField)}]
   :initial-state {:field {}}}
  (ui-click-to-edit field))
Note
The older string support for refs is still present, but will not be supported in future versions. Use the function-based version of refs and port old code to that to avoid future problems.

4.8.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:

Example 5. D3

(ns book.ui.d3-example
  (:require
    [com.fulcrologic.fulcro.dom :as dom]
    ;; REQUIRES shadow-cljs, with "d3" in package.json
    ["d3" :as d3]
    [goog.object :as gobj]
    [com.fulcrologic.fulcro.mutations :as m :refer [defmutation]]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]))

(defn render-squares [dom-node props]
  (let [svg       (-> d3 (.select dom-node))
        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 [this]
                            (when-let [dom-node (gobj/get this "svg")]
                              (render-squares dom-node (comp/props this))))
   :shouldComponentUpdate (fn [this next-props next-state]
                            (when-let [dom-node (gobj/get this "svg")]
                              (render-squares dom-node next-props))
                            false)}
  (dom/svg {:style   {:backgroundColor "rgb(240,240,240)"}
            :width   200 :height 200
            :ref     (fn [r] (gobj/set this "svg" r))
            :viewBox "0 0 1000 1000"}))

(def d3-thing (comp/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 #(comp/transact! this
                             `[(add-square {})])} "Add Random Square")
    (dom/button {:onClick #(comp/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.8.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.

The set-state! function 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. Note, however, that React 16’s setState schedules a state update and render. It is not guaranteed to be immediate.

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

The following code is an example using component local state to render a box that follows the hover position of the mouse.


(ns book.ui.hover-example
  (:require
    [com.fulcrologic.fulcro.mutations :refer [defmutation]]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc initial-state]]
    [goog.object :as gobj]
    [com.fulcrologic.fulcro.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. "
  [canvas props coords]
  (let [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 coords) 5) (- (second coords) 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]
  (let [canvas (gobj/get child "canvas")]
    (comp/transact! child `[(update-marker
                              {:coords ~(event->normalized-coords evt 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 [canvas         (gobj/get child "canvas")
        updated-coords (event->dom-coords evt canvas)]
    (comp/set-state! child {:coords updated-coords})
    (render-hover-and-marker canvas (comp/props child) updated-coords)))

(defsc Child [this {:keys [id size] :as props}]
  {:query          [:id :size :marker]
   :initial-state  (fn [_] {:id 0 :size 50 :marker [0.5 0.5]})
   :ident          (fn [] [:child/by-id id])
   :initLocalState (fn [this _] {: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
  (when-let [canvas (gobj/get this "canvas")]
    (render-hover-and-marker canvas props (comp/get-state this :coords)))
  (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
                                (gobj/set this "canvas" r)
                                (render-hover-and-marker r props (comp/get-state this :coords))))
               :style       {:border "1px solid black"}}))

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

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

The component receives mouse move events to show a hover box. In this case we really don’t care to take the overhead to track the cursor position in real app state, so we leverage component local state instead. Clicking to save the box position and resizing the container are real transactions, and will actually cause a refresh from application state to update the rendering.

4.9. Using Javascript React Components

One of the great parts about React is the ecosystem. There are some great libraries out there. Many libraries will work without issue as long as you’re using shadow-cljs as your compiler; However, the interop story requires a few adaptations. The goal of this section is to make that story a little clearer.

4.9.1. Factory Functions for JS React Components

You must understand React and the ecosystem. This book is not a tutorial on the fundamental js ecosystem. Some important points to understand:

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

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

  3. 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. com.fulcrologic.fulcro.components/force-children helps us in this regard by taking a seq and returning a vector.

The react-iterop namespace includes a pre-written function for generating factories that can accept CLJS maps and children, and properly takes the normal corrective actions:

(ns app
  (:require
    ["some-react-thing" :refer [Thing]]
    [com.fulcrologic.fulcro.algorithms.react-interop :as interop]))

(def ui-thing (interop/react-factory Thing))

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 irrelevant, it’s the implementation we care about.

The running code and source are below:


(ns book.ui.victory-example
  (:require
    [cljs.pprint :refer [cl-format]]
    ;; REQUIRES shadow-cljs, with "victory" in package.json
    ["victory" :refer [VictoryChart VictoryAxis VictoryLine]]
    [com.fulcrologic.fulcro.dom :as dom]
    [com.fulcrologic.fulcro.algorithms.react-interop :as interop]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [taoensso.timbre :as log]))

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

(def vchart (interop/react-factory VictoryChart))
(def vaxis (interop/react-factory VictoryAxis))
(def vline (interop/react-factory 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      (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     (mapv (fn [{:keys [year value]}]
                           {:x (new js/Date year 1 2)
                            :y value})
                     plot-data)]
    (vchart nil
      (vaxis {:label      label
              :standalone false
              :scale      "time"
              :tickFormat (fn [d] (.getFullYear d))
              :tickValues dates})
      (vaxis {:dependentAxis true
              :standalone    false
              :tickFormat    (fn [y] (us-dollars y))
              :domain        #js [min-value max-value]})
      (vline {:data points}))))

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

(defsc Root [this props]
  {:initial-state {:label     "Yearly Value"
                   :x-step    2
                   :plot-data [{:year 1983 :value 20}
                               {: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.9.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 book.react-interop.react-motion-example
  (:require ["react-motion" :refer [Motion spring]]
            [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
            [com.fulcrologic.fulcro.dom :as dom]
            [com.fulcrologic.fulcro.mutations :as m]
            [com.fulcrologic.fulcro.algorithms.react-interop :as interop]))

(def ui-motion (interop/react-factory Motion))

(defsc Block [this {:block/keys [name]} {:keys [top]}]
  {:query         [:block/id :block/name]
   :ident         :block/id
   :initial-state {:block/id   1
                   :block/name "A Block"}}
  (dom/div {:style {:position "relative"
                    :top      top}}
    (dom/div
      (dom/label "Name?")
      (dom/input {:value    name
                  :onChange #(m/set-string! this :block/name :event %)}))))

(def ui-block (comp/factory Block {:keyfn :id}))

(defsc Demo [this {:keys [ui/slid? block]}]
  {:query         [:ui/slid? {:block (comp/get-query Block)}]
   :initial-state {:ui/slid? false :block {:id 1 :name "N"}}
   :ident         (fn [] [:control :demo])}
  (dom/div {:style {:overflow      "hidden"
                    :height        "150px"
                    :margin        "5px"
                    :padding       "5px"
                    :border        "1px solid black"
                    :border-radius "10px"}}
    (dom/button {:onClick (fn [] (m/toggle! this :ui/slid?))} "Toggle")
    (ui-motion {:style {"y" (spring (if slid? 175 0))}}
      (fn [p]
        (let [y (comp/isoget p "y")]
          ; The binding wrapper ensures that internal fulcro bindings are held within the lambda
          (comp/with-parent-context this
            (dom/div :.demo
              (ui-block (comp/computed block {:top y})))))))))

(def ui-demo (comp/factory Demo))

(defsc Root [this {:root/keys [demo] :as props}]
  {:query         [{:root/demo (comp/get-query Demo)}]
   :initial-state {:root/demo {}}}
  (ui-demo demo))

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.10. Colocated CSS

Please see fulcro-garden-css for a library that allows you to co-locate CSS with Fulcro components.

4.11. Component Middleware

When creating a Fulcro application you have the option of setting two bits of component middleware:

(def app (fulcro-app {:props-middleware ...
                      :render-middleware ...}))

These enable some advanced functionality for Fulcro without having to write your own macro that deals with various internals.

4.11.1. Render Middleware

The render middleware is a function (or chain of functions) with the general form:

(fn [this real-render]
   ...
   (real-render))

The intention is that you can wrap or instrument/measure rendering. For example, using the tufte library:

(ns my.ui
  (:require
    ...
    [com.fulcrologic.fulcro.components :as comp]
    [com.fulcrologic.fulcro.rendering.ident-optimized-render :refer [render!]]
    [taoensso.tufte :as tufte :refer [p profile]]))

(tufte/add-basic-println-handler! {})

(defonce SPA (app/fulcro-app
               {:render-middleware (fn [this real-render]
                                     ;; record how long each component, by name, takes to render
                                     (p {:id (comp/component-name this)}
                                       (real-render)))
                :optimized-render! (fn render-performance
                                     ([app options]
                                      ;; capture the recordings and print a report at the end
                                      (profile {}
                                        (render! app options)))
                                     ([app] (render-performance app {})))

would print statistics like this to the Chrome developer’s console:

pId                  nCalls        Min      50% ≤      90% ≤      95% ≤      99% ≤        Max       Mean   MAD      Clock  Total

app.ui.root/Root          1        3ms        3ms        3ms        3ms        3ms        3ms        3ms   ±0%        3ms    17%
app.ui.root/User          1        0ns        0ns        0ns        0ns        0ns        0ns        0ns ±NaN%        0ns     0%

Accounted                                                                                                             3ms    17%
Clock                                                                                                                18ms   100%

Note that you can see the component props using comp/props on the component you’re passed, but you are not allowed to manipulate it. Limited manipulation can be done via the props middleware.

4.11.2. Extra Props Middleware

The props middleware lets you manipulate the raw props that flow through your components.

This is an advanced functionality, since misuse could cause your application to fail to work properly at all.

The primary use for this middleware is to allow libraries to extend the "extra" props argument that defsc suppports.

The defsc macro actually supports up to four arguments: this, props , computed, and extra. This last arg is a map that can be filled by libraries that wish to more tightly integrate with Fulcro’s component system.

Typically you’ll set such middleware something like this:

(def app (app/fulcro-app
           {:props-middleware (comp/wrap-update-extra-props
                                (fn [cls extra-props]
                                   (assoc extra-props :x 1)))))

which then allows you to write components like this:

(defsc MyComponent [this props computed {:keys [x]]
  ...)

The "extra props" are actually encoded onto the internal raw js props of React, so the most common operation is to leverage wrap-update-extra-props so you only affect those.

4.12. Component Options

The component options map that appears after the argument list is just that: a plain map. The defsc macro adds a bit of magic to the three central items (initial-state, query, and ident), but otherwise the map is open; that is to say you can add you own stuff to it, and whatever you add will be available via comp/component-options on both the component class and the instances.

This opens up all sorts of possibilities for extensions. The form-state library, component-local CSS, and new dynamic router use this fact and add their own keys that only they use and know about. You can too.

4.13. 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.13.1. Fulcro and React HOC

There are two things that need to be "patched" in order for Fulcro components to function properly as targets of the HOC pattern:

  • Raw React libraries pass props as js objects. Fulcro normally wraps/unwraps these for you.

  • Fulcro wraps in some special items to props that carry along context for transactional operation.

Fulcro includes an interop function for your convenience that should work for many examples of this pattern "in the wild". The com.fulcrologic.fulcro.algorithms.react-interop namespace includes a function that does the necessary props and context manipulation for you:

;; injected-props in the computed section passes in the raw js props that the HOC passed in (a js map)
(defsc TargetOfHOC [this props {:keys [injected-props]}] ...)

(def ui-target-of-hoc (interop/hoc-factory TargetOfHOC hocFunction))

The following two live examples show you how to use this function in the context of the Google Maps React API, and the Stripe Elements API.


(ns book.react-interop.google-maps-example
  (:require
    ["google-maps-react" :as maps :refer [GoogleApiWrapper Map Marker]]
    [com.fulcrologic.fulcro.algorithms.react-interop :as interop :refer [react-factory]]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.dom :as dom]))

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

;; Another wrapper factory for Marker component
(def ui-map-marker (react-factory Marker))

(defsc LocationView [this {:map/keys [latitude longitude] :as props} {:keys [injected-props] :as computed}]
  {:query         [:map/latitude :map/longitude]
   :ident         (fn [] [:component/id ::location-view])
   :initial-state {:map/latitude 45.5051 :map/longitude -122.6750}}
  (dom/div
    (dom/div {:style {:width "520px" :height "520px"}}
      ;; The API docs say the the HOC adds a "google" prop that should be passed to Map
      (ui-google-map {:google        (.-google injected-props)
                      :zoom          5
                      :initialCenter {:lat latitude :lng longitude}
                      :style         {:width "90%" :height "90%"}}
        (ui-map-marker {:position {:lat latitude :lng longitude}})))))

(def injectGoogle (GoogleApiWrapper #js {:apiKey "AIzaSyAM9PWfj-rJDnmyJvh8QNkO4pgGzy5s_Yg"}))
(def ui-location-view (interop/hoc-factory LocationView injectGoogle))

(defsc Root [this {:root/keys [location-view] :as props}]
  {:query         [{:root/location-view (comp/get-query LocationView)}]
   :initial-state {:root/location-view {}}}
  (dom/div
    (dom/h3 "A Map View")
    (ui-location-view this location-view)))

(ns book.react-interop.stripe-example
  (:require
    ["react-stripe-elements" :as stripe :refer [StripeProvider Elements injectStripe
                                                CardNumberElement
                                                CardExpiryElement
                                                CardCvcElement]]
    [com.fulcrologic.fulcro.algorithms.react-interop :as interop]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.dom :as dom]
    [com.fulcrologic.fulcro.mutations :as m]))

(def test-key "pk_test_crQ5GM8nCR8tM06YZcgunnmZ00qjGWhSNV")

;; Wrap the stripe UI components in interop factories
(def ui-stripe-provider (interop/react-factory StripeProvider))
(def ui-elements (interop/react-factory Elements))
(def ui-card-element (interop/react-factory CardNumberElement))
(def ui-exp-element (interop/react-factory CardExpiryElement))
(def ui-cvc-element (interop/react-factory CardCvcElement))

(defn handle-result [result]
  (let [{{:keys [message]} :error
         {:keys [id]}      :token} (js->clj result :keywordize-keys true)]
    (when message
      (js/alert (str "Error: " message)))
    (when id
      (js/alert (str "Created payment token " id)))))

(defsc CCForm [this {:customer/keys [name] :as props} {:keys [injected-props] :as computed}]
  {:query         [:customer/name]
   :initial-state {:customer/name ""}
   :ident         (fn [] [:component/id ::ccform])}
  ;; isoget is a util that gets props from js maps in cljs, or CLJ ones in clj
  (let [stripe (comp/isoget injected-props "stripe")]
    (dom/form :.ui.form
      {:onSubmit (fn [e] (.preventDefault e))}
      (dom/div :.field
        (dom/label "Name")
        (dom/input {:value    name
                    :onChange #(m/set-string! this :customer/name :event %)}))
      (dom/div :.field
        (dom/label "Card Number")
        (ui-card-element {}))
      (dom/div :.two.fields
        (dom/div :.field
          (dom/label "Expired")
          (ui-exp-element {}))
        (dom/div :.field
          (dom/label "CVC")
          (ui-cvc-element {})))
      (dom/button {:onClick (fn [e]
                              ;; createToken is a calls to the low-level JS API
                              (-> (.createToken stripe #js {:type "card" :name name})
                                (.then (fn [result] (handle-result result)))
                                (.catch (fn [result] (handle-result result)))))}
        "Pay"))))

;; NOTE: The js library says you should call injectStripe(FormComponent) to get your wrapped component,
;; so use interop to do that:
(def ui-cc-form (interop/hoc-factory CCForm injectStripe))

(defsc Root [this {:keys [root/cc-form] :as props}]
  {:query         [{:root/cc-form (comp/get-query CCForm)}]
   :initial-state {:root/cc-form {}}}
  (dom/div
    (dom/h2 "Payment Form")
    ;; Stripe documentation says to wrap a stripe provider around the whole thing, and use the
    ;; HOC form.
    (ui-stripe-provider #js {:apiKey test-key}
      (ui-elements #js {}
        ;; NOTE: Remember to pass parent's `this` to the child when using an HOC factory.
        (ui-cc-form this cc-form)))))

4.13.2. How It Works

The property passing is relatively simple: pull the props apart at the correct time (and from the correct type) and pass them through as needed.

The other thing that has to happen is maintaining context from the Fulcro component parent as discussed earlier. The components namespace includes a with-parent-context function that can do this in any scenario where some subtree of Fulcro components will be rendered by some function outside of the normal Fulcro tree:

(defsc Component [this {:keys [child-props] :as props}]
  (dom/div {}
    (some-js-component {:content (fn []
                                   (comp/with-parent-context this
                                     (ui-fulcro-child child-props)))})))

If you look at the source of react-interop, you’ll see that both things are being done in order to make the target child component tolerant of being wrapped by a HOC pattern.

5. EQL — 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 queried and manipulated from the UI using the EDN Query Language (EQL). EQL is a subset of Datomic’s pull query syntax with extensions for unions and mutations. A query is a graph walk, so 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 portion of the 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 (comp/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/id (comp/get-query Person)
                  :place/id (comp/get-query Place)
                  :thing/id (comp/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 (comp/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:

{ :person-place-or-thing [:place/id 3]
  :place/id { 3 { :place/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, (comp/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:

{ :person-place-or-thing [[:person/id 1] [:place/id 3]]
  :person/id { 1 { :person/id 1 :person/name "Julie" }}
  :place/id { 3 { :place/id 3 :place/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 [ {:person/id 1 :person/name "Joe"} {:place/id 3 :place/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 :person/id) [:person/id (:person/id props)]
      (contains? props :place/id) [:place/id (:place/id props)]
      :else [:thing/id (:thing/id props)]))}
  ...)

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.

5.3.1. Demo – Using Unions as a UI Type Selector

The com.fulcrologic.fulcro.routing.legacy-ui-routers/defsc-router macro (2.6.18+) 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 defsc-router (e.g. with ID :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 the macro:

(defsc-router ItemDetail [this props]
  {:router-id :detail-router
   :ident (fn [] (item-ident props))
   :default-route PersonDetail
   :router-targets {:person/id PersonDetail
                    :place/id PlaceDetail
                    :thing/id ThingDetail}}
  (dom/div "Route not set"))

is roughly this (cleaned up from macro output):

(defsc ItemDetail-Union [this props]
  {:initial-state (fn [params] (comp/get-initial-state PersonDetail params)) ;; defaults to the first one listed
   :ident (fn [] (item-ident props))
   :query (fn [] {:person/id (comp/get-query PersonDetail),
                  :place/id (comp/get-query PlaceDetail),
                  :thing/id (comp/get-query ThingDetail)}}
   (let [page (first (comp/get-ident this))]
    (case page
     :person/id ((comp/factory PersonDetail) (comp/props this))
     :place/id ((comp/factory PlaceDetail) (comp/props this))
     :thing/id ((comp/factory ThingDetail) (comp/props this))
     (dom/div (str "Cannot route: Unknown Screen " page))))

(defsc ItemDetail [this props]
  {:initial-state (fn [params] {:com.fulcrologic.fulcro.routing.legacy-ui-routers/id :detail-router
                                :com.fulcrologic.fulcro.routing.legacy-ui-routers/current-route (comp/get-initial-state ItemDetail-Union params)})
   :ident (fn [] [:com.fulcrologic.fulcro.routing.legacy-ui-routers.routers/id :detail-router])
   :query [:com.fulcrologic.fulcro.routing.legacy-ui-routers/id {:fulcro.client.routing/current-route (comp/get-query ItemDetail-Union)}]}
  (let [computed (comp/get-computed this)
        props (:com.fulcrologic.fulcro.routing.legacy-ui-routers/current-route (comp/props this))
        props-with-computed (comp/computed props computed)]
   ((comp/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:


(ns book.queries.union-example-1
  (:require
    [com.fulcrologic.fulcro.dom :as dom :refer [div table td tr th tbody]]
    [com.fulcrologic.fulcro.routing.legacy-ui-routers :as r :refer [defsc-router]]
    [book.elements :as ele]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [taoensso.timbre :as log]))

(defn person? [props] (contains? props :person/id))
(defn place? [props] (contains? props :place/id))
(defn thing? [props] (contains? props :thing/id))

(defn item-ident
  "Generate an ident from a person, place, or thing."
  [props]
  (cond
    (person? props) [:person/id (:person/id props)]
    (place? props) [:place/id (:place/id props)]
    (thing? props) [:thing/id (:thing/id props)]
    :else (log/error "Cannot generate a valid ident. Invalid props." props)))

(defn item-key
  "Generate a distinct react key for a person, place, or thing"
  [props] (str (item-ident props)))

(defn make-person [id n] {:person/id id :person/name n})
(defn make-place [id n] {:place/id id :place/name n})
(defn make-thing [id n] {:thing/id id :thing/label n})

(defsc PersonDetail [this {:person/keys [id name] :as props}]
  ; defsc-router 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 defsc-router.
  {:ident         (fn [] (item-ident props))
   :query         [:person/id :person/name]
   :initial-state {:person/id :no-selection}}
  (dom/div
    (if (= id :no-selection)
      "Nothing selected"
      (str "Details about person " name))))

(defsc PlaceDetail [this {:place/keys [id name] :as props}]
  {:ident         (fn [] (item-ident props))
   :query         [:place/id :place/name]
   :initial-state {:place/id :no-selection}}
  (dom/div
    (if (= id :no-selection)
      "Nothing selected"
      (str "Details about place " name))))

(defsc ThingDetail [this {:thing/keys [id label] :as props}]
  {:ident         (fn [] (item-ident props))
   :query         [:thing/id :thing/label]
   :initial-state {:thing/id :no-selection}}
  (dom/div
    (if (= id :no-selection)
      "Nothing selected"
      (str "Details about thing " label))))

(defsc PersonListItem [this
                       {:person/keys [id name] :as props}
                       {:keys [onSelect] :as computed}]
  {:ident (fn [] (item-ident props))
   :query [:person/id :person/name]}
  (dom/li {:onClick #(onSelect (item-ident props))}
    (dom/a {} (str "Person " id " " name))))

(def ui-person (comp/factory PersonListItem {:keyfn item-key}))

(defsc PlaceListItem [this {:place/keys [id name] :as props} {:keys [onSelect] :as computed}]
  {:ident (fn [] (item-ident props))
   :query [:place/id :place/name]}
  (dom/li {:onClick #(onSelect (item-ident props))}
    (dom/a {} (str "Place " id " : " name))))

(def ui-place (comp/factory PlaceListItem {:keyfn item-key}))

(defsc ThingListItem [this {:thing/keys [id label] :as props} {:keys [onSelect] :as computed}]
  {:ident (fn [] (item-ident props))
   :query [:thing/id :thing/label]}
  (dom/li {:onClick #(onSelect (item-ident props))}
    (dom/a {} (str "Thing " id " : " label))))

(def ui-thing (comp/factory ThingListItem item-key))

(defsc-router ItemDetail [this props]
  {:router-id      :detail-router
   :ident          (fn [] (item-ident props))
   :default-route  PersonDetail
   :router-targets {:person/id PersonDetail
                    :place/id  PlaceDetail
                    :thing/id  ThingDetail}}
  (dom/div "No route"))

(def ui-item-detail (comp/factory ItemDetail))

(defsc ItemUnion [this props]
  {:ident (fn [] (item-ident props))
   :query (fn [] {:person/id (comp/get-query PersonListItem)
                  :place/id  (comp/get-query PlaceListItem)
                  :thing/id  (comp/get-query ThingListItem)})}
  (cond
    (person? props) (ui-person props)
    (place? props) (ui-place props)
    (thing? props) (ui-thing props)
    :else (dom/div "Invalid ident used in app state.")))

(def ui-item-union (comp/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/id :singleton])
   :query         [{:items (comp/get-query ItemUnion)}]}
  (dom/ul :.ui.list
    (map (fn [i] (ui-item-union (comp/computed i {:onSelect onSelect}))) items)))

(def ui-item-list (comp/factory ItemList))

(defsc Root [this {:keys [item-list item-detail]}]
  {:query         [{:item-list (comp/get-query ItemList)}
                   {:item-detail (comp/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   (comp/get-initial-state ItemList nil)
                             :item-detail (comp/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]]
                     (comp/transact! this `[(r/route-to {:handler :detail :route-params {:kind ~kind :id ~id}})]))]
    ; devcards, embed in iframe so we can use bootstrap css easily
    (div {:key "example-frame-key"}
      (dom/style ".boxed {border: 1px solid black}")
      (table :.ui.table {}
        (tbody
          (tr
            (th "Items")
            (th "Detail"))
          (tr
            (td (ui-item-list (comp/computed item-list {:onSelect showDetail})))
            (td (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]))

...
   (comp/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 (comp/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 ~(comp/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/id 1] ]

results in something like this:

(defsc Phone [this props]
  {:query [:id :phone/number]
   :ident [:phone/id :id]}
  ...)

(defsc Person [this props]
  {:query [:id {:person/phone (comp/get-query Phone)}]
   :ident [:person/id :id]}
  ...)

(defsc X [this props]
  {:query [ [:person/id 1] ] } ; query just for the ident
  (let [person (get props [:person/id 1]) ; NOT get-in. The key of the result is an ident
        ...
        ; person contains {:id 1 :person/phone [:phone/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/id 1] (comp/get-query Person)}]}
  (let [person (get props [:person/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 '_] (comp/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-switcher]}]
  {:query [{:locale-switcher (comp/get-query LocaleSwitcher)}]}
  (ui-locale-switcher locale-switcher)) ; data comes back nil!!!

The problem is that the query engine walks the database and query in tandem. When it sees a join (:locale-switcher 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-switcher]}]
  {:query [{:locale-switcher (comp/get-query LocaleSwitcher)}]
   :initial-state (fn [params] {:locale-switcher (comp/get-initial-state LocaleSwitcher)})} ; empty map from child
  (ui-locale-switcher locale-switcher)) ; 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 comp/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/make-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]} (comp/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

EQL 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:

Example 12. Recursive Demo 1

(ns book.queries.recursive-demo-1
  (:require [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
            [com.fulcrologic.fulcro.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 (comp/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/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 (comp/factory Person {:keyfn :db/id}))

(defsc Root [this {:keys [person-of-interest]}]
  {:initial-state {:person-of-interest {}}
   :query         [{:person-of-interest (comp/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):

Example 13. Recursive Demo 2

(ns book.queries.recursive-demo-2
  (:require [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
            [com.fulcrologic.fulcro.mutations :refer [defmutation]]
            [com.fulcrologic.fulcro.dom :as dom]))

(declare ui-person)

(defmutation make-older [{:keys [id]}]
  (action [{:keys [state]}]
    (swap! state update-in [:person/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/id :db/id]}
  (dom/div
    (dom/div "Name:" name)
    (dom/div "Age:" age
      (dom/button {:onClick
                   #(comp/transact! this `[(make-older {:id ~id})])} "Make Older"))
    (when spouse
      (dom/ul
        (dom/div "Spouse:" (ui-person spouse))))))

(def ui-person (comp/factory Person {:keyfn :db/id}))

(defsc Root [this {:keys [person-of-interest]}]
  {:initial-state {:person-of-interest {}}
   :query         [{:person-of-interest (comp/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.

Example 14. Recursive Demo 3

(ns book.queries.recursive-demo-3
  (:require [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
            [com.fulcrologic.fulcro.mutations :refer [defmutation]]
            [com.fulcrologic.fulcro.dom :as dom]))

(declare ui-person)

(defmutation make-older [{:keys [id]}]
  (action [{:keys [state]}]
    (swap! state update-in [:person/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/id :db/id]}
  (dom/div
    (dom/div "Name:" name)
    (dom/div "Age:" age
      (dom/button {:onClick
                   #(comp/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 (comp/computed spouse {:render-depth (inc render-depth)})))))))

(def ui-person (comp/factory Person {:keyfn :db/id}))

(defsc Root [this {:keys [person-of-interest]}]
  {:initial-state {:person-of-interest {}}
   :query         [{:person-of-interest (comp/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.

Example 15. Recursive Demo 4

(ns book.queries.recursive-demo-bullets
  (:require [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
            [com.fulcrologic.fulcro.mutations :as m :refer [defmutation]]
            [com.fulcrologic.fulcro.dom :as dom]
            [clojure.string :as str]))

(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 (comp/factory Item {:keyfn :db/id}))

(defsc ItemList [this {:keys [db/id list/items] :as props}]
  {:query [:db/id {:list/items (comp/get-query Item)}]
   :ident [:list/by-id :db/id]}
  (dom/ul
    (map ui-item items)))

(def ui-item-list (comp/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 (comp/get-query ItemList)}]}
  (dom/div
    (ui-item-list list)))

5.10. The AST

You can convert any expression of EQL 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 com.fulcrologic.fulcro.components/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]}] (comp/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!

(defsc Child [this props]
  {:initial-state (fn [params] {:child/id 1})
   :ident :child/id
   :query [:child/id]}
  ...)

(defsc Parent [this props]
  {:initial-state (fn [params] {:parent/id 1 {:parent/child (comp/get-initial-state Child)}})
   :ident :parent/id
   :query [:parent/id {:parent/child (comp/get-query Child)}]}
  ...)

...

Notice the nice symmetry here. The initial state is (usually) a map that represents (recursively) the entity and its children. The query is a vector that lists the "scalar" props, and joins as maps. So, in Child we have initial state and a query for its ID. In the parent we have a query for the parent’s data with a join to the child, and initial state mirrors that with an identical structure pulling the child state.

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 a 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 (comp/get-query Person) :place (comp/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:

(defsc Person [this props]
  {:initial-state (fn [{:keys [id name]}] {:id id :name name :type :person})
   ...)

(defsc PersonPlaceUnion [this props]
  {:initial-state (fn [p] (comp/get-initial-state Person {:id 1 :name "Joe"})) ; I can only put in one of them!
   :query {:person (comp/get-query Person) :place (comp/get-query Place)})
  ...)

(defsc Parent [this props]
  {:initial-state (fn [p] {:person-or-place (comp/get-initial-state PersonPlaceUnion)}))
   :query [{:person-or-place (comp/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.

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.

Example 16. Initial State

(ns book.demos.initial-app-state
  (:require
    [com.fulcrologic.fulcro.dom :as dom]
    [com.fulcrologic.fulcro.mutations :as m]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]))

(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 (comp/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 (comp/get-query ItemLabel)}]
   :initial-state (fn [{:keys [id label]}] {:id id :type :foo :label (comp/get-initial-state ItemLabel {:value label})})}
  (dom/div
    (dom/h2 "Foo")
    (ui-label label)))

(def ui-foo (comp/factory Foo {:keyfn :id}))

(defsc Bar [this {:keys [label]}]
  {:query         [:type :id {:label (comp/get-query ItemLabel)}]
   :initial-state (fn [{:keys [id label]}] {:id id :type :bar :label (comp/get-initial-state ItemLabel {:value label})})}
  (dom/div
    (dom/h2 "Bar")
    (ui-label label)))

(def ui-bar (comp/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] [(comp/get-initial-state Bar {:id 1 :label "A"}) (comp/get-initial-state Foo {:id 2 :label "B"}) (comp/get-initial-state Bar {:id 3 :label "C"})])
   :query         (fn [] {:foo (comp/get-query Foo) :bar (comp/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 (comp/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 (comp/get-initial-state ItemLabel {:value "Settings"})})
   :query         [:type :id {:label (comp/get-query ItemLabel)}]}
  (ui-label label))

(def ui-settings (comp/factory Settings {:keyfn :type}))

(defsc Main [this {:keys [label]}]
  {:initial-state (fn [params] {:type :main :id :singleton :label (comp/get-initial-state ItemLabel {:value "Main"})})
   :query         [:type :id {:label (comp/get-query ItemLabel)}]}
  (ui-label label))

(def ui-main (comp/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] (comp/get-initial-state Main nil))
   :query         (fn [] {:settings (comp/get-query Settings) :main (comp/get-query Main)})
   :ident         (fn [] [type id])}
  (case type
    :settings (ui-settings props)
    :main (ui-main props)
    (dom/p "NO PANE!")))

(def ui-panes (comp/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 (comp/get-initial-state PaneSwitcher nil)
                                :items (comp/get-initial-state ListItem nil)})
   :query         [{:items (comp/get-query ListItem)}
                   {:panes (comp/get-query PaneSwitcher)}]}
  (dom/div
    (dom/button {:onClick (fn [evt] (comp/transact! this '[(nav/settings)]))} "Go to settings")
    (dom/button {:onClick (fn [evt] (comp/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

Since our rendering via React is a pure function of state you think about your application as a sequence of states that are rendered in time (like a movie), and the initial state just generates the first state for that progression.

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.

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 fnorm/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 (comp/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 Interactions, 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 what happens to that initial tree when it is detected by Fulcro at startup (with the addition of the alternate union merging, described earlier).

Network interactions send a UI-based query (which 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.

It is the same thing when using websockets: 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) that can be used in conjunction with fnorm/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:

  • merge/merge-component! and merge/merge-component - merge new instances of a (possibly recursive) entity into the normalized database.

  • merge/merge! and merge/merge* - merge root-level out-of-band data into your application.

  • fnorm/tree→db - General utility for normalizing data via a query and chunk of data.

  • merge/integrate-ident* - A utility for adding an ident into existing to-one and to-many relations in your database. Can be used within mutations.

Note
The general merge operations all support the option :remove-missing?. This option defaults to false. When set to true it will cause the merge algorithms to exhibit the Fulcro remote load cleanups: if something is in the query but not the data then it will be removed from the state database. The deep merge used by the merge routines is not otherwise meant to "write over" existing versions of your entities, since they may have ui-only attributes that the incoming trees do not know about. See the section on remote result merging.

8. 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 middleware to handle plumbing for you.

However, 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:

  • The Network protocol is included (and extensible))

    • 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 can provide error handling for remote problems at a global or mutation-local level.

  • You can interact directly with the network result or use default result processing.

  • 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. You can also change the global rewrite logic to elide other local props from remote queries.

  • Normalization of a remote query result is automatic when using defmutation, but customizable.

    • Query results use intelligent overwrite for properties that are already present in the client database.

  • Any number of remotes can be defined

  • 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 or GraphQL APIs: See the Pathom library for ways of converting those services into custom client remotes that isolate those APIs to the networking layers of your client.

8.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)

  • Run a remote operation, which can optionally return a graph of data as a response.

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. This is true even for a server push (though in that case the client needs to know what implied query 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!

8.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 external data integration just starts at the "Response" step by manually providing the query:

New External Data + Query w/Idents --> Normalized Data --> Database Merge --> New Database

There is a primitive function merge/merge! function that implements this, so that you can simplify the picture to:

Tree of Data + Query --> merge! --> New Database

8.1.2. The Central Functions: transact! and merge!

The two core functions that allow you to trigger abstract operations or data merges externally (via your app) are:

comp/transact!

The central function for running abstract (possibly full-stack) changes in the application. Can be run with a component or app. If run with the app, will typically cause a root re-render.

merge/merge!

A function that can be run against the app to merge a tree of data via a UI query.

merge/merge-component!

Like merge!, but can be passed a component instead of a query.

merge/merge*

merge!, but in a mutation via swap!

These (and related/derived helpers) are the primary tools used to put new data into your client’s database.

8.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 (preferred).

  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 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 makes it happen. Fortunately, pathom makes this approach quite tractable and good.

In some cases, though, you’d like to morph it a bit on the client-side. We’ll show you an example of this as we explore the data fetch options.

8.1.4. Server Interaction Order

Fulcro will serialize requests unless you mark queries as parallel (an option you can specify with df/load!). Two different events that queue mutations or loads will be processed in the order of events. For example, if the user clicks on something and you trigger two loads during that event, then both of those may be combined (if they don’t conflict) and 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.

Fulcro will also try to reorder writes that are submitted alongside reads so that the writes appear on the server first, giving you the best possible chance that the reads get the most up-to-date versions of data.

In Summary:

  • Loads/transactions queued while "holding the UI thread" may be joined together in a single network request.

  • Remote writes go before reads (for a given processing event)

  • 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! or transact!.

8.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/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 (comp/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 (re)load the entity being edited to help prevent user confusion.

8.1.6. Defining Error Conditions

Both loads and mutations have a general concept of network errors that is customizable. The default detection of error conditions looks at the status-code of the network result. If it is 200, then it is considered "ok", and anything else is an error. You may want to customize this behavior to also look for other bits of content in the result, which can be specific to the remote and middleware definitions.

There is one global definition for error detection that is used for both loads and mutations. It can be overridden with the :remote-error? option when creating the application:

(def app (app/fulcro-app {:remote-error? (fn [result] ...)}))

8.2. 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. This is also true for any sort of side-effecting against your application state since the React lifecycle calls can be triggered for unexpected reasons (e.g. instability in a parent’s key can cause all children to unmount/remount).

9. Mutations

Mutations are the primary mechanism by which side effects happen in Fulcro. In fact, even the load API in the data-fetch namespace is implemented via the mutation system. Mutations are data expressions that represent a command to execute (locally and/or remotely). The remote operation of a mutation can optionally return a graph tree to be merged into your application’s state.

Mutations are submitted to Fulcro’s transaction processing system via comp/transact!. Technically the algorithm for processing transactions is pluggable, but this book will only cover the built-in version.

Mutations are known by their symbol and are dispatched to the internal multi-method com.fulcrologic.fulcro.mutations/mutate. To handle a mutation you can do two basic things: use defmethod to add a mutation, or use the macro defmutation to handle this for you.

The macro is highly recommended for all but the most advanced cases because:

  • Your editor/IDE can treat it like defn for better navigation and support.

  • It is where the default remote response handling is "plugged in".

  • It prevents certain implementation errors.

  • It isolates you from any potential internal changes to Fulcro.

  • It auto-namespaces the mutation to the namespace of declaration without the need to quote.

  • It adds support to use the mutation in a transaction without syntax quoting.

You might use the internal multi-method directly for more advanced things like:

  • You’re writing your own defmutation wrapper to change some global behavior for your application (note, though, that the built-in defmutation has an extension point).

  • You want to dynamically affect a mutation in some complex way at runtime.

Important
The internals of mutations changed drastically between version 2 and 3 of Fulcro. The low-level multimethod requires a different format for the return value in version 3, and mutations directly handle network results in 3.

9.1. Stages of Mutation

A mutation expresses the local effects, outgoing remote request(s), and network result processing for a given abstract operation in a Fulcro application.

The local effects are applied first, the remote requests are queued, and the responses are processed as they arrive.

The default behavior of the transaction processing system is to process all of the local effects of a given transaction first, then submit all of the network requests later. This "optimistic" processing mode can be toggled with an option to the top-level transact!:

(comp/transact! this [(f) (g) (h)] {:optimistic? false})

This would run the optimistic actions of f, wait for any network results, then run the optimistic actions of g, etc.

whereas

(comp/transact! this [(f) (g) (h)])

will cause the optimistic actions of f, g, and h to all run, and then submit the network actions.

In Fulcro 3 each stage of the mutation is implemented as a lambda, and each stage receives an environment that describes the state of the app (and even what the state looked like before the mutation started).

9.2. Using defmutation

Using defmutation looks like this:

(defmutation mutation-symbol
  "docstring"
  [params]
  (action [{:keys [state] :as env}]
    (swap! state ...))
  (rest-api [env] true)
  (remote [env] true))

It intentionally looks a bit like a function definition so that 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 sites. This makes development a lot easier.

The symbol itself does get interned into the local namespace as a function-like thing that will return the call itself as data (i.e. running (f {:x 1}) in a REPL returns the list (the.ns/f {:x 1})). This allows you to use the mutation unquoted in most situations (except where there are circular references and you cannot require it):

(ns app
  (:require [app.mutations :as am]))

...
   (comp/transact! this [(am/mutation-symbol {})])
Note
You can get the "quoteless" feature using the m/declare-mutation function as well.

9.2.1. The env

Each stage of the mutation will receive an environment parameter the has a number of things in it that can be used to accomplish the real work of the mutation:

:app

The application itself. Can be used to do things like call transact!, etc.

:component

Will be the component (if any) that ran the transaction

:ref

Will be the ident of the component (if any) that ran the transaction

:state

The atom holding the state database. Optimistic actions swap! on this.

:state-before-action

(in remotes) A map holding the value of the state database before the :action ran.

:ast

The AST of the mutation node that is running.

:com.fulcrologic.fulcro.algorithms.tx-processing/options

The map of options that was passed to transact! (if using the default built-in transaction processing).

:result

(passed to :result-action, which may subsequently call ok-action or error-action). The raw remote response. This raw result’s content will depend on the actual remote and middleware you’re using for that remote, but it will typically include things like the raw response and the network result status code.

:dispatch

A map from section name to the lambdas that represent the other sections of the mutation (e.g. :action in this map is the lambda representing the action section of the mutation). This can be used to cross-call sections, usually to invent your own purpose-built sections of mutations via overriding the default result-action.

9.2.2. The Sections

The sections of a mutation include:

action

The optimistic action. Run before any network traffic is submitted.

remote

Instructions for the remote side of the mutation. The names of these sections are user-defined by the list of remotes you define when you create the application. The return value of a remote determines what, if anything, is sent to that remote.

result-action

Optional. The default value of this can be overridden as an application parameter. The result-action section receives network results as the arrive. The default result action handles mutation return values and cross-calls ok-action or error-action.

ok-action

Optional. If you are using the default result-action, the ok-action is called when the remote result received is free from errors.

error-action

Optional. If you are using the default result-action, the error-action is called when the remote result was an error. See Defining Error Conditions.

9.3. 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

Take a moment to note that getting the "right" level of abstraction for these commands can give you great leverage. These are the units of optimistic and full-stack operation.

9.3.1. The remote Section(s)

You can define any number of remotes for your Fulcro application. The default remote is an HTTP remote that talks to the server of the page at /api through HTTP POST requests. Additional remotes are created and named by you.

The remote operation you want to run against any mutation just needs to share the name of that mutation:

(def app (app/fulcro-application {:remotes {:rest-api ...}}))

(defmutation some-thing [params]
  (rest-api [env] true))

A mutation is allowed to trigger itself against any number of simultaneous remotes.

The return value of a remote section must be one of:

  • true: Sends the exact expression that was sent via transact! to that remote.

  • An env containing an :ast: Sends the expression of the :ast key from that env. Useful with built-in helpers.

  • An EQL AST: Sends the expression of the AST node, which generally can be generated via eql/query→ast1.

  • false: The same as not listing the section.

The remote section is treated as a lambda, and it is call after the optimistic action has run. The env passed to it will include the :state-before-action which is a snapshot (i.e. normalized database map) of the state before the optimistic change occurred. This allows you to make the logic of your remote conditional on either the state as it is now (via :state) or before.

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 against `env`
(defmutation do-thing [params]
  (action [env] ...)
  ; send (do-thing {:x 1}) even if params are different than that on the client
  (remote [env] (m/with-params env {:x 1})) ; change the param list for the remote

or even change which mutation the server sees:

(defmutation some-mutation [ params]
  ;; Send a completely different mutation to the server
  (remote [env] (eql/query->ast1 `[(some-other-thing {:x 2})])))

9.3.2. Result Action

A key part of defmutation is that it emits a "default" result action that does a number of automatic operations for your mutation’s network results:

  • If there is an error, it calls your error-action section.

  • If there is not an error, it calls you ok-action section.

  • If it was configured with a global error handler, then that handler is called on errors (see the docstring of default-result-action).

  • It merges and targets the mutation’s remote return value (if you indicated there was a return value).

  • It resolves tempids.

Important
If you override the result-action section, then none of these default behaviors will happen unless you do them yourself. If you want to augment the default behavior, then you can manually call the m/default-result-action function as part of your own :result-action.

9.3.3. Plugging in a New Global Result Action

You can supply a lambda to use instead of default-result-action when you create your application:

(def app (app/fulcro-app {:default-result-action (fn [env] ...)}))
Warning
This setting will affect all mutations in your system, and should be used with care. If you choose not to call m/default-result-action within your custom function then mutation return values, tempid resolution, and ok/error action handlers will not work.

9.3.4. Optimistic vs. Pessimistic

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.

You may, of course, decide not to do anything in the action section (don’t provide one). In this case you’re operating in a pessimistic mode where the user won’t see a result until the network result has arrived and you :result-action has processed it.

9.3.5. Writing The Server Mutations

Server-side mutations in Fulcro arrive as an EQL transaction, and must be interpreted as such. Typically you’ll use an EQL parser built by Pathom. The pathom defmutation looks very similar to the Fulcro one:

;; CLIENT (Fulcro)
;; src/my_app/mutations.cljs
(ns my-app.mutations
  (:require [com.fulcrologic.fulcro.mutations :refer [defmutation]]))

(defmutation do-something [{:keys [p]}]
  (action [{:keys [state]}]
    (swap! state assoc :value p))
  (remote [env] true))
;; SERVER (Pathom)
;; src/my_app/mutations.clj
(ns my-app.mutations
  (:require
    [com.wsscode.pathom.connect :as pc]))

;; `env` can be augmented in parser setup to include things like database connection, etc.
(pc/defmutation do-something [env params]
  {}
  ...))

or even a CLJC file:

(ns my-app.mutations
  (:require
    [com.wsscode.pathom.connect :as pc]
    [com.fulcrologic.fulcro.mutations :refer [defmutation]])))

#?(:cljs
    (defmutation do-something [{:keys [p]}]
      (action [{:keys [state]}]
        (swap! state assoc :value p))
      (remote [env] true))

   :clj
   (pc/defmutation do-something [env params]
     ...))

Pathom lets you override the symbol on the server (via a ::pc/sym options) but it is still a good idea to use the same actual namespace (via clj/cljs pairs or just a CLJC file) to "co-locate" the mutations for easier code navigation. Your tools will likely readily navigate you to just one of them, so having them side-by-side in CLJC is often the easiest to maintain, even though it is a bit noisier to write.

#?(:clj (pc/defmutation do-thing ...)
   :cljs (defmutation do-thing ...))
Note
Fulcro’s mutations and Pathom’s mutations are both CLJC themselves. This allows for things like using your UI code (Fulcro’s defmutation) within the server for server-side rendering and building a client-layer EQL parser (Pathom’s defmutation) to process EQL on simulated remotes in the client. Be careful that you don’t accidentally use the wrong version of defmutation in a given context.

9.3.6. New item creation – Temporary IDs

Fulcro has a built in function tempid/tempid that will generate a unique temporary ID of a special type. 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!

Note
It is generally considered a "best practice" to generate and pass tempids as parameters to a mutation from the UI. This ensures that the same tempid flows through the local and all remotes.

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 can 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 used from the start (since that’s what the ID is in app state). When the create operation 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 correct server entity, as will the delete.

All the mutation has to do is return a map with the special key :tempids whose value is a map of tempid→realid. 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 [com.fulcrologic.fulcro.mutations :refer [defmutation]]))

(defmutation new-item [{:keys [tempid text]}]
  (action [{:keys [state]}]
    (swap! state assoc-in [:item/id tempid] {:db/id tempid :item/text text}))
  (remote [env] true))
;; server
;; src/my_app/mutations.clj
(ns my-app.mutations
  (:require
    [com.wsscode.pathom.connect :as pc]))

(pc/defmutation new-item [env {:keys [tempid text]}]
  {}
  (let [database-tempid (make-database-tempid)
        database-id (add-item-to-database database {:db/id database-tempid :item/text text})]
      {:tempids {tempid database-id}})))

Other mutation return values are covered in Mutation Return Values.

Avoiding Tempids

It is also possible to avoid tempids altogether. For example, if you make a unique attribute (column) on your server-side entities (rows) that holds a UUID. This has the following trade-offs:

  • It consumes a bit more space in your database.

  • The client can generate new IDs for entities without needing a server connection.

  • If you use GUUIDs, then they are globally unique. They are all you might need in your data-store to find a given entity.

  • Transactions that use a UUID for creation are easier to make idempotent (can be applied more than once without breaking things). Tempids are remapped by the server, so two creations in a row with the same tempid will create two different new rows. This is a useful property for systems where you’d like to enable auto-retry at the network layer without worrying about correctness.

  • It is impossible to tell if an entity on the client is persisted without additional tracking. Tempids give you an easy way to instantly "see" that something isn’t yet saved.

9.3.7. Actions After a Mutation

Fulcro will automatically queue remote reads after writes when they are submitted in the same thread interaction. The following code:

(comp/transact! this `[(f)])
(df/load this :thing Thing)
(comp/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 mutations followed by a read is to simply run a mutation and a load.

However, this is not sufficient in many cases because not only do you need to know what happened on the server, you may also need to make some local changes to your application state based on that result. Common examples abound, but include:

  • You want to navigate away from a form after you know it is saved.

  • You want to close a payment modal after a charge is processed.

  • You want to route to a screen based upon a result.

  • You want to load something else based upon a prior result.

There are several ways to accomplish these tasks in Fulcro 3: pessimistic transactions, and various approaches using the ok-action and error-action of a mutation.

9.3.8. Pessimistic Transactions

A pessimistic transaction is a transaction processing semantic where each mutation in a transaction is processed to completion (full stack) before the next mutation in that same transaction does anything. Pessimistic transactions have no interaction with each other (other than guarantees of submission order).

This means that you can code a mutations whose optimistic action checks the result of the prior mutation to decide what to do. This looks like this at the UI layer:

(comp/transact! this `[(charge-card) (route-after-charge)] {:optimistic? false})

and will run the full-stack operations of charge-card before the optimistic action of route-after-change.

This mechanism has the advantage of keeping all transactions "top-level". In other words this scheme makes the sequence of transactions apparent right at the point of call to the initial transact! and you can follow the sequence linearly right from there. Unfortunately, you still have to analyze the nested logic in the mutations to figure out what really happens. Fulcro 3 has some additional options for handling this.

9.3.9. Result Action for Pessimism

You are allowed to submit new transactions (via transact! or load!) in the ok-action and error-action sections of a mutation. Thus, instead of writing a top-level transaction you can simply queue the first action:

(comp/transact! this `[(charge-card)])

and then trigger the next step(s) in the mutation’s remote handling:

(defmutation charge-card [params]
  ...
  (ok-action [{:keys [app result] :as env}]
    ;; you can `transact!`, `swap!`, etc.
    (if (charge-ok? result)
      (comp/transact! app [(show-ok-screen)])
      (comp/transact! app [(show-error-screen)]))))

9.3.10. 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})

(pc/defresolver
  ...
  (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.3.11. Running Mutations in the Context of an Entity

If you submit a transaction and include an ident:

(comp/transact! app `[(f)] {:ref [:person/id 3]})

then the env of the mutation will include that explicit ref in the env. If you use transact! against a component instance then ref is automatically set to the ident of that instance. This means that you can write mutations that are "context sensitive" and can do things relative to the "invoking on-screen component instance". This is how the mutation helper functions m/set-string! and m/toggle! work.

9.3.12. 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.

The df/load! function simply runs a transact! on an internally-defined mutation with a special :result-action.

If you’d like to compose one or more loads into a mutation you can simply call them from any part of your mutation (even the remote sections, though technically it is probably better to use one of the action sections).

The basic pattern is:

(defmutation next-page [params]
  (action [{:keys [app state] :as env}]
    (swap! state ...) ; local optimistic db updates
    (df/load! app :prop Component {:remote :remote}) ; same basic args as `load`, except state atom instead of `this`
    (df/load! app :other Other) {:remote :other-remote})) ; as many as you need...

9.4. Server Mutation Return Values

We’ve talked a lot about the client-side perspective of mutations, what they send to the server, and where you can place code to process the response of a mutation; however, we have not yet completely addressed the actual interaction with the server.

A server mutation is always allowed to return a value. Normally the only value that makes sense is temporary ID remappings:

; server-side
(defmutation new-thing [params]
  (action [env]
    ...
    {:tempids {old-id new-id}}))

In some cases you’d like to return other details. However, remember that any arbitrary data merge on the client 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.

Of course, you can override result-action or ok-action and directly access the mutation return value in :result of env, but this is Fulcro: we can do better!

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.4.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 in EQL for a mutation join looks like this:

`[{(f) [:x]}]

but you never write them this way because a manual query doesn’t have component metadata information and cannot aid normalization. Instead, you write them just like you do when grabbing queries for anything else:

[{(f) (comp/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 [:item/id :item/value] then the server mutation could just return a simple map like so:

; server-side.
(pc/defmutation f [env params]
  {::pc/output [:item/id]}
   {:item/id 1})

The reason we’re only returning :item/id in this example is that Pathom resolvers understand how to resolve the return value queries as if they were normal queries. This means that as long as there are resolvers that can look up item details based on :item/id then the mutation need return nothing more, but the client can walk any arbitrary graph from there!

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).

9.4.2. Mutation Joins: Simpler Notation

Writing transact! using manual mutation joins in the UI is a quite visually noisy. It turns out there is a better way. If you remember: the remote section of client mutations can modify the remote EQL. 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) ~(comp/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:

; in the UI
(transact! this `[(f)])

; in your mutation definition (client-side)
(defmutation f [params]
  (action [env] ...)
  (remote [{:keys [ast state]}] (returning env 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.4.3. Targeting Return Values From Mutation Joins

If you deal with return type at the mutation then Fulcro gives you some additional bonus features: m/with-params and m/with-target. These can be used together to fully customize the outgoing request. Remember that m/returning just sets the return type. By default, you’re just returning some entity tree. The data gets normalized, but there is no further linkage into your app state.

You will sometimes need to change outgoing parameters on the mutation, and also to pepper idents around your app state to point at the return value.

These helpers all work on env and return env, and the remote sections accept env as a return value:

(defmutation f [params]
  (action [env] ...)
  (remote [env]
    (-> env
      (m/with-params (merge params {:api-token "cygnus x-1"}))
      (m/returning Item)
      (m/with-target (targeting/append-to [:path :to :field])))))

All special targets are supported:

(defmutation f [params]
  (action [env] ...)
  (remote [{:keys [ast state]}]
    (-> ast
      (m/returning state Item)
      (m/with-target
        (targeting/multiple-targets
          (targeting/append-to [:table 3 :field])
          (targeting/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:

    (defmutation trigger-error
      "This mutation causes an unstructured error on the server (just a map), but targets that value
       to the field `:error-message` on the component that invoked it."
      [_]
      (remote [{:keys [ref] :as env}]
        (m/with-target env (conj ref :error-message))))

    and the server simply returns a raw value (map is recommended)

    (pc/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 (targeting/append-to path-to-target)) ; where to put it
            (= :prepend where?) (m/with-target (targeting/prepend-to path-to-target))
            (= :replace-first where?) (m/with-target (targeting/replace-at (conj path-to-target 0)))))))

    The server mutation just returns the entity.


(ns book.demos.server-targeting-return-values-into-app-state
  (:require
    [com.fulcrologic.fulcro.dom :as dom]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.dom :as dom]
    [com.fulcrologic.fulcro.algorithms.data-targeting :as targeting]
    [com.fulcrologic.fulcro.mutations :as m :refer [defmutation]]
    [com.fulcrologic.fulcro.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 (targeting/append-to path-to-target)) ; where to put it
        (= :prepend where?) (m/with-target (targeting/prepend-to path-to-target))
        (= :replace-first where?) (m/with-target (targeting/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 (comp/factory Entity {:keyfn :db/id}))

(defsc Item [this {:keys [db/id error-message children]}]
  {:query         [:db/id :error-message {:children (comp/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] (comp/transact! this `[(trigger-error {})]))} "Trigger Error")
    (dom/h6 "Children")
    (map ui-entity children)
    (dom/button {:onClick (fn [evt] (comp/transact! this `[(create-entity {:where? :prepend :db/id ~(comp/tempid)})]))} "Prepend one!")
    (dom/button {:onClick (fn [evt] (comp/transact! this `[(create-entity {:where? :append :db/id ~(comp/tempid)})]))} "Append one!")
    (dom/button {:onClick (fn [evt] (comp/transact! this `[(create-entity {:where? :replace-first :db/id ~(comp/tempid)})]))} "Replace first one!")))

(def ui-item (comp/factory Item {:keyfn :db/id}))

(defsc Root [this {:keys [root/items]}]
  {:query         [{:root/items (comp/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.4.4. Augmenting the Ring Response

The middleware that comes with Fulcro can modify the outgoing response in arbitrary ways. By default it sets the content type to transit and the body to the EDN your mutation(s) have returned (as a map keyed by mutation symbol). However, there are times when you need to modify something about the low-level response itself (such as adding a cookie).

You can ask Fulcro’s middleware to do additional changes to the Ring response by adding metadata to your mutation response. The built-in function api-middleware/augment-response can be used to easily accomplish this.

(pc/defmutation f [env params]
  {}
  (api-middleware/augment-response
    {:normal :response}
    (fn [ring-response] modified-ring-response)))

For example, if you are using the api-middleware/wrap-api and Ring Session middleware you could cause user information to be stored in a server session store simply by returning this from a query on user:

...
(let [user (find-user ...)]
   (server/augment-response user (fn [resp] (update resp :session merge {:uid real-uid}))))

9.5. Additional Mutation Topics

9.5.1. Ownership and Mutations

A common case that causes some confusion is working with mutations that affect the ownership of some child in the UI. 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.


(ns book.demos.parent-child-ownership-relations
  (:require
    [com.fulcrologic.fulcro.dom :as dom]
    [com.fulcrologic.fulcro.mutations :as m]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.algorithms.merge :as merge]
    [taoensso.timbre :as log]))

; 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"}]}})

(m/defmutation delete-item
  "Mutation: Delete an item from a list"
  [{:keys [list-id id]}]
  (action [{:keys [state]}]
    (log/info "Deleting item" id "from list" list-id)
    (swap! state
      (fn [s]
        (-> s
          (update :items dissoc id)
          (merge/remove-ident* [:items id] [:lists list-id :list/items]))))))

(defsc Item [this
             {:keys [item/id item/label] :as props}
             {:keys [on-delete] :as computed}]              ;; (1)
  {: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 (comp/factory Item {:keyfn :item/id}))

(defsc ItemList [this {:list/keys [id name items]}]
  {:initial-state (fn [p] {:list/id    1
                           :list/name  "List 1"
                           :list/items [(comp/get-initial-state Item {:id 1 :label "A"})
                                        (comp/get-initial-state Item {:id 2 :label "B"})]})
   :query         [:list/id :list/name {:list/items (comp/get-query Item)}]
   :ident         [:lists :list/id]}
  (let [delete-item (fn [item-id] (comp/transact! this [(delete-item {:list-id id :id item-id})])) ;; (2)
        item-props  (fn [i] (comp/computed i {:on-delete delete-item}))]
    (dom/div
      (dom/h4 name)
      (dom/ul
        (map #(ui-list-item (item-props %)) items)))))

(def ui-list (comp/factory ItemList))

(defsc Root [this {:keys [main-list]}]
  {:initial-state (fn [p] {:main-list (comp/get-initial-state ItemList {})})
   :query         [{:main-list (comp/get-query ItemList)}]}
  (dom/div (ui-list main-list)))
  1. The item uses the computed callback

  2. The list creates a lambda that closes over the list id

9.5.2. Mutations Without Quoting or Circular References

There are times when you need to use a component in a mutation (for a merge, for example), but the component is also using mutations. You can easily end up with circular references in your code which Clojure does not allow. Of course since mutations are just data you can just quote the fully-qualified mutation name in the UI, but that is inconvenient because the names get rather long.

(comp/transact! this '[(some.namespace.that.is.long/f params)])

and it also leads to another problem: Did you notice the error in that last expression? params wasn’t un-quoted!:

(comp/transact! this '[(some.namespace.that.is.long/f ~params)])
Declare Your Mutations Elsewhere

The defmutation macro creates a version of itself that is actually a mutation declaration. You can use that same feature to put delcarations of your mutations in a namespace different from the mutation itself. Thus, you can code up a mutation "interface" namespace that won’t generate circular references with the UI:

(ns my-mutations
  (:require [com.fulcrologic.fulcro.mutations :as m :refer [declare-mutation]])

(declare-mutation boo 'real-ns.of.mutations/boo)

and now you can use this new boo in place of the real one without quoting:

(ns real-ui
  (:require
    [my-mutations :refer [boo]]
    ...))

...

   (comp/transact! this [(boo {:a "hello" :b 22})])

There’s really not much to the magic. boo becomes a function-like thing that just returns a list with the proper symbols and data in it:

(boo {:a "hello"}) => '(real-ns.of.mutations/boo {:a "hello"})

Additionally, since your real mutation is in a whole different namespace that is never required by the UI or the interface you are now free to require the UI in the mutation namespace if you need it for merges and other component-centric mutations concerns without causing circular references.

This requires an extra namespace and a little extra declaration work, but cleans up the UI and ns requires quite well.

Use the Component Registry

There is also a global component registry that can help with this problem. Any component that is (transitively) required in your application will appear in the registry. This makes it possible to track component classes in app state (since the name is serializable) and also look up a component class for merges from within mutations:

(defmutation f [params]
  (action [{:keys [state]}]
    (let [todo-list-class (comp/registry-key->class :com.company.app/TodoList)]
      (swap! comp/merge-component todo-list-class {:list/id ...}))))

9.5.3. IDE Integration

The syntax of defmutation and declare-mutation are both intentionally "shaped" like defn and def so that you can tell your IDE how to resolve them. In IntelliJ, you can do this by clicking on the macro name (e.g. defmutation) and waiting for the light bulb. Then select "Resolve as" and then defn for defmutation and def for delcare-mutation.

You may not need to do this for defmutation, as IntelliJ does have some predefined resolutions for Fulcro built-in.

9.5.4. 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 mutations are not functions, and wont' compose as functions.

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 → 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 → node'. These operations may modifiy a scalar or an edge to another node.

Mutation "Helpers"

Fulcro adopts the general notation of suffixing functions with * when they operate on a normalized state map (not atom). These functions are referred to as "mutation helpers" because they can easily be used with swap! against the mutation atom:

(swap! state merge/integrate-ident* ...)

and composed together as well:

(defn create-person*
  "Create a person entity and add it to the normalized database in state-map."
  [state-map id name]
  (assoc-in state-map [:person/id id] {:person/id id :person/name name}))

(defmutation create-friend
  "Mutation: create a new person and add them to the friends list"
  [{:person/keys [id name]}]
  (action [{:keys [state]}]
      (swap! state
        (fn [s]
          (-> s
            (create-person* id name)
            (merge/integrate-ident* [:person/id id] :append [:list/id :friends :list/people])))))))

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!:

(comp/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.

9.6. Advanced Mutation Topics

This section covers a number of additional mutation techniques that arise in more advanced situations.

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.

9.6.1. Using the Multimethod Directly

The multi-method approach just requires you to return a map containing all of the things that should be done. This looks somewhat similar to defmutation in structure:

(defmethod com.fulcrologic.fulcro.mutations/mutate `mutation-symbol [params]
   {:action (fn [env] ...)
    :result-action (fn [env] ...)
    :remote (fn [env] true)})
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.

The biggest trick to this technique is the :result-action key. Any remote results will be passed, unprocessed, to this handler and only this handler. The defmutation macro uses a default implementation of this action that merges results, checks for errors, calls other handlers, etc. If you make a multimethod, then you will not get any of that built-in behavior. If all you want to do is change the result action, then look into setting the :default-result-action option when you create your application.

If you would like your multimethod mutation to work like defmutation then you should do the following in your :result-action:

(defmethod m/mutate `mutation-symbol [params]
   {...
    :result-action (fn [env]
                     (when-let [default-action (lookup/app-algorithm (:app env) :default-result-action)]
                       (default-action env)))
    ...
    })

9.6.2. Using app Directly

When you set up your application it is best to save it in a global place so that you can use it outside of the normal flow of things (e.g. a websocket push notification):

(comp/transact! app ...)

9.6.3. Swapping on the State Atom?

You can technically tweak the database atom in the application directly, but that side-steps all of the internals of Fulcro including UI refresh. It can be handy in limited cases, but generally we recommend you use transact!.

Note
Fulcro 3 does not watch the state atom (though Inspect does).
(let [{::app/keys [state-atom]} app]
  (swap! state-atom ...))

If you do such a change then you are responsible for triggering a UI refresh (if you want one). You can schedule a render by calling (app/schedule-render! app).

9.6.4. Leveraging tree→db and db→tree

It is sometimes useful to convert a bit of the database to a tree for manipulation in a mutation, or the other way around. Remember that these utility functions exist as part of the toolbox.

9.6.5. 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 merge/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)

9.6.6. 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/id :id]
   :query [:id :data]})

(defsc TopQuery [t p]
  {:ident [:top/id :id]
   :query [:id {:subs (comp/get-query SubQuery)}]})

(ns book.tree-to-db
  (:require
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.dom :as dom]
    [devcards.util.edn-renderer :refer [html-edn]]
    [com.fulcrologic.fulcro.mutations :as m :refer [defmutation]]
    [com.fulcrologic.fulcro.algorithms.normalize :as fnorm]))

(defsc SubQuery [t p]
  {:ident [:sub/by-id :id]
   :query [:id :data]})

(defsc TopQuery [t p]
  {:ident [:top/by-id :id]
   :query [:id {:subs (comp/get-query SubQuery)}]})

(defmutation normalize-from-to-result [ignored-params]
  (action [{:keys [state]}]
    (let [result (fnorm/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 [] (comp/transact! this `[(normalize-from-to-result {})]))} "Normalized (Run tree->db)")
    (dom/button {:onClick (fn [] (comp/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 to work right. The ident functions are used to determine the table locations and idents to place into the normalized database!

9.6.7. Using com.fulcrologic.fulcro.components/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:

(merge/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.")

9.6.8. Using merge/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.

(merge/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 its UI shows up

(defsc Counter [this {:keys [counter/id counter/n] :as props} {:keys [onClick] :as computed}]
  {:query [:counter/id :counter/n]
   :ident [:counter/id :counter/id]}
   ...

(defn add-counter
  "Merge the given counter data into app state and append it to our list of counters"
  [app counter]
  (comp/merge-component! app Counter counter
    :append [:panels/by-kw :counter :counters]))

...
(defsc Root ...
  (let [app (comp/any->app this)]  ; another way to get to the app
    (dom/button {:onClick #(add-counter app {:counter/id 4 :counter/n 22})} "Simulate Data Import")
   ...

Here is the full running example with source:


(ns book.merge-component
  (:require
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.dom :as dom]
    [com.fulcrologic.fulcro.mutations :as m :refer [defmutation]]
    [com.fulcrologic.fulcro.data-fetch :as df]
    [com.fulcrologic.fulcro.algorithms.merge :as merge]))

(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 (comp/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 (comp/get-query Counter)}]
   :ident         (fn [] [:panels/by-kw :counter])}
  (let [click-callback (fn [id] (comp/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 (comp/computed % {:onClick click-callback})) counters))))

(def ui-counter-panel (comp/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..."
  [app counter]
  (merge/merge-component! app Counter counter
    :append [:panels/by-kw :counter :counters]))

(defsc Root [this {:keys [panel]}]
  {:query         [{:panel (comp/get-query CounterPanel)}]
   :initial-state {:panel {}}}
  (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 (comp/any->app this) {:counter/id 4 :counter/n 22})} "Simulate Data Import")
    (dom/hr)
    "Counters:"
    (ui-counter-panel panel)))

10. Loads

Most of your server API should be written with something like Pathom Connect. There are also ways to adapt to GraphQL servers, REST APIs, etc. This chapter will be strictly using a Pathom parser to supply data from the servers according to the following database schema and data:

10.1. Load Demo Database

(ns book.database
  (:require
    [cljs.spec.alpha :as s]
    [datascript.core :as d]
    [com.wsscode.pathom.connect :as pc]
    [com.wsscode.pathom.core :as p]
    [cljs.core.async :as async]
    clojure.pprint
    [taoensso.timbre :as log]))

(def schema {:person/id            {:db/unique :db.unique/identity}
             :person/spouse        {:db/cardinality :db.cardinality/one}
             :person/children      {:db/cardinality :db.cardinality/many}
             :person/addresses     {:db/cardinality :db.cardinality/many}
             :person/phone-numbers {:db/cardinality :db.cardinality/many}})

(defonce connection (d/create-conn schema))

(defn db [connection] (deref connection))

(defn seed-database []
  (d/transact! connection [{:db/id                1
                            :person/id            1
                            :person/age           45
                            :person/name          "Sally"
                            :person/spouse        2
                            :person/addresses     #{100}
                            :person/phone-numbers #{10 11}
                            :person/children      #{4 3}}

                           {:db/id        10
                            :phone/id     10
                            :phone/number "812-555-1212"
                            :phone/type   :work}

                           {:db/id        11
                            :phone/id     11
                            :phone/number "502-555-1212"
                            :phone/type   :home}

                           {:db/id        12
                            :phone/id     12
                            :phone/number "503-555-1212"
                            :phone/type   :work}

                           {:db/id                2
                            :person/id            2
                            :person/name          "Tom"
                            :person/age           48
                            :person/phone-numbers #{11 12}
                            :person/addresses     #{100}
                            :person/spouse        1
                            :person/children      #{3 4}}

                           {:db/id            3
                            :person/id        3
                            :person/age       17
                            :person/addresses #{100}
                            :person/name      "Amy"}

                           {:db/id            4
                            :person/id        4
                            :person/age       25
                            :person/addresses #{100}
                            :person/name      "Billy"
                            :person/children  #{5}}

                           {:db/id            5
                            :person/addresses #{100}
                            :person/id        5
                            :person/age       1
                            :person/name      "Billy Jr."}

                           {:db/id               100
                            :address/id          100
                            :address/street      "101 Main St"
                            :address/city        "Nowhere"
                            :address/state       "GA"
                            :address/postal-code "99999"}]))

(pc/defresolver person-resolver [{:keys [connection]} {:person/keys [id]}]
  {::pc/input  #{:person/id}
   ::pc/output [:person/name
                :person/age
                {:person/addresses [:address/id]}
                {:person/phone-numbers [:phone/id]}
                {:person/spouse [:person/id]}
                {:person/children [:person/id]}]}
  (d/pull (db connection)
    [:person/name
     :person/age
     {:person/addresses [:address/id]}
     {:person/phone-numbers [:phone/id]}
     {:person/spouse [:person/id]}
     {:person/children [:person/id]}]
    id))

(pc/defresolver all-people-resolver [{:keys [connection]} _]
  {::pc/output [{:all-people [:person/id]}]}
  {:all-people (mapv
                 (fn [id] {:person/id id})
                 (d/q '[:find [?e ...]
                        :where
                        [?e :person/id]]
                   (db connection)))})

(pc/defresolver address-resolver [{::p/keys [parent-query]
                                   :keys    [connection] :as env} {:address/keys [id]}]
  {::pc/input  #{:address/id}
   ::pc/output [:address/street :address/state :address/city :address/postal-code]}
  (d/pull (db connection) parent-query id))

(pc/defresolver phone-resolver [{::p/keys [parent-query]
                                 :keys    [connection] :as env} {:phone/keys [id]}]
  {::pc/input  #{:phone/id}
   ::pc/output [:phone/number :phone/type]}
  (d/pull (db connection) parent-query id))

;; Allow a person ID to resolve to (any) one of their addresses
(pc/defresolver default-address-resolver [{:keys [connection]} {:person/keys [id]}]
  {::pc/input  #{:person/id}
   ::pc/output [:address/id]}
  (let [address-id (d/q '[:find ?a .
                          :in $ ?pid
                          :where
                          [?pid :person/addresses ?addr]
                          [?addr :address/id ?a]]
                     (db connection) id)]
    (when address-id
      {:address/id address-id})))

;; Resolve :server/time-ms anywhere in a query. Allows us to timestamp a result.
(pc/defresolver time-resolver [_ _]
  {::pc/output [:server/time-ms]}
  {:server/time-ms (inst-ms (js/Date.))})

(def general-resolvers [phone-resolver address-resolver person-resolver
                        default-address-resolver
                        all-people-resolver time-resolver])

This database and small set of resolvers can resolve quite complex queries:

(parse
  [{:all-people [:person/name
                 :person/age
                 {:person/phone-numbers [:phone/number :phone/type]}
                 {:person/spouse [:person/name]}
                 {:person/children [:person/name {:person/spouse [:person/name]}]}
                 {:person/addresses [:address/street]}
                 {:person/addresses [:address/id :address/street :address/state
                                     :address/city :address/postal-code]}]}])
;; result
{:all-people
 [{:person/spouse {:person/name "Tom"},
   :person/age 45,
   :person/name "Sally",
   :person/children [{:person/name "Amy"} {:person/name "Billy"}],
   :person/addresses [{:address/street "101 Main St"}],
   :person/phone-numbers
   [{:phone/type :work, :phone/number "812-555-1212"}
    {:phone/type :home, :phone/number "502-555-1212"}]}
  {:person/spouse {:person/name "Sally"},
   :person/age 48,
   :person/name "Tom",
   :person/children [{:person/name "Amy"} {:person/name "Billy"}],
   :person/addresses [{:address/street "101 Main St"}],
   :person/phone-numbers
   [{:phone/type :home, :phone/number "502-555-1212"}
    {:phone/type :work, :phone/number "503-555-1212"}]}
  {:person/age 17,
   :person/name "Amy",
   :person/addresses [{:address/street "101 Main St"}]}
  {:person/age 25,
   :person/name "Billy",
   :person/children [{:person/name "Billy Jr."}],
   :person/addresses [{:address/street "101 Main St"}]}
  {:person/age 1,
   :person/name "Billy Jr.",
   :person/addresses [{:address/street "101 Main St"}]}]}

10.1.1. Playing with the Database

If you’re running the live book with Inspect installed you can leverage a really nice feature of that tool: When you use Pathom as your parser it is able to send an autocomplete index to your tools, which allows you play with the parser on your server to see what it can do!

Try the following:

  1. Go to the Loading Data Basics example.

  2. Open Fulcro Inspect

  3. Focus the inspector on that example (Use the "Focus Inspector" button).

  4. In Inspect, choose the Query sub-tab.

  5. In the while bar just below the tabs there is a round grey button and a "Run query". Button. Press the grey button. This will load the indexes from the Pathom server. The button should turn green.

  6. Place your cursor in the left half of the query tool. This is where you type in queries. Autocomplete should be working (try typing : inside of the vector)!

Example 21. Server Query Autocomplete
Server Query Autocomplete

If you write a legal query and press "Run query", it will contact the server and give you the result.

Example 22. Server Query Result
Server Query Autocomplete

Some quick keyboard shortcuts (OSX):

  • CMD-J: Turn the keyword under the cursor into a query join (wrap it in a map with a subquery vector).

  • CMD-ENTER: Run the query

10.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’ve invented a root-level keyword, :all-people that can query the database for every entity that has a :person/id. In Fulcro component queries that would look like this:

[{:all-people (comp/get-query Person)}]

What we’d like to see then from the server is a return value with the shape of the query:

{ :all-friends [ {:person/id 1 ...} {:person/id 2 ...} ...] }

If we combined that query with the tree result and were to manually call merge/merge! then we’d end up with this in our client database:

{ :all-friends [ [:person/id 1] [:person/id 2] ...]
  :person/id { 1 { :person/id 1 ...}
               2 { :person/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 [com.fulcrologic.fulcro.data-fetch :as df]))

...

  (df/load! comp-or-app :all-friends Person)

The first argument is the component (i.e. this) or the application. The next argument is a keyword or an ident, and the next argument defines the type of thing (to-one or many) that will be the next "level" of the query.

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 in this scenario would appear in your root node).

This is sometimes what you want, but often you really want that data loaded at some other spot in your graph.

10.2.1. Basic Targeting

When you want the data from load to appear at some other edge in your database you can simply tell load the normalized path (which is rarely more than 3 elements) to the location you’d like to place the result. For example, if you had list components with IDs, then this:

  (df/load! comp-or-app :all-friends Person {:target [:list/id :people :list/people]})

might load two people (with IDs 1 and 2) and would result in your database containing:

{:person/id {1 {...} 2 {...}} ; normalized people
 :list/id   {:people {:list/people [[:person/id 1] [:person/id 2]]

10.2.2. Ident-based Loading

You can also simply load (and normalize the sub-tree) a given entity with:

  (df/load! this [:person/id 1] Person)

which will simply update the entries in the tables, but won’t add any edges to the graph. You can use targeting with these loads in order to that ident in the graph at arbitrary locations:

  (df/load! this [:person/id 1] Person {:target [:current-user]})

which will result in something like this:

{:person/id {1 {:person/id 1 :person/name "Joe"}}
 :current-user [:person/id 1]}

10.2.3. A Loading Example

The two general loading techniques are shown in the following example which uses our database described in Load Demo Database. You should Focus the Inspect tool on this example’s database so you can see what is happening (via the DB, Transaction, and Network sub-tabs).

Pressing the "Load People" button will load all of the people. Once they are loaded you can refresh any one of them. A special resolver on the server for :server/time-ms allows us to timestamp the entities with the server’s time so we can see the refresh taking effect.


(ns book.demos.loading-data-basics
  (:require
    [com.fulcrologic.fulcro.data-fetch :as df]
    [book.demos.util :refer [now]]
    [com.fulcrologic.fulcro.mutations :as m]
    [com.fulcrologic.fulcro.dom :as dom]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.data-fetch :as df]))

(defsc Person [this {:person/keys [id name age] :server/keys [time-ms] :as props}]
  {:query [:person/id :person/name :person/age :server/time-ms]
   :ident :person/id}
  (dom/li
    (str name " (last queried at " time-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 (comp/get-ident this) Person))} "Update")))

(def ui-person (comp/factory Person {:keyfn :db/id}))

(defsc People [this {:list/keys [people]}]
  {:initial-state {:list/id :param/id :list/people []}
   :query         [:list/id {:list/people (comp/get-query Person)}]
   :ident         :list/id}
  (dom/ul
    (map ui-person people)))

(def ui-people (comp/factory People {:keyfn :people/kind}))

(defsc Root [this {:root/keys [list]}]
  {:initial-state (fn [_] {:root/list (comp/get-initial-state People {:id :people})})
   :query         [{:root/list (comp/get-query People)}]}
  (dom/div
    (dom/button
      {:onClick (fn []
                  (df/load! this :all-people Person {:target [:list/id :people :list/people]}))}
      "Load People")
    (dom/h4 "People in Database")
    (ui-people list)))

10.2.4. Targeting Loads

We covered a little bit about targeting in Basic Targeting. It turns out that the targeting system gives you a bit more power than what we showed there. This section covers targeting in a bit more detail.

Simple Targeting

The simplest targeting is to put a "root" result somewhere besides root, or to add an additional ident (edge) when you’ve loaded something by ident:

(df/load comp :all-people Person {:target [:list/id :people :list/people]})

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.

This is equivalent to the full replacement of the edge. Anything that existed at the target path will be overridden by an ident (to-one) or a vector of idents (to-many). Remember that the cardinality is determined based on the actual result (not some kind of client-side schema).

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:

(df/load comp [:person/id 42] Person {:target (targeting/append-to [:list/id :people :list/people])})

The append-to function 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 in the list of idents.

There is also prepend-to.

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 (targeting/multiple-targets
                                              (targeting/append-to [:screens/by-type :friends :friends/list])})
                                              [:other-spot]
                                              (targeting/prepend-to [:screens/by-type :summary :friends]))})

Note that multiple-targets can use plain target vectors (replacement) or any of the special wrappers.

10.2.5. 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, and the parameters to pass to it.

:remote

The name of the remote you want to load from.

: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 your configured remote-error? returns true on the network result.

: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.

:update-query

A function that will take the component original query and should return a query that will be sent to the remote

:params

A map. If supplied the params will appear as the params of the query on the server.

See the docstring of the load! function for more.

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 a large value.

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).


(ns book.demos.parallel-vs-sequential-loading
  (:require
    [com.fulcrologic.fulcro.data-fetch :as df]
    [com.fulcrologic.fulcro.dom :as dom]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.wsscode.pathom.connect :as pc]
    [taoensso.timbre :as log]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SERVER:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(pc/defresolver long-query-resolver [_ _]
  {::pc/output [:background/long-query]}
  {:background/long-query 42})

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CLIENT:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defsc Child [this {:keys [id name background/long-query] :as props}]
  {:query (fn [] [:id :name :background/long-query [df/marker-table '_]])
   :ident [:background.child/by-id :id]}
  (let [status (get-in props [df/marker-table [:fetching id]])]
    (dom/div {:style {:display "inline" :float "left" :width "200px"}}
      (dom/button {:onClick #(df/load-field! this :background/long-query {:parallel true
                                                                          :marker   [:fetching id]})} "Load stuff parallel")
      (dom/button {:onClick #(df/load-field! this :background/long-query {:marker [:fetching id]})} "Load stuff sequential")
      (dom/div
        name
        (if (df/loading? status)
          (dom/span "Loading...")
          (dom/span long-query))))))

(def ui-child (comp/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 (comp/get-query Child)}]}
  (dom/div
    (mapv ui-child children)
    (dom/br {:style {:clear "both"}}) (dom/br)))

10.3. Tracking Specific Loads

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. Fulcro provides a pre-built facility in the load API that can put markers into a normalized table, allowing you to query for them in the components that need to show the activity of interest.

Load markers are normalized into a well-known table via an ID that you choose. You can use just about any value (except a boolean) for the id. This has the following behavior:

  1. Load markers are placed the top-level table (the var com.fulcrologic.fulcro.data-fetch/marker-table holds the table name), using your marker value as their ID.

  2. You can explicitly query for them using an ident-based join.

10.3.1. Working with Normalized Load Markers

The steps are rather simple: Include the :marker parameter with load, and add a query for the load marker on a UI component.

(defsc SomeComponent [this props]
  {:query [:data :prop2 :other [df/marker-table :marker-id]]} ; an ident in queries pulls in an entire entity
  ;; important: `get`, NOT `get-in`.
  (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/loading? marker) - Returns true if the item is still in progress (of any kind)

  • (df/failed? marker) - Returns true if the item failed, according to your definition of an error.

The marker will disappear from the table when network activity completes successfully, but will remain in place on failures.

The rendering is up to you, but that is really all there is to it.

Notes on Marker IDs

Marker IDs can be any value that supports equivalence, including data structures. You can also pull the entire load marker table into a component with a link query:

(defsc Component [_ props]
  {:query [... [df/marker-table '_] ...]}
  (let [table (get props df/marker-table)
        marker (get table id-of-marker)]
    ...))

Beware, though, that querying for the entire marker table will tend to "over render" the given component (every time any load marker changes).

10.4. Initializing Loaded Items

There are two main ways to augment the items you load. One is using post-mutations, and the other is Pre-merge.

10.4.1. 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 may not be possible if you’re using something less powerful than Pathom.

Another way of handling this is to let the client morph the incoming data into a shape that makes more sense to the 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

If you are taking the approach we’ve described then you have a problem: None of your UI matches the query that we need for the server! It turns out that it is perfectly legal (and recommended) to use defsc to define a graph query (and normalization) for something like this that doesn’t exactly exist on your UI.

Simply code your (nested) queries using defsc, and skip writing the body:

(defsc ItemQuery [this props]
  {:query [:db/id :item/name {:item/category (comp/get-query CategoryQuery)}]
   :ident [:items/id :db/id]})

and now you can load (and normalize) the data that the server knows how to provide, and then use a post-mutation to re-shape it to suit your needs:

(df/load! this :all-items ItemQuery {:post-mutation `group-items})
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 should interact with it and view the database in Inspect to A/B compare the before/after state.

Example 25. Morphing Example

(ns book.server.morphing-example
  (:require
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.dom :as dom]
    [book.macros :refer [defexample]]
    [com.fulcrologic.fulcro.mutations :refer [defmutation]]
    [com.fulcrologic.fulcro.algorithms.normalize :as fnorm]
    [com.fulcrologic.fulcro.algorithms.merge :as merge]))

(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 (comp/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 (comp/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 (comp/factory ToolbarItem {:keyfn :db/id}))

(defsc ToolbarCategory [this {:keys [category/name category/items]}]
  {:query [:db/id :category/name {:category/items (comp/get-query ToolbarItem)}]
   :ident [:categories/by-id :db/id]}
  (dom/li
    name
    (dom/ul
      (map ui-toolbar-item items))))

(def ui-toolbar-category (comp/factory ToolbarCategory {:keyfn :db/id}))

(defmutation group-items-reset [params]
  (action [{:keys [app state]}]
    (swap! state (fn [s]
                   (-> s
                     (dissoc :categories/by-id :toolbar/categories)
                     (merge/merge* component-query sample-server-response))))))

(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 []) (comp/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 #(comp/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 (comp/get-query ToolbarCategory)}]}
  (dom/div
    (dom/button {:onClick #(comp/transact! this `[(group-items {})])} "Trigger Post Mutation")
    (dom/button {:onClick #(comp/transact! this `[(group-items-reset {})])} "Reset")
    (dom/ul
      (map ui-toolbar-category categories))))

(defexample "Morphing Data" Toolbar "morphing-example" :initial-db (fnorm/tree->db component-query sample-server-response true))

10.5. Pre-Merge

Pre merge offers hook to manipulate data entering your Fulcro app at component level.

During the lifetime of a Fulcro application data will enter the system:

  1. During app initialization

  2. When loaded from a remote

  3. By being added via mutations

In the first case the data comes from the component initial state or user-provided initial state. Such pre-supplied data usually includes both domain and ui-centric data.

In the second case the data comes from a remote and contains only domain data, however, the UI might still need to do some type of initialization to add ui-centric props for this in the local database.

Post-mutations are a little limiting in this case because they are not component-centric: they are not co-located with the components, and may have to deal with an entire sub-tree all at once. Pre hooks decomposes this logic to the component level making things a bit simpler in many cases.

To illustrate these issues we are going to write a small app that has some countdown buttons, the idea is that each counter will go down each time it is clicked until it reaches zero. The important detail will be how we can handle the initial counter value.

(defsc Countdown [this {::keys   [counter-label]
                        :ui/keys [count]}]
  {:ident [::counter-id ::counter-id]
   :query [::counter-id ::counter-label :ui/count]}
  (dom/div
    (dom/h4 counter-label)
    (let [done? (zero? count)]
      (dom/button {:disabled done?
                   :onClick  #(m/set-value! this :ui/count (dec count))}
        (if done? "Done!" (str count))))))

The :ui/count is a ui-only concern that must start with some value. We will arbitrarily choose 5 as a start. Whatever it is we need to initialize it when a new instance of a Countdown enters the system.

First, we’ll use this post-mutation to initialize this ui property:

(m/defmutation initialize-counter [{::keys [counter-id]}]
  (action [{:keys [state]}]
    (swap! state update-in [::counter-id counter-id] #(merge {:ui/count 5} %))))

And use it via this call to load:

(df/load this [::counter-id 1] Countdown
  {:target               [:counter]
   :post-mutation        `initialize-counter
   :post-mutation-params {::counter-id 1}})

Here is a running demo:


(ns book.demos.pre-merge.post-mutation-countdown
  (:require
    [com.fulcrologic.fulcro.data-fetch :as df]
    [book.demos.util :refer [now]]
    [com.fulcrologic.fulcro.mutations :as m]
    [com.fulcrologic.fulcro.dom :as dom]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.data-fetch :as df]
    [com.wsscode.pathom.connect :as pc]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SERVER:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def all-counters
  [{::counter-id 1 ::counter-label "A"}])

(pc/defresolver counter-resolver [env {::keys [counter-id]}]
  {::pc/input  #{::counter-id}
   ::pc/output [::counter-id ::counter-label]}
  (let [{:keys [id]} (-> env :ast :params)]
    (first (filter #(= id (::counter-id %)) all-counters))))

(def resolvers [counter-resolver])

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CLIENT:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(m/defmutation initialize-counter [{::keys [counter-id]}]
  (action [{:keys [state]}]
    (swap! state update-in [::counter-id counter-id] #(merge {:ui/count 5} %))))

(defsc Countdown [this {::keys   [counter-label]
                        :ui/keys [count]}]
  {:ident [::counter-id ::counter-id]
   :query [::counter-id ::counter-label :ui/count]}
  (dom/div
    (dom/h4 counter-label)
    (let [done? (zero? count)]
      (dom/button {:disabled done?
                   :onClick  #(m/set-value! this :ui/count (dec count))}
        (if done? "Done!" (str count))))))

(def ui-countdown (comp/factory Countdown {:keyfn ::counter-id}))

(defsc Root [this {:keys [counter]}]
  {:initial-state (fn [_] {})
   :query         [{:counter (comp/get-query Countdown)}]}
  (dom/div
    (dom/h3 "Counters")
    (if (seq counter)
      (ui-countdown counter)
      (dom/button {:onClick #(df/load! this [::counter-id 1] Countdown
                               {:target               [:counter]
                                :post-mutation        `initialize-counter
                                :post-mutation-params {::counter-id 1}})}
        "Load one counter"))))

(defn initialize
  "To be used in :started-callback to pre-load things."
  [app])

Now let’s see in case of a to-many, this is what our new mutation looks like:

(m/defmutation initialize-counters [_]
  (action [{:keys [state]}]
    (swap! state
      (fn [state]
        (reduce
          (fn [state ref]
            (update-in state ref #(merge {:ui/count 5} %)))
          state
          (get state ::all-counters))))))

(ns book.demos.pre-merge.post-mutation-countdown-many
  (:require
    [com.fulcrologic.fulcro.data-fetch :as df]
    [book.demos.util :refer [now]]
    [com.fulcrologic.fulcro.mutations :as m]
    [com.fulcrologic.fulcro.dom :as dom]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.wsscode.pathom.connect :as pc]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SERVER:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def all-counters
  [{::counter-id 1 ::counter-label "A"}
   {::counter-id 2 ::counter-label "B"}
   {::counter-id 3 ::counter-label "C"}
   {::counter-id 4 ::counter-label "D"}])

(pc/defresolver counter-resolver [env _]
  {::pc/output [{::all-counters [::counter-id ::counter-label]}]}
  {::all-counters all-counters})

(def resolvers [counter-resolver])

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CLIENT:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(m/defmutation initialize-counters [_]
  (action [{:keys [state]}]
    (swap! state
      (fn [state]
        (reduce
          (fn [state ref]
            (update-in state ref #(merge {:ui/count 5} %)))
          state
          (get state ::all-counters))))))

(defsc Countdown [this {::keys   [counter-label]
                        :ui/keys [count]}]
  {:ident [::counter-id ::counter-id]
   :query [::counter-id ::counter-label :ui/count]}
  (dom/div
    (dom/h4 counter-label)
    (let [done? (zero? count)]
      (dom/button {:disabled done?
                   :onClick  #(m/set-value! this :ui/count (dec count))}
        (if done? "Done!" (str count))))))

(def ui-countdown (comp/factory Countdown {:keyfn ::counter-id}))

(defsc Root [this {::keys [all-counters]}]
  {:initial-state (fn [_] {})
   :query         [{::all-counters (comp/get-query Countdown)}]}
  (dom/div
    (dom/h3 "Counters")
    (if (seq all-counters)
      (dom/div {:style {:display "flex" :alignItems "center" :justifyContent "space-between"}}
        (mapv ui-countdown all-counters))
      (dom/button {:onClick #(df/load! this ::all-counters Countdown
                               {:post-mutation `initialize-counters})}
        "Load many counters"))))

As you can see, adding a single depth to the query can yield a fair amount of extra code to hook things up, and as you app grows each join depth will add more post-mutation code to compose over. Solving this composition issue is the main motivation behind pre-merge.

10.5.1. Pre-merge Basics

Pre merge addresses these issues by providing a component level hook that allows the additional merge logic to be localized to the component of interest.

This is how we would set up our Countdown component using :pre-merge:

(defsc Countdown [this {::keys   [counter-label]
                        :ui/keys [count]}]
  {:ident     [::counter-id ::counter-id]
   :query     [::counter-id ::counter-label :ui/count]
   :pre-merge (fn [{:keys [current-normalized data-tree]}]
                ; (1)
                (merge
                  {:ui/count 5} ; (2)
                  current-normalized ; (3)
                  data-tree))} ; (4)
  (dom/div
    (dom/h4 counter-label)
    (let [done? (zero? count)]
      (dom/button {:disabled done?
                   :onClick  #(m/set-value! this :ui/count (dec count))}
        (if done? "Done!" (str count))))))
  1. This merge setup works for most of the cases

  2. We start the merge with a map containing defaults

  3. Then we merge with the data that is already there for this component (may be nil if component doesn’t have previous data)

  4. Finally merge in the incoming data tree, since it’s the last it will win over any of the previous

The :pre-merge lambda receives a single map containing the following keys:

  • :data-tree - the new data tree entering the database

  • :current-normalized - the current entity value (if any), comes normalized from the db (as in (get-in state ref))

  • :state-map - the current app state map (you may use to lookup refs).

  • :query - the query used to do this request (user may have modified the original using :focus, :without or :update-query during the load call

With this setup we can get rid of the post-mutation. The cleaned up examples follow.

Example 28. Pre merge

(ns book.demos.pre-merge.countdown
  (:require
    [com.fulcrologic.fulcro.data-fetch :as df]
    [book.demos.util :refer [now]]
    [com.fulcrologic.fulcro.mutations :as m]
    [com.fulcrologic.fulcro.dom :as dom]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.wsscode.pathom.connect :as pc]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SERVER:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def all-counters
  [{::counter-id 1 ::counter-label "A"}])

(pc/defresolver counter-resolver [env {::keys [counter-id]}]
  {::pc/input  #{::counter-id}
   ::pc/output [::counter-id ::counter-label]}
  (let [{:keys [id]} (-> env :ast :params)]
    (first (filter #(= id (::counter-id %)) all-counters))))

(def resolvers [counter-resolver])

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CLIENT:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defsc Countdown [this {::keys   [counter-label]
                        :ui/keys [count]}]
  {:ident     [::counter-id ::counter-id]
   :query     [::counter-id ::counter-label :ui/count]
   :pre-merge (fn [{:keys [current-normalized data-tree]}]
                (merge
                  {:ui/count 5}
                  current-normalized
                  data-tree))}
  (dom/div
    (dom/h4 counter-label)
    (let [done? (zero? count)]
      (dom/button {:disabled done?
                   :onClick  #(m/set-value! this :ui/count (dec count))}
        (if done? "Done!" (str count))))))

(def ui-countdown (comp/factory Countdown {:keyfn ::counter-id}))

(defsc Root [this {:keys [counter]}]
  {:initial-state (fn [_] {})
   :query         [{:counter (comp/get-query Countdown)}]}
  (dom/div
    (dom/h3 "Counters")
    (if (seq counter)
      (ui-countdown counter)
      (dom/button {:onClick #(df/load! this [::counter-id 1] Countdown {:target [:counter]})}
        "Load one counter"))))

(defn initialize
  "To be used in :started-callback to pre-load things."
  [app])

(ns book.demos.pre-merge.countdown-many
  (:require
    [book.demos.util :refer [now]]
    [com.fulcrologic.fulcro.mutations :as m]
    [com.fulcrologic.fulcro.dom :as dom]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.data-fetch :as df]
    [com.wsscode.pathom.connect :as pc]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SERVER:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def all-counters
  [{::counter-id 1 ::counter-label "A"}
   {::counter-id 2 ::counter-label "B"}
   {::counter-id 3 ::counter-label "C"}
   {::counter-id 4 ::counter-label "D"}])

(pc/defresolver counter-resolver [env _]
  {::pc/output [{::all-counters [::counter-id ::counter-label]}]}
  {::all-counters all-counters})

(def resolvers [counter-resolver])

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CLIENT:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defsc Countdown [this {::keys   [counter-label]
                        :ui/keys [count]}]
  {:ident     [::counter-id ::counter-id]
   :query     [::counter-id ::counter-label :ui/count]
   :pre-merge (fn [{:keys [current-normalized data-tree]}]
                (merge
                  {:ui/count 5}
                  current-normalized
                  data-tree))}
  (dom/div
    (dom/h4 counter-label)
    (let [done? (zero? count)]
      (dom/button {:disabled done?
                   :onClick  #(m/set-value! this :ui/count (dec count))}
        (if done? "Done!" (str count))))))

(def ui-countdown (comp/factory Countdown {:keyfn ::counter-id}))

(defsc Root [this {::keys [all-counters]}]
  {:initial-state (fn [_] {})
   :query         [{::all-counters (comp/get-query Countdown)}]}
  (dom/div
    (dom/h3 "Counters")
    (if (seq all-counters)
      (dom/div {:style {:display "flex" :alignItems "center" :justifyContent "space-between"}}
        (mapv ui-countdown all-counters))
      (dom/button {:onClick #(df/load! this ::all-counters Countdown)}
        "Load many counters"))))

(defn initialize
  "To be used in :started-callback to pre-load things."
  [app])

10.5.2. Pre-merge with Server Data

To make this example more interesting let’s make it possible, but optional, for the server to define an initial counter value.


(ns book.demos.pre-merge.countdown-with-initial
  (:require
    [com.fulcrologic.fulcro.data-fetch :as df]
    [book.demos.util :refer [now]]
    [com.fulcrologic.fulcro.mutations :as m]
    [com.fulcrologic.fulcro.dom :as dom]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.wsscode.pathom.connect :as pc]
    [com.fulcrologic.fulcro.algorithms.merge :as merge]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SERVER:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def all-counters
  [{::counter-id 1 ::counter-label "A"}
   {::counter-id 2 ::counter-label "B" ::counter-initial 10}
   {::counter-id 3 ::counter-label "C" ::counter-initial 2}
   {::counter-id 4 ::counter-label "D"}])

(pc/defresolver counter-resolver [env _]
  {::pc/output [{::all-counters [::counter-id ::counter-label ::counter-initial]}]}
  {::all-counters all-counters})

(def resolvers [counter-resolver])

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CLIENT:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def default-count 5)

(defsc Countdown [this {::keys   [counter-label counter-initial]
                        :ui/keys [count]}]
  {:ident     [::counter-id ::counter-id]
   :query     [::counter-id ::counter-label ::counter-initial :ui/count]
   :pre-merge (fn [{:keys [current-normalized data-tree]}]
                (merge
                  ; (1)
                  {:ui/count (or (merge/nilify-not-found (::counter-initial data-tree)) default-count)}
                  current-normalized
                  data-tree))}
  (dom/div
    (dom/h4 (str counter-label " [" (or counter-initial default-count) "]"))
    (let [done? (zero? count)]
      (dom/button {:disabled done?
                   :onClick  #(m/set-value! this :ui/count (dec count))}
        (if done? "Done!" (str count))))))

(def ui-countdown (comp/factory Countdown {:keyfn ::counter-id}))

(defsc Root [this {::keys [all-counters]}]
  {:initial-state (fn [_] {})
   :query         [{::all-counters (comp/get-query Countdown)}]}
  (dom/div
    (dom/h3 "Counters")
    (if (seq all-counters)
      (dom/div {:style {:display "flex" :alignItems "center" :justifyContent "space-between"}}
        (mapv ui-countdown all-counters))
      (dom/button {:onClick #(df/load! this ::all-counters Countdown)}
        "Load many counters"))))
  1. During the normalization step of a load Fulcro will put ::merge/not-found value on keys that were not delivered by the server, this is used to do a sweep-merge later, the merge/nilify-not-found will return the same input value (like identity) unless it’s a ::merge/not-found, in which case it returns nil, this way the or works in both cases.

10.5.3. Pre-merge and UI Children

Next, we are going to extract the counter button into its own component to illustrate how you can augment pure UI concerns with this feature:

(defsc CountdownButton [this {:ui/keys [count]}]
  {:ident     [:ui/id :ui/id]
   :query     [:ui/id :ui/count]
   :pre-merge (fn [{:keys [current-normalized data-tree]}]
                (merge
                  ; (1)
                  {:ui/id    (random-uuid)
                   :ui/count default-count}
                  current-normalized
                  data-tree))}
  (let [done? (zero? count)]
    (dom/button {:disabled done?
                 :onClick  #(m/set-value! this :ui/count (dec count))}
      (if done? "Done!" (str count)))))

(defsc Countdown [this {::keys   [counter-label counter-initial]
                        :ui/keys [counter]}]
  {:ident     [::counter-id ::counter-id]
   :query     [::counter-id ::counter-label ::counter-initial
               {:ui/counter (comp/get-query CountdownButton)}]
   :pre-merge (fn [{:keys [current-normalized data-tree]}]
                (let [initial (comp/nilify-not-found (::counter-initial data-tree))]
                  (merge
                    ; (2)
                    {:ui/counter (cond-> {} initial (assoc :ui/count initial))}
                    current-normalized
                    data-tree)))}
  (dom/div
    (dom/h4 (str counter-label " [" (or counter-initial default-count) "]"))
    (ui-countdown-button counter)))
  1. For the UI element we can also set the initial id using pre-merge, and now we moved the default there

  2. Note we pass a blank map to the :ui/counter, this will reach the pre-merge from CountdownButton as the data-tree part

Full demo:


(ns book.demos.pre-merge.countdown-extracted
  (:require
    [com.fulcrologic.fulcro.data-fetch :as df]
    [book.demos.util :refer [now]]
    [com.fulcrologic.fulcro.mutations :as m]
    [com.fulcrologic.fulcro.dom :as dom]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.algorithms.merge :as merge]
    [com.wsscode.pathom.connect :as pc]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SERVER:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def all-counters
  [{::counter-id 1 ::counter-label "A"}
   {::counter-id 2 ::counter-label "B" ::counter-initial 10}
   {::counter-id 3 ::counter-label "C" ::counter-initial 2}
   {::counter-id 4 ::counter-label "D"}])

(pc/defresolver counter-resolver [env _]
  {::pc/output [{::all-counters [::counter-id ::counter-label]}]}
  {::all-counters all-counters})

(def resolvers [counter-resolver])

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CLIENT:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def default-count 5)

(defsc CountdownButton [this {:ui/keys [count]}]
  {:ident     [:ui/id :ui/id]
   :query     [:ui/id :ui/count]
   :pre-merge (fn [{:keys [current-normalized data-tree]}]
                (merge
                  ; (1)
                  {:ui/id    (random-uuid)
                   :ui/count default-count}
                  current-normalized
                  data-tree))}
  (let [done? (zero? count)]
    (dom/button {:disabled done?
                 :onClick  #(m/set-value! this :ui/count (dec count))}
      (if done? "Done!" (str count)))))

(def ui-countdown-button (comp/factory CountdownButton {:keyfn ::counter-id}))

(defsc Countdown [this {::keys   [counter-label counter-initial]
                        :ui/keys [counter]}]
  {:ident     [::counter-id ::counter-id]
   :query     [::counter-id ::counter-label ::counter-initial
               {:ui/counter (comp/get-query CountdownButton)}]
   :pre-merge (fn [{:keys [current-normalized data-tree]}]
                (let [initial (merge/nilify-not-found (::counter-initial data-tree))]
                  (merge
                    ; (2)
                    {:ui/counter (cond-> {} initial (assoc :ui/count initial))}
                    current-normalized
                    data-tree)))}
  (dom/div
    (dom/h4 (str counter-label " [" (or counter-initial default-count) "]"))
    (ui-countdown-button counter)))

(def ui-countdown (comp/factory Countdown {:keyfn ::counter-id}))

(defsc Root [this {::keys [all-counters]}]
  {:initial-state (fn [_] {})
   :query         [{::all-counters (comp/get-query Countdown)}]}
  (dom/div
    (dom/h3 "Counters")
    (if (seq all-counters)
      (dom/div {:style {:display "flex" :alignItems "center" :justifyContent "space-between"}}
        (mapv ui-countdown all-counters))
      (dom/button {:onClick #(df/load! this ::all-counters Countdown)}
        "Load many counters"))))

10.5.4. Pre-merge During App Initialization

Another interesting property about pre-merge is that it also runs during normalization of app initialization, this means you can provide the minimum amount of data as the initial state and let the pre-merge fill the UI needs, to illustrate let’s add some initial state to our demo:

(defsc Root [this _]
  {:initial-state (fn [_] {::all-counters
                           [{::counter-id    (comp/tempid)
                             ::counter-label "X"}
                            {::counter-id    (comp/tempid)
                             ::counter-label "Y"}
                            {::counter-id      (comp/tempid)
                             ::counter-label   "Z"
                             ::counter-initial 9}]})
   :query         [{::all-counters (comp/get-query Countdown)}]}
  ...)

Pre merge will be applied on top of that running the pre-merge on the respective data parts. Full demo:


(ns book.demos.pre-merge.countdown-initial-state
  (:require
    [com.fulcrologic.fulcro.data-fetch :as df]
    [book.demos.util :refer [now]]
    [com.fulcrologic.fulcro.mutations :as m]
    [com.fulcrologic.fulcro.dom :as dom]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.wsscode.pathom.connect :as pc]
    [com.fulcrologic.fulcro.algorithms.merge :as merge]
    [com.fulcrologic.fulcro.algorithms.tempid :as tempid]
    [taoensso.timbre :as log]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SERVER:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def all-counters
  [{::counter-id 1 ::counter-label "A"}
   {::counter-id 2 ::counter-label "B" ::counter-initial 10}
   {::counter-id 3 ::counter-label "C" ::counter-initial 2}
   {::counter-id 4 ::counter-label "D"}])

(pc/defresolver counter-resolver [env _]
  {::pc/output [{::all-counters [::counter-id ::counter-label]}]}
  {::all-counters all-counters})

(def resolvers [counter-resolver])

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CLIENT:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def default-count 5)

(defsc CountdownButton [this {:ui/keys [count]}]
  {:ident     [:ui/id :ui/id]
   :query     [:ui/id :ui/count]
   :pre-merge (fn [{:keys [current-normalized data-tree]}]
                (merge
                  {:ui/id    (random-uuid)
                   :ui/count default-count}
                  current-normalized
                  data-tree))}
  (let [done? (zero? count)]
    (dom/button {:disabled done?
                 :onClick  #(m/set-value! this :ui/count (dec count))}
      (if done? "Done!" (str count)))))

(def ui-countdown-button (comp/factory CountdownButton {:keyfn ::counter-id}))

(defsc Countdown [this {::keys   [counter-label counter-initial]
                        :ui/keys [counter]}]
  {:ident     [::counter-id ::counter-id]
   :query     [::counter-id ::counter-label ::counter-initial
               {:ui/counter (comp/get-query CountdownButton)}]
   :pre-merge (fn [{:keys [current-normalized data-tree]}]
                (let [initial (merge/nilify-not-found (::counter-initial data-tree))]
                  (log/spy :info (merge
                                   {:ui/counter (cond-> {} initial (assoc :ui/count initial))}
                                   current-normalized
                                   data-tree))))}
  (dom/div
    (dom/h4 (str counter-label " [" (or counter-initial default-count) "]"))
    (ui-countdown-button counter)))

(def ui-countdown (comp/factory Countdown {:keyfn ::counter-id}))

(defsc Root [this {::keys [all-counters]}]
  {:initial-state (fn [_] {::all-counters
                           [{::counter-id    (tempid/tempid)
                             ::counter-label "X"}
                            {::counter-id    (tempid/tempid)
                             ::counter-label "Y"}
                            {::counter-id      (tempid/tempid)
                             ::counter-label   "Z"
                             ::counter-initial 9}]})
   :query         [{::all-counters (comp/get-query Countdown)}]}
  (dom/div
    (dom/h3 "Counters")
    (when (seq all-counters)
      (dom/div {:style {:display "flex" :alignItems "center" :justifyContent "space-between"}}
        (mapv ui-countdown all-counters)))))

10.5.5. Pre-merge via Mutations

We’ve already talked about ways to integrate new data using things like comp/merge-component, and since pre-merge takes effect there as well we can rely on it in mutations:

(m/defmutation create-countdown [countdown]
  (action [{:keys [state ref]}]
    (swap! state comp/merge-component Countdown countdown :append [::all-counters])
    (swap! state update-in ref assoc :ui/new-countdown-label "")))

And add some UI to trigger this mutation:

(defsc Root [this {::keys [all-counters]}]
  {:initial-state (fn [_] {::all-counters
                           [{::counter-id    (comp/tempid)
                             ::counter-label "X"}
                            {::counter-id    (comp/tempid)
                             ::counter-label "Y"}
                            {::counter-id      (comp/tempid)
                             ::counter-label   "Z"
                             ::counter-initial 9}]})
   :query         [{::all-counters (comp/get-query Countdown)}
                   :ui/new-countdown-label]}
  (dom/div
    (dom/h3 "Counters")
    (dom/button {:onClick #(comp/transact! this [`(create-countdown ~{::counter-id    (comp/tempid)
                                                                      ::counter-label "New"})])}
      "Add counter")
    (dom/div {:style {:display "flex" :alignItems "center" :justifyContent "space-between"}}
      (mapv ui-countdown all-counters))))

Full demo:


(ns book.demos.pre-merge.countdown-mutation
  (:require
    [book.demos.util :refer [now]]
    [com.fulcrologic.fulcro.mutations :as m]
    [com.fulcrologic.fulcro.dom :as dom]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.algorithms.merge :as merge]
    [com.fulcrologic.fulcro.algorithms.tempid :as tempid]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CLIENT:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def default-count 5)

(defsc CountdownButton [this {:ui/keys [count]}]
  {:ident     [:ui/id :ui/id]
   :query     [:ui/id :ui/count]
   :pre-merge (fn [{:keys [current-normalized data-tree]}]
                (merge
                  {:ui/id    (random-uuid)
                   :ui/count default-count}
                  current-normalized
                  data-tree))}
  (let [done? (zero? count)]
    (dom/button {:disabled done?
                 :onClick  #(m/set-value! this :ui/count (dec count))}
      (if done? "Done!" (str count)))))

(def ui-countdown-button (comp/factory CountdownButton {:keyfn ::counter-id}))

(defsc Countdown [this {::keys   [counter-label counter-initial]
                        :ui/keys [counter]}]
  {:ident     [::counter-id ::counter-id]
   :query     [::counter-id ::counter-label ::counter-initial
               {:ui/counter (comp/get-query CountdownButton)}]
   :pre-merge (fn [{:keys [current-normalized data-tree] :as x}]
                (let [initial (merge/nilify-not-found (::counter-initial data-tree))]
                  (merge
                    {:ui/counter (cond-> {} initial (assoc :ui/count initial))}
                    current-normalized
                    data-tree)))}
  (dom/div
    (dom/h4 (str counter-label " [" (or counter-initial default-count) "]"))
    (ui-countdown-button counter)))

(def ui-countdown (comp/factory Countdown {:keyfn ::counter-id}))

(m/defmutation create-countdown [countdown]
  (action [{:keys [state ref]}]
    (swap! state merge/merge-component Countdown countdown :append [::all-counters])
    (swap! state update-in ref assoc :ui/new-countdown-label "")))

(defsc Root [this {::keys [all-counters]}]
  {:initial-state (fn [_] {::all-counters
                           [{::counter-id    (tempid/tempid)
                             ::counter-label "X"}
                            {::counter-id    (tempid/tempid)
                             ::counter-label "Y"}
                            {::counter-id      (tempid/tempid)
                             ::counter-label   "Z"
                             ::counter-initial 9}]})
   :query         [{::all-counters (comp/get-query Countdown)}
                   :ui/new-countdown-label]}
  (dom/div
    (dom/h3 "Counters")
    (dom/button {:onClick #(comp/transact! this [`(create-countdown ~{::counter-id    (tempid/tempid)
                                                                      ::counter-label "New"})])}
      "Add counter")
    (dom/div {:style {:display "flex" :alignItems "center" :justifyContent "space-between"}}
      (mapv ui-countdown all-counters))))

10.6. 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:

10.6.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}
                                :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?). Of course, Pathom makes this quite easy since it honors the query presented to it.

10.6.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. We can do this using df/load-field!, which does the opposite of :without on the query:

(defsc Blog [this props]
  {:ident  :blog/id
   :query  [:blog/id :blog/title {:blog/content (comp/get-query BlogContent)} {:blog/comments (comp/get-query BlogComment)}]}
  (dom/div
     ...
     (dom/button {:onClick #(df/load-field! this :blog/comments {})} "Show Comments")
     ...)))

The load-field! function finds the query on the component (via this) and prunes everything from that 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}]

In the example above, this would end up something like this:

[{[:blog/id 1] [{:blog/comments [:comment/id :comment/author :comment/body]}]}]

This kind of query can be handled automatically by Pathom as long as there is a resolver that takes :blog/id as an input, and resolvers exist to fulfill the subgraph from there!

(pc/defresolver blog-resolver [env {:blog/keys [id]}]
  {::pc/input #{:blog/id}
   ::pc/output [... {:blog/comments [:comment/id]}]}
  ...)

(pc/defresolver comment-resolver [env {:comment/keys [id]}]
  {::pc/input #{:comment/id}
   ::pc/output [:comment/body {:comment/author [:author/id]}]}
  ...)

;; and so on
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/id :db/id]
   :query  [:db/id :blog/title {:blog/content (comp/get-query BlogContent)} {:blog/comments (comp/get-query BlogComment)}]}
  (dom/div
     ...
     (dom/button {:onClick #(load this (comp/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.

10.7. Load Errors

The load! function includes a number of options for dealing with problems during load. The docstring load! describes your options, and we leave that discussion to the chapter on overall error handling and user experience.

11. Network Latency, Error Handling, and User Experience

Most applications have to contend with network latency. A good user experience requires that applications be able to give feedback to the user about the progress of network operations. The optimistic nature of Fulcro means that you can often give the user the impression of zero latency (the UI changes before the networking even starts); however, there are many circumstances where you would like to give the user some assurance that the full-stack operation is complete before moving on. For example, when saving a form you usually don’t want to pop up some kind of error about the submission of that form when the user has already moved on to a new screen.

You’ve certainly seen a number of different ways this can be handled. Network applications like Google Docs give the user a continuous indication of when their changes are pending vs. when they have been saved, which allows the user to work in an uninterrupted fashion where they only need to worry about the network when they are done with the entire session. More and more applications save data from things like settings screens as the changes are made, removing the need for a user "save step" as well.

Still others do more complex schemes that persist data into browser local storage as a method of preventing data loss even if the network is spotty or the browser crashes while unsaved changes are queued.

Fulcro gives you the power to express any user experience, and this chapter covers some of the techniques you can use to provide the experience you desire.

11.1. Global Network Activity

Fulcro tracks the general idea that network activity is happening in the state database under the key :com.fulcrologic.fulcro.application/active-remotes. The value is a set of remote names (keywords) that have scheduled remote activity that has yet to complete. A remote will appear in this set from the time the desire for remote content enters the transaction processing system until that desire is met (or fails).

This global set can easily be queried for by a component that wants to track the status of network activity by simply querying for it with a root link element in the query: [::app/active-remotes '_].

Note
Any component querying for the network active remotes will see a lot of re-renders. It is recommended that such a component be a leaf in the UI graph so you don’t trigger refresh on entire subtrees due to network activity status updates.
(defsc GlobalStatus [this {::app/keys [active-remotes]}]
  {:query [[::app/active-remotes '_]]
   :ident (fn [] [:component/id :activity])
   ;; important: components that query only for root data need to have at least some empty state of their own
   :initial-state {}}
  ...
  (let [loading? (boolean (seq active-remotes))]
    (when loading?
      (dom/div "Loading..."))))
Example 34. Network Activity

(ns book.server.network-activity
  (:require
    [com.fulcrologic.fulcro.application :as app]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.dom :as dom]
    [com.fulcrologic.fulcro.mutations :as m :refer [defmutation]]
    [com.wsscode.pathom.connect :as pc]
    [taoensso.timbre :as log]
    [clojure.pprint :refer [pprint]]
    [com.fulcrologic.fulcro.data-fetch :as df]))

;; Simulated server

(pc/defresolver silly-resolver [_ _]
  {::pc/output [::data]}
  {::data 42})

;; Client

(defsc ActivityIndicator [this props]
  {:query         [[::app/active-remotes '_]]
   :ident         (fn [] [:component/id ::activity])
   :initial-state {}}
  (let [active-remotes (::app/active-remotes props)]
    (dom/div
      (dom/h3 "Active Remotes")
      (dom/pre (pr-str active-remotes)))))

(def ui-activity-indicator (comp/factory ActivityIndicator {:keyfn :id}))

(defsc Root [this {:keys [indicator]}]
  {:query         [{:indicator (comp/get-query ActivityIndicator)}]
   :initial-state {:indicator {}}}
  (dom/div {}
    (dom/p {} "Use the server controls to slow down the network, so you can see the activity")
    (dom/button {:onClick #(df/load! this ::data nil)} "Trigger a Load")
    (ui-activity-indicator indicator)))

11.2. Pessimistic Operation

Fulcro is optimistic by default, but pessimism is also a supported mode. There are a couple of clear techniques you can use to allow the network activity to complete before allowing the user to continue:

  1. The Load API has a number of parameters that can allow you to plug in your own code for dealing with results.

  2. For mutations: use the action/ok-action/error-action to do a sequence of optimistically blocking the UI, sending the request, and then using the ok-action/error-action as the place to unblock the UI and report the result. This is the most similar to the approach other libraries use.

  3. Use pessimistic transactions. This is a mode of transaction processing that allows each mutation in a single top-level transaction (call to transact!) to complete (full-stack) before anything from the next one begins. This mode has the advantage of making the sequence visible at the transaction layer, but requires you share progressive data through the app state itself. Some people prefer this because the "happy path" can be expressed clearly in the UI, even though the details are still within the mutation. Something like (comp/transact! this [(submit-form) (process-result)] {:optimistic? false}) is more directly informative to the reader than (comp/transact! this [(submit-form)]) (implied post-processing hidden in the mutation ok-action).

There are also some more advanced techniques that allow you to leverage more global control:

  1. Change the default-result-action for mutations and provide application-specific defaults for error handling.

  2. Change the internal-load mutation for loads to add in whatever global changes you desire.

The above two techniques typically would leverage the default implementations so that the pre-defined behaviors still work, but might include mechanisms you define to handle common cases without having to code them repeatedly. If you want to explore these advanced techniques expect to use the source. See mutations.cljc and data_fetch.cljc for the default implementations, and the docstring of app/fulcro-app for the options you can use to override the defaults.

11.3. 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.

In more modern apps 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 that behavior is simple: it is much less like an actual 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.

11.3.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 not the central concern.

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 allows the UI to show the correct result without treating anything as a distributed-systems/data-consistency kind of error.

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, since it is quite expensive). 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. 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.

11.3.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. The following example is a common way to do UI blocking with the mechanisms you’ve already seen. It overrides the default result-action to look at the response from the server and decide what to do.

Example 35. UI Blocking

(ns book.server.ui-blocking-example
  (:require
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.dom :as dom]
    [com.fulcrologic.fulcro.mutations :as m :refer [defmutation]]
    [com.wsscode.pathom.connect :as pc]
    [taoensso.timbre :as log]))

;; SERVER

(pc/defmutation submit-form-mutation [env params]
  {::pc/sym `submit-form}
  (log/info "Server got " params)
  (if (> 0.5 (rand))
    {:message "Everything went swell!"
     :ok?     true}
    {:message "Service temporarily unavailable!"
     :ok?     false}))

;; 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 {:style {:position        :absolute
                    :display         (if active? "block" "none")
                    :zIndex          65000
                    :width           "400px"
                    :height          "100px"
                    :backgroundColor "rgba(0,0,0,0.5)"}}
    (dom/div {:style {:position  :relative
                      :top       "40px"
                      :color     "white"
                      :textAlign "center"}} message)))

(def ui-overlay (comp/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))

(defmutation submit-form [params]
  (action [{:keys [state]}]
    (swap! state (fn [s]
                   (-> s
                     (set-overlay-message* "Working...")
                     (set-overlay-visible* true)))))
  (result-action [{:keys [app state result]}]
    (log/info "Result:" result)
    (let [mutation-result (-> result :body (get `submit-form))
          {:keys [message ok?]} mutation-result]
      (if ok?
        (swap! state set-overlay-visible* false)
        (do
          (swap! state set-overlay-message* (str message "   Retrying submission in 1s."))
          ;; could use setTimeout or immediately do it
          (js/setTimeout
            #(comp/transact! app [(submit-form params)])
            1000)))))
  (remote [_] true))

(defsc Root [this {:keys [ui/name overlay]}]
  {:query         [:ui/name {:overlay (comp/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 #(comp/transact! this [(submit-form {:made-up-data 42})])}
      "Submit")))

11.3.3. Defining "Remote Errors"

Fulcro’s concept of a "remote error" is configurable. It defaults to a function that returns true if the network result includes a :status-code attribute that is not 200. However, it is common for applications to define errors more widely. For example, you might consider certain data return values to be indications of an error.

You can define the meaning of "remote errors" when you create your Fulcro application via the :remote-error? option:

(app/fulcro-app {:remote-error? (fn [result] boolean)})

where the result value is the raw value that your selected remote encodes. Fulcro’s HTTP remote encodes result as:

{:body raw-result
 :transaction outgoing-tx
 :status-code n
 ...}

This result is determined by the remote implementation and client network middleware can certainly expand/modify this as needed.

11.3.4. Global Error Handler

When you create your application you can set a global function that is called on any kind of network error that happens against any remote. The function will be called with the env, which will contain things like the AST, network result, etc.

(app/fulcro-app {:global-error-action (fn [env] ...))

You can of course swap against the state atom, use the app from the env to submit a new transaction, etc.

11.3.5. Load Errors

Loads can define a mutation symbol that should be used to trigger a transaction on errors. Loads may also choose to specify a lambda

11.3.6. Server Error Demo

We recommend using Pathom for the server, and one of the recommended plugins for Pathom will convert exceptions into data errors on the response. This allows pathom to give partial responses where it can partially fufill a request for data.

Fulcro does not default to considering these particular kinds of errors as "hard errors", and leaves them up to your interpretation; however, it is simply enough to redefine what Fulcro things of as hard errors by setting the :remote-error? option on your application.

The live example below demonstrates redefining the meaning of hard errors to look for these Pathom values, and shows the resulting use of the built-in error-action support of mutations.

Example 36. Error Handling

(ns book.demos.server-error-handling
  (:require
    [com.fulcrologic.fulcro.data-fetch :as df]
    [com.fulcrologic.fulcro.mutations :as m :refer [defmutation]]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.dom :as dom]
    [taoensso.timbre :as log]
    [com.wsscode.pathom.connect :as pc]
    [com.wsscode.pathom.core :as p]
    [com.fulcrologic.fulcro.application :as app]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SERVER:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(pc/defmutation server-error-mutation [env params]
  {::pc/sym `error-mutation}
  ;; Throw a mutation error for the client to handle
  (throw (ex-info "Mutation error" {})))

(pc/defresolver child-resolver [env input]
  {::pc/output [:fulcro/read-error]}
  (throw (ex-info "read error" {})))

(def resolvers [server-error-mutation child-resolver])

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CLIENT:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; Mutation used as a fallback for load error: In this case the `env` from the load result *is* the params to this mutation
(defmutation read-error [params]
  (action [env]
    (js/alert "There was a read error")
    (log/info "Result from server:" (:result params))
    (log/info "Original load params:" (:load-params params))))

;; an :error key is injected into the fallback mutation's params argument
(defmutation error-mutation [params]
  (ok-action [env] (log/info "Optimistic action ran ok"))
  ;; Error action is only called if `:remote-error?` for the application is defined to consider the response an error.
  (error-action [{:keys [app ref result]}]
    (js/alert "Mutation error")
    (log/info "Result " result))
  (remote [env] true))

(defsc Child [this props]
  {:initial-state {}
   :query         ['*]
   :ident         (fn [] [:error.child/by-id :singleton])}
  (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 :fulcro/read-error nil {:fallback `read-error})}
      "Failing read with a fallback")
    (dom/button {:onClick #(comp/transact! this `[(error-mutation {})])} "Failing mutation")
    ))

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

(defsc Root [this {:keys [child]}]
  {:initial-state (fn [params] {:child (comp/get-initial-state Child {})})
   :query         [{:child (comp/get-query Child)}]}
  (dom/div (ui-child child)))

(defn contains-error?
  "Check to see if the response contains Pathom error indicators."
  [body]
  (when (map? body)
    (let [values (vals body)]
      (reduce
        (fn [error? v]
          (if (or
                (and (map? v) (contains? (set (keys v)) ::p/reader-error))
                (= v ::p/reader-error))
            (reduced true)
            error?))
        false
        values))))

(def SPA (app/fulcro-app {:remote-error? (fn [{:keys [body] :as result}]
                                           (or
                                             (app/default-remote-error? result)
                                             (contains-error? body)))}))

11.4. Aborting a Request

Requests that are heavy on the server or require significant data transfer often need to be cancelled. User experience, especially in mobile environments, can be heavily impacted if their navigation is blocked with no way back. Futhermore, there is no real reason for you to pay for the bandwidth to send or receive something that the user is no longer interested in.

The built-in HTTP remote support aborts. If you use a custom or alternate remote implementation then that may affect the availability of both abort and progress reports. See the documentation and source of your remote for more information.

The abort feature requires that the request be assigned a unique identifier. You can use any data type that supports equivalence, but we recommend using keywords. The df/load! and comp/transact! functions both accept an option for supplying this ID:

(df/load! this :thing Thing {:abort-id :thing})
(comp/transact! this [(f)] {:abort-id :made-up-id})

The application namespace includes and abort! function that can accept a component or app, and the abort ID:

(app/abort! this :made-up-id)

Aborting a load or transaction while holding the thread of submission will have no effect on the load just issues (the load/mutation will be on a submission queue, not in active processing):

(df/load! this :thing Thing {:abort-id :thing})
(when bad-thing?
  ;; WILL NOT WORK
  (app/abort! this :thing))

Technically, it will abort any earlier submission(s) with abort ID :thing that have entered the processing phase.

Aborting a request that is active on the network or has yet to start networking will remove the request from the processing queue, cancel the network operation, and trigger a result action. Your definition of remote-error? on your application will determine if the result is considered an error (which will trigger the error side of post-processing) or not (which will trigger the OK side of post-processing).

The :result reported to your remote-error? and post-processing will not include a body or status code (since the network request didn’t finish and provide one), but will instead look like this:

{::txn/aborted? true}

Therefore the default remote error definition will consider aborts to be an error (since it does not have a status code of 200).

11.5. Progress Updates

Obtaining progress updates on mutations are requested the same way as abort IDs: through AST manipulation on the mutation:

(defmutation some-mutation [params]
  (remote [{:keys [ast]}]
     (m/with-progressive-updates ast `(progress-mutation {:x 1}))))

Progress updates are sent to the given mutation (which will always receive the parameters you specified). The parameters will be augmented with information about the current progress:

  • :progress-phase - will be one of :sending, :receiving, :complete, or :failed.

  • :progress-event - the raw XhrIO event (which has loading progress data)

  • :transaction - The transaction that is running

  • :body - The current low-level body. Body is NOT processed through the middleware, and could be partial.

  • :status-code - The HTTP status code if receiving.

The fulcro.client.network/progress% function can be used to convert this into a number between 0 and 100.

Progress updates are currently supported on mutations. Support for progress directly on loads is possible, but not yet implemented. The workaround for the moment to get progress for a load is to use mutation joins to return a value from a mutation:

(defmutation some-mutation [params]
  (remote [{:keys [ast state]}]
    (-> ast
      (m/with-progressive-updates `(progress {}))
      (m/returning state Thing))))

where Thing is some component that defines the query for the data being returned from the mutation.

In general progress updates are only really useful for larger requests, such as file uploads. Triggering downloads to the user’s machine should probably be done using tricks that force the item into the normal browser download mechanisms (external to Fulcro).

File uploads also require that you augment the new client network middleware, since there is no reliable way to encode an image of arbitrary size into a transaction.

12. Building a Server

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.

In general you should probably use a template project that already has a server set up, along with the various bits you need for configuration, restart during development, testing, etc.

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.

  2. Use api-middleware/handle-api-request to handle requests for the correct URI (defaults to /api)

An example basic Ring stack was demonstrated in the Getting Started chapter’s Going Remote section.

12.1. Use Pathom to Process the EQL

You can hand-write a server-side parser to handle EQL from the client, but you should really just use Pathom. See the Pathom Developer’s Guide.

12.2. Server Configuration

Fulcro includes a small set of functions that can help you with managing your server-side configuration data. These functions are in the com.fulcrologic.fulcro.server.config namespace. The primary function is load-config!.

Server configuration requires two EDN files:

  • config/defaults.edn: This file (from CLASSPATH, typically resources) 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). The content can be 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 production environment that is missing a configuration file. 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 configuration loading has a number of built-in features:

  • 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 (recursive) merge.

  • You can override the configuration file that will be used as :config-path with a JVM option: -Dconfig=filename. This allows you to specify the file on a per-environment basis.

  • 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 instead of local disk. This allows you to package your config with your application.

If you are using something like mount, then you will typically use load-config! directly:

(defstate config :start (server/load-config! {:config-path "config/dev.edn"}))

and then config (after start) will simply contain the EDN of the defaults/config file merge.

13. UI State Machines

User interfaces are full of interactions that beg for the help of state machines. Just a simple login form is usually quite difficult to code (independent of tools) because its behavior depends on a number of factors…​factors that are actually quite easy to represent with state machines, but get ugly quickly when spread out in various code artifacts.

This is not a new idea. People have been using state machines to control user interfaces and graphics systems for decades. For some reason most UI libraries and frameworks don’t usually have a well-known or particularly general-purpose state machine aspect. Part of the problem is that a state machine needs to interact with actual UI state and vice-versa, so it is difficult to "bolt on" something, and there is often a lot of "glue code" to make the two hang together properly.

It turns out that Fulcro’s approach to UI is quite easily amenable to state machines because it has the following facets:

  • The transactional nature of mutations and central state is already a lot like a state machine.

  • Application state is just data.

  • The application database is normalized: It is very easy to describe where particular bits of data are in a non-ambiguous manner.

  • The UI refresh is based on the normalized data model, and not the UI structure. Triggering refreshes requires only that you know what data your changing.

Thus, it turns out to be quite easy to build a state machine system for Fulcro with the following properties:

  1. The state machine doesn’t need not know anything about the UI

  2. The UI only needs to support displaying the declared state of the state machine.

  3. Simple aliasing can map the state machine "values" onto Fulcro database values.

  4. The aliasing makes it possible to re-use state machines on UIs that have varying shapes, and need not even name their Fulcro state according to the state machine’s conventions.

  5. State machines can be instanced, so that more than one of the same kind can be running at once.

  6. Active state machine data is stored in Fulcro’s app database, so it honors all history properties (e.g. support viewer, etc.) and is amenable to great tooling.

  7. Any number of simultaneous state machines of varying type can be running at once (even on the same component).

  8. The state machine declarations are reusable (they are just maps), and make it easy to "derive" new definitions based on existing ones with simple clj functions like merge and assoc-in.

13.1. Aliases

The first powerful concept for our state machines is aliasing. The first kind of aliasing is for the "actors" that will participate in our UI. An actor is simply a keyword defined in the state machine declaration, and is meant to stand for "some UI component". The actions of a state machine can then be written to abstractly refer to that component without actually needing to know anything else about it:

(defstatemachine login
  {::uism/actor-names #{:actor/dialog :actor/form}
   ...})

In this example we plan to have a "dialog" and a "form" on the UI. These could be separate UI components, or could be the same. It doesn’t matter to the state machine! We adopt the convention of using the actor namespace for these, but it is not required.

In reality actors are merely names for some "yet to be specified" ident that points to an entity in your database.

The next layer of aliasing is for the data our state machine will manipulate:

(defstatemachine login-machine
  {::uism/actor-names #{:actor/dialog :actor/form}
   ::uism/aliases {:visible?       [:actor/dialog :ui/active?]
                   :login-enabled? [:actor/form :ui/login-enabled?]
                   :busy?          [:actor/form :ui/busy?]
                   :error          [:actor/form :ui/login-error]
                   :username       [:actor/form :user/email]
                   :password       [:actor/form :user/password]}
   ...})

Thus, the alias busy? is now the data path to some field of an entity in the database. The actual path will be resolved at runtime based on what components actually end up being used as the actors. For example if you use a component with ident [:modal/id :general] as the :actor/dialog then the alias :visible? means the path [:modal/id :general :ui/active?]. This is the primary aspect of UI state machines that makes them reusable.

13.2. Derived State Machines

State machine definitions are just maps. If you needed the field on the dialog to be called :ui/visible? instead of :ui/active? you could simply do this:

(defstatemachine custom-login-machine
  (assoc-in login-machine [::uism/aliases :visible? 1] :ui/visible?))

This makes it possible to easily build a library of state machines that work on your app state in a very general and configurable way without having to change any actual logic!

Note
State machine definitions must be declared with defstatemachine even though they are just maps. The declaration registers them with the system so that they have a unique ID that can be used within the instances in Fulcro’s client database.

13.3. Plugins

In order for a state machine to be as reusable as possible we’d also like to be able to write logic that the state machine uses in a form that can be easily changed. We call these bits of logic "plugins". The are simply functions that will receive a map of the current UI state (by alias name) and will do some calculation. They are meant to be side-effect free calculations.

For example a login form we usually don’t want them to be able to press "Login" (or enter) until both username and password fields have something in them. If the username is an email we might also want to check that it looks like a valid email before allowing submission.

The state machine can come with a simple plugin like this:

   ::uism/plugins     {:valid-credentials? (fn [{:keys [username password]}]
                                             (boolean (and (seq username) (seq password))))}

Plugins receive (as a map) all of the current aliased data used by the state machine (so they can easily destructure it as shown). They can return any value.

Changing a plugin is as simple as the trick shown for overriding an alias in Derived State Machines.

Plugins are called within a state handler like so:

  ...
  :event/login        {::uism/handler
                        (fn [env]
                          (let [valid?   (uism/run env :valid-credentials?)]
                            ...))}

13.4. States

The final bit of a state machine definition is, of course, the actual states. In our system we define these as a map from user-defined state name to a function that will receive the running state machine environment for all events triggered on that state machine.

The states must include an :initial state, whose handler will be invoked with a ::uism/started event when the state machine is first started. The "current state" handler is always invoked for each event that is triggered while it is active, but only the :initial state sees a ::uism/started event.

The overall configuration of states looks like this:

(defstatemachine login-machine
  {::uism/actor-names #{...}
   ::uism/aliases {...}
   ::uism/states {:initial { ... }   ; REQUIRED
                  :state/state-id { ... }
                  :state/state2-id { ... }
   ...

By convention it is a good idea to name your states with a state namespace for readability, but this is not required.

You have two options for what you put in a state’s definition.

13.4.1. Option 1 — Predicate/handler (preferred)

With this option you specify a map of events to a description of what should happen:

::uism/states {:initial    {::uism/events
                            {:event/thing-happened!
                              {::uism/event-predicate (fn [env] ... true)
                               ;; target-states is doc for diagram tools
                               ::uism/target-states   #{:state/next-state :state/other-state}
                               ::uism/handler         (fn [env] ... (uism/activate env :state/next-state))}}}
               :state/next-state {...}
               ...

In this case the event :event/thing-happened! is an event that can happen while in the :initial state which:

  1. If there is an event predicate, it is run. The default predicate is (constantly true). If the predicate returns false then the event is ignored and nothing else happens.

  2. If the predicate returned true (or didn’t exist), then the handler is run. Any effects it has on env are propagated.

The predicate is useful for a few reasons:

  1. You may have a condition that should short-circuit triggers of numerous events. Without the predicate you’d have to code an if into each handler.

  2. The UISM helper functions that set state (e.g. set-string!) apply state changes before your handler. Under certain circumstances you’d like to avoid that. If predicate is false, then these events (as per the rules above) are not applied.

13.4.2. Option 2 — A Single Handler

This format of defining the states allows you to write just one function, but is not normally recommended, as it does not give you the ability to analyze the states/events as a diagram via simple data analysis. It does, however, allow you complete flexibility with how the state machine is defined, so you are welcome to use it. Basically you do not define an event map, and instead embed a handler in it’s place:

   ::uism/states  {:initial
                   {::uism/handler
                     (fn [env]
                       (log/info "Initial state.")
                       ...)}}

13.5. Writing Handlers and Data Manipulation

From here it’s pretty easy. The handlers are functions that receive a state machine (SM) environment and must return an updated environment (or nil, which is considered "no change"). Since the environment is an immutable value you will typically thread a sequence of these together to end up with a final result to return from the handler:

(fn [env]
  (-> env
     (uism/assoc-aliased :visible? true)
     ...))

The library includes many functions for manipulating the system via the state machine handler’s env:

(uism/assoc-aliased env alias new-value & more-kv-pairs)

Sets Fulcro state associated with the given alias to the given new value. Can accept multiple k-v pairs (like assoc).

(uism/dissoc-aliased env alias & more-aliases)

Removes given aliases from Fulcro state. Can accept multiple aliases (like dissoc).

(uism/update-aliased env alias f & args)

Updates given aliases in Fulcro state with function f and given arguments. (like update).

(uism/integrate-ident env ident & named-parameter)

Integrates idents (append or prepend) to aliases in Fulcro state that refer to a list of idents. (like mutations/integrate-ident*).

(uism/remove-ident env ident alias)

Removes ident from aliases that refer to a list of idents, in Fulcro state. (like fulcro.client.mutations/remove-ident*).

(uism/alias-value env alias)

Gets the current Fulcro state value associated with an alias.

(uism/run env plugin-name)

Runs the given plugin (passing it all of the aliased data from current Fulcro state) and returns the value from the plugin.

(uism/activate env state-name)

Returns a new env with state-name as the new active state.

(uism/exit env)

Returns a new env that will end the state machine (and GC it’s instance from Fulcro state) after the results of the handler are processed.

(uism/store env k v)

Saves a state-machine local value. Useful for keeping track of some additional bit of data while your state machine is running.

(uism/retrieve env k)

Get state-machine local value.

(uism/apply-action env (fn [state-map] state-map))

use a fn of state-map (i.e. some mutation helper) via a SM env.

(uism/asm-value env ks)

Get the value of from an active state machine based on keyword OR key-path ks. Primarily used for extending the library and internal use.

(uism/actor→ident env actor-name)

Get the ident of an actor

Some of the utilities allow you to work fields directly on actors without having to alias fields:

(uism/actor-path env actor-name)

Get the real Fulcro state-path for the entity of the given actor.

(uism/actor-path env actor-name k)

Get the real Fulcro state-path for the attribute k of the entity of the given actor.

(uism/set-actor-value env actor-name k v)

Set a value in the actor’s Fulcro entity. Only the actor is resolved. The k is not processed as an alias.

(uism/actor-value env actor-name k follow-idents?)

Get the value of a particular key in the given actor’s entity. If follow-idents? is true (which is the default), then it will recursively follow idents until it finds a non-ident value.

13.6. Using State Machines from the UI

The next step, of course, is hooking this state machine up so it can control your UI (which really just means your app state).

13.6.1. Starting An Instance

The first thing you need to do is create an instance and start it:

(uism/begin! app-or-component machine-def instance-id actor-map)

Installs an instance of a state machine (to be known as instance-id, an arbitrary keyword), based on the definition in machine-def, into Fulcro’s state and sends the ::uism/started event.

This is a Fulcro transaction internally, so it follows the normal transactional semantics and can be used from almost anywhere (including inside of another state machine handler).

The instance is stored in your database at the well-known ::uism/asm-id table with an id that is the instance ID you supply to this call.

The Actor Map

The actor-map is a map, keyed by actor-id, that lets the state machine know what components in your Fulcro app are being acted upon. It also supplies the necessary information that is needed when doing remote mutations and loads (since a component class or instance is needed to figure out normalization).

The actor map values must be one of the following:

An ident

A raw ident is allowed, but should normally be augmented using with-actor-class so that it can work with loads/mutations.

A component class

In this case the actor is assumed to be a singleton. The ident will be derived by calling (comp/get-ident class {}). This actor will work properly with remote return values and loads.

A component instance (e.g. this)

A component instance can be this, or can be found using Fulcro’s live indexes (e.g. (comp/ref→any reconciler [:person/id 1])). A component instance is sufficient for the state machine to find the correct ident and query for the UI component, so it will work with loads/mutations.

Example Starts

For example, to start the above state machine with an instance ID of ::loginsm:

(uism/begin! this login-machine ::loginsm {:actor/dialog  Dialog
                                           :actor/session Session
                                           :actor/form    LoginForm})

In this example all three of our components are singletons whose idents are constant. If you are working with actors that are more dynamic you either need to use a react instance (such as this), or an explicit ident:

(uism/begin! this person-editing-machine ::personsm {:person (uism/with-actor-class [:person/id 3] Person)
                                                     :editor this
                                                     :dialog Dialog})

13.6.2. Triggering Events

Now that you have a state machine running it is ready to receive events. It will have already run the initial state handler. For example, in our login case the initial state might show the dialog, clear the input fields, etc.

Forms may want to send an event to indicate that a value is changing in some form field. Because this is such a common operation there are two helpers for it. For example, to update a string from a DOM onChange event or raw string:

(uism/set-string! component state-machine-id data-alias event-or-string)

Puts a string into the given data alias (you can pass a string or a DOM onChange event) and sends a ::uism/value-changed event to your machine.

(uism/set-value! component state-machine-id data-alias raw-value)

Puts a raw (unmodified) value into the given data alias and sends a ::uism/value-changed event to your machine.

You can define other "custom" events to stand for whatever you want (and they can include aux data that you can pass along to the handlers). To trigger arbitrary events use:

(uism/trigger! comp-or-reconciler state-machine-id event)

Trigger an arbitrary event on the given state machine.

For example:

(uism/trigger! this ::loginsm :event/failure)

would send a (user-defined) :event/failure event. Event data is just a map that can be passed as an additional parameter:

(uism/trigger! reconciler ::loginsm :event/failure {:message "Server is down. Try in 15 minutes."})

13.6.3. Looking at The Running Instance

For debugging purposes you can just look in Fulcro Inspect at the database. The active state machines are in the ::uism/asm-id table, with the ID you assigned.

UI code can also look at the state machine’s current state:

(uism/get-active-state this asm-id)

Returns the current state name (keyword).

(uism/asm-ident asm-id)

Returns the ident of the active state machine with the given ID.

Important
Technically you must query for any data you use in the UI, but these functions help keep the internals of the UISM system out of your UI code.

Any changes made to actor aliases presumably change data that a component is already querying for, meaning that those components will automatically refresh without a problem; however, if you use data without querying for it (and this is true everywhere in Fulcro) then you can experience what looks like "missed refreshes".

Remember that Fulcro won’t re-render a component unless its props change (they are all pure components). If you use (get-active-state) from a UI then you are "grabbing data" without it going through props. In order to get proper refreshes on a component whose UI depends on the current state you must query for that state machine’s data. Including the ident in your query is enough:

(defsc Component [this props]
  {:query (fn [] [ (uism/asm-ident ::my-machine) ...])
   ...}
  ...
  (let [s (get-active-state this ::my-machine)] ...))

13.7. Mutations and State Machines

Functions are included that allow you to trigger remote mutations. Your state machine handlers are already an implementation of the client side operations of a mutation, so really what we need is a way to trigger a remote mutation and then trigger events based on the outcome.

The trigger-remote-mutation function does this. It takes:

  • env - The SM handler environment

  • actor - The name (keyword) of a defined actor. The mutation will be run in the context of this actor’s state (see pm/pmutate!), which means that progress will be visible there. THERE MUST BE A MOUNTED COMPONENT with this actor’s name ON the UI, or the mutation will abort. This does not have to be the same component as you’re (optionally) returning from the mutation itself. It is purely for progress UI.

  • mutation - The symbol (or mutation declaration) of the server mutation to run. This function will not run a local version of the mutation (nor do you have to write one).

  • options-and-params - The parameters to pass to your mutation. This map can also include these additional options (which will not show up in the remote and are namespaced to avoid collision):

    ::m/returning Class

    Indicate a return value of the remote mutation (for normalizing the returned result). If you know the class you can use it. Use (actor-class actor-name) to get the correct class for an arbitrary actor, or (comp/registry-key→class component-fully-qualified-symbol) to look a class up (for avoiding circular `require`s in Clojure).

    ::targeting/target explicit-target

    Option for targeting retuned result using data-targeting. Should be used with ::m/returning.

    ::uism/target-actor actor

    Helper that can translate an actor name to a target, if returning a result.

    ::uism/target-alias field-alias

    Helper that can translate a data alias to a target (ident + field).

    ::uism/ok-event event-id

    The SM event to trigger when the pessimistic mutation succeeds (no default).

    ::uism/error-event event-id

    The SM event to trigger when the pessimistic mutation fails (no default).

    ::uism/ok-data map-of-data

    Data to include in the event-data on an ok event

    ::uism/error-data map-of-data

    Data to include in the event-data on an error event

    ::uism/mutation-remote

    The keyword name of the Fulcro remote (defaults to :remote)

The mutation response follows the same rules as normal mutations (e.g. default-result-action). The result will also be in the ::uism/event-data under the ::uism/mutation-result key so that the ok-event and error-event handlers can simply look in event-data for the data sent back from the server.

Note
In general you should use actors as the mechanism for obtaining a return type’s class, since that will keep your state machine code decoupled from UI code.

13.8. Loads and State Machines

The API includes these functions for doing loads in the context of a running state machine:

(load env k component-class params)

Just like Fulcro’s load, but takes a SM env. Typically you should use actor-class or the component registry comp/registry-key→class to get the component class so you don’t couple UI code into your machine.

(load-actor env actor-name params)

(Re)load the given actor.

The params of these functions are the normal Fulcro load! options, but can include these additional special items:

::uism/ok-event

An event to send when the load is done (instead of calling a mutation)

::uism/ok-event-data

Extra parameters to send as event-data on the post-event.

::uism/error-event

The event to send if the load triggers a fallback.

::uism/error-event-data

Extra parameters to send as event-data on a fallback.

DEPRECATED (use the ok/error versions instead): ::uism/post-event:: An event to send when the load is done (instead of calling a mutation) ::uism/post-event-params:: Extra parameters to send as event-data on the post-event. ::uism/fallback-event:: The event to send if the load triggers a fallback. ::uism/fallback-event-params:: Extra parameters to send as event-data on a fallback.

13.8.1. Dynamic Actor Idents

There are all sorts of situations where you may not know the ident of an actor when the machine is first started, or perhaps the ident of an actor can change over time. The reset-actor-ident function can be used to update an actor.

Say you start the machine like this, with a (made-up) :none marker as an ID.

(uism/begin! this person-editing-machine ::personsm
  {:actor/selected-person (uism/with-actor-class [:person/id :none] Person)
   :actor/list            [:person-list :singleton})

You can later set the actor’s ident like so:

... ;; somewhere in the UI
(uism/trigger! this ::personsm :event/person-selected {:new-ident [:person/id 33]})

...

;; in the machine definition:
:event/person-selected
{::uism/handler (fn [{{:keys [new-ident]} ::uism/event-data :as env}]
                  (uism/reset-actor-ident env :actor/selected-person new-ident))}

13.9. Timer Events

Many UI interactions require some kind of timeout. For example, you don’t want to issue a load on an autocomplete search field until the user stops typing for 300ms, or perhaps you’d like to close a dialog and show an error if a data load takes more than 5 seconds.

The (uism/set-timeout env timer-id event-id event-data timeout cancel-on-events) function can be used in a handler to schedule a ms timer, where timer-id is a user-invented name for the timer (keyword), the event-id is the invented keyword for the event you want to send, event-data is additional data you’d like to send with the event, and timeout is in ms.

The cancel-on-events parameter is a function that will be sent the name of any event that occurs while the timeout is waiting. If it returns true (not false or nil) then the timeout will be auto-cancelled.

You can cancel a timeout with `(uism/clear-timeout! env timer-id)

13.10. Sending Events from one State Machine to Another

The mechanism for sending events from one state machine to another is the trigger method (with no !). This version takes and returns a handler env, and is composed into the threading of env in your handlers:

(fn [env]
  (-> env
    (uism/trigger :state-machine-id :event-id {:data :map})
    ...))

This has the effect of queueing the event for after the current handler has finished.

13.10.1. Nested Event Order

State machines that trigger events may cause handlers to run that themselves trigger further events. The ordering of such a cascade will be that of function call semantics. That is to say that if state machine A triggers an event on B and D, and B triggers an event on C, then the runtime evaluation order will be A, B, C, D.

13.10.2. Leveraging State Machine Local Storage

There are many times that you will want to send in "configuration" information to a state machine that should be remembered ov the lifetime of that machine. For example if you have code that triggers events in other state machines you might want to pass the ID of the other state machine as a parameter when you start the new one:

(uism/begin! this SM ::sm-id actor-map {:other-machine :machine-id})

You can then simply add a handler for the ::uism/started event that extracts this data and stores it in the state machine’s local store:

(uism/defstatemachine SM
  {...
   ::uism/states      {:initial
                       {::uism/events
                        {:uism/started
                         {::uism/handler
                          (fn [{::uism/keys [event-data] :as env}]
                            (let [{:keys [other-machine]} event-data]
                              (-> env
                                (uism/store :mid other-machine))))}}}}})

That value is then available via (uism/retrieve env key) for the lifetime of that state machine.

13.11. Aborting Loads and Mutations

The built-in Fulcro support for aborting network requests requires the use of the actual application. The general recommendation is to save your app into a defonce somewhere that can be required in any other file.

The state machine load/mutation system supports abort IDs by simply adding an :abort-id to the options map:

(uism/load env ::session (uism/actor-class env :session)
  {:abort-id         :abort/session-load
   ::uism/post-event :session-checked})

You can then explicitly cancel such a request in the normal way (via your app) inside of your state machine handlers:

...
  ::uism/handler
    (fn [env]
      (when @my-app
        (app/abort-request! my-app :abort/session-load))
      env)

The trigger-remote-mutation function also support abort IDs using the ::txn/abort-id key.

Tip
Aborting mutations is a risky business. Doing so leaves you in an unknown state since you don’t know how much the server actually saw or did; also, aborting the network request usually does not affect the server processing once the mutation has been sent.

13.12. Parting Thoughts

This relatively small set of primitives gives you quite a bit of power. Here are some things you can do with this system that you might not immediately realize:

  • Associate Multiple Machines with a Control

You might have a state machine that is interested in tracking something like the autocomplete status of a dropdown. Another state machine could be tracking the overall state of the form that the autocomplete is embedded in.

  • Create a Library of Reusable Machines

We’ve mentioned this, but it bears repeating. Common patterns exist all over the place.

Take an autocomplete dropdown. The behavior of waiting for some period of time between keystrokes before issuing a load, cancelling a load if the user starts typing again, showing/hiding the list of options and such can all be parameterized. The loads load an actor with parameters. This means the actual query and results for the load portion are controlled at begin!, not from within the state machine. Various other aspects are also easy to make "tunable" by using the state machine’s local storage:

(defstatemachine dropdown-autocomplete
  {::uism/actors #{:dropdown-control ...}
   ::uism/aliases {:options-visible? [:dropdown-control :ui/show-options?]
                   ...}
   ::uism/states
     {:initial {
       ::uism/events {::uism/started
                       {::uism/handler (fn [{::uism/keys [event-data] :as env}]
                                         (uism/store env :params event-data)
                                         ...
...

(uism/begin! this dropdown-autocomplete :dropdown-car-make-sm
  {:dropdown-control (uism/with-actor-class [:dropdown/id :car-make] Dropdown))}
  {:dropdown-key-timeout 200})

13.12.1. Form Validation

My initial experiments lead me towards the opinion that form validation does not generally belong in a state machine. Here are my reasons:

  • Rules around validation are often large and complex. This leads to a lot of states that become hard to follow.

  • The forms-state namespace in Fulcro does a nice job of tracking field state, letting you undo, diff, etc. It is much easier to "follow" validation at the UI layer, where it is also simpler to co-locate validity checks and messages with fields.

So, simple things like a login form might be ok to validate in the state machine, but larger forms should probably localize validation to the form itself.

There is one element that you will often need within the state machine: whether or not the form is currently valid.

Remember:

  • The env in the state machine handler includes the current Fulcro state map as the key ::uism/state-map.

  • You can use fdn/db→tree to convert a state map into a tree using a component’s class and ident.

  • You can get an actor’s component class with uism/actor-class.

  • You can get an actor’s ident with uism/actor→ident.

  • The form-state support in Fulcro can give you a validity check based on the form props.

You can also pass specific event data when you trigger events, so you can trigger your own state change events (instead of using uism/set-value!) that include the current validity of the form.

14. Dynamic Router

The dynamic router in Fulcro 3 uses dynamic queries to change the route, and was designed with the following requirements in mind:

Composition

Routers should compose in a flexible manner that includes the easy ability to refactor the application and restructure.

There should be a way to complete network operations before moving to a given route

This should be a chain of operations that is derived from the target routers themselves (or their children?).

UI Code should be able to prevent a route change

E.g. say there are unsaved changes on some page in a tree: a UI component must be able to "hook into" the routing system in order to prevent changes. NOTE: This is not a feature of the routers, but a feature of the content under a router. This can be modelled as a global concern, since routing (esp. that involves URI changes) is a global concern.

Code Splitting

A complete routing system for SPAs should make it easy to do code splitting at particular routes so that an initial load of the application need not load the code for every feature.

DRY

It should not be necessary to repeat the "route path" as an external data structure when it matches the UI structure. The UI composition itself can easily act as a "default" routing path structure.

URI <⇒ UI Routes should be flexible and able to "alias"

Reshaping the URI can be done by optional functions that sit in-between browser URI and code.

Navigable Code

During development it should be easy to see (as local concerns) what happens along a given UI route. Code navigation (jumping from root through subcomponents) should make it trivial to understand all routing concerns. Route "operations" like loading should be co-located with the components that act as routing targets.

Introspection

One should be able to query for the available routes (of loaded code), and the current visible route.

14.1. Routers

Dynamic routing in Fulcro can be easily facilitated by leveraging the UI query, which is a tool of composition that is always guaranteed to be present in a properly-structured application. Each component’s state will be normalized, and the class and relative UI position can be determined by examining the current UI query.

Take the following UI layout:

tree

The routers in this system can easily be autogenerated by a macro that is given nothing more than the classes of the components that are the targets of routing (i.e. User, Settings, etc.). The macro can simply compose them together into a component that has a dynamic query whose "current route" points to the first class listed (marked with D in the diagram). If the given router is to be shown on initial startup, then these default routing targets must be singletons (have an ident that does not depend on their props).

This delegates the novelty of routing targets to the target itself. Interestingly, this is quite convenient for composition and refactoring. The router is not programmed with any foreknowledge of the routing novelty of a target…​only it’s symbolic name!

(defrouter RootRouter [this props]
  {:router-targets [Settings User]})
(def ui-root-router (prim/factory RootRouter))

(defrouter SettingsRouter [this props]
  {:router-targets [Pane1 Pane2]})
(def ui-settings-router (prim/factory SettingsRouter))

The parameter list (this and props) are used with deferred routing, explained below.

The use of query scanning and dynamic queries for routing mean that you can easily add or remove a sub-route just by moving the symbol to a different router.

Such routers are simple Fulcro components, and can be composed into the UI just like any other components. The initial state parameters passed to such a router are forwarded to the first listed router target (if it has initial state).

Important
Routers are true singletons. A router ends up with a dynamic query keyed by the class itself. This means that a given router cannot be used in a UI in more than one place. This seems like a reasonable restriction given that they are so simple to declare at a given tree (sub)root, and are typically very positional in nature.

14.2. Routing Targets

Most of the novelty about routes can now be encoded into normal components with simple declarations.

(ns app
  (:require
    ...
    [fulcro.incubator.dynamic-routing :as dr :refer [defrouter]]))

(defsc X [this props]
 {...
  :route-segment   ["path"] ; the portions of the path that this component represents. typically just one, or one with params
  ;; optional: defaults the the component's ident (singletons only)
  :will-enter      (fn [app route-params] ...defer or immediate...)
  ;; optional, defaults to nothing
  :route-cancelled (fn [route-params] ...called if deferred route to here is cancelled before it completes...)
  ;; optional, defaults to true
  :will-leave      (fn [this props] ...return true or false to accept being removed from screen...)]}
 (dom/div ...))
:route-segment

A (relative) path segment that this component can "consume" from an incoming route. This is purely static data. The current composition of routing targets in the UI determines the overall "absolute" path of a route. Each router in the UI should be thought of as a stand-in for a "/" in an HTML5 URI path. MUST contain only strings and keywords.

:will-enter

A notification that this route target should be shown. Can return a value indicating a desire to do so immediately, or that it would like a delay (for some I/O). This method is called before the component is on-screen, so it cannot receive a react component instance. It is passed the app and router parameters which can be used to do things like issues loads and run mutations. Defaults to (route-immediate (get-ident class {}))

:route-cancelled

A notification that this route target was in a deferred state but the user made some other routing decision during that delay. This can be used to cancel heavy I/O operations for this target. Defaults to no-op.

:will-leave

A method that can prevent a route change that causes this component to leave the screen. This is called on the instance, so this and props are available. A request to change routes will signal this method from deepest child towards the parent, and will stop if any returns false. Optional.

Warning
will-enter SHOULD NOT side-effect (order of operations could cause strange behavior), but must instead do any I/O in the lambda passed to route-deferred. It must also trigger the dr/target-ready mutation to indicate that the route is ready. You can use plain transact! to start a mutation within route-deferred, and the mutation can use target-ready! to send the signal.

Route targets can be singletons or regular components that have multiple instances. In the latter case you must be sure that the ident returned from will-enter points to valid data in state by the time the route is resolved.

14.3. Initial Route

The dynamic routing relies on a call to change-route in order to start the routing system. Therefore you MUST make a call to change-route on start in order for the dynamic routers to work; however, there is also the concern of what gets rendered on the "first frame" of application mount.

Your top-most router will be in an "uninitialized" state on initial load. You can use the body of that router to render the "first frame" of your mounted app:

(defrouter RootRouter2 [this props]
  {:router-targets     [Settings User]}
  (case current-state
    :pending (dom/div "Loading...")
    :failed (dom/div "Failed!")
    ;; default will be used when the current state isn't yet set
    (dom/div "No route selected.")))

If you are doing SSR, then you will need to simulate calling change-route there. The function dr/ssr-initial-state (written, but untested) can be used to help you construct the proper state for a given path (which must be used for the server-side render, and also as the initial state for the client). Technically, this means that the function can also be used to generate initial state for the client on the front-end as well.

14.4. Route Segments and Changing Routes

UI Composition determines the available routes, and each route target must declare what part of the current "route" they can consume. The declaration is a vector of literal strings and keywords:

["user" :user-id]

Strings in the route segment MUST exactly match an incoming path prefix or the route does not match. The keyword parameters are route parameters, and capture the incoming route element as a string (this ensures that URI’s will work just as well as code-based paths that might contain other data types). Any data types you pass in the vector are converted via str, so if you need a more complicated coercion please do it before using it to change the route.

Path segments compose in the UI. In our earlier diagram the Settings component might have the route segments: ["settings"] and the User component ["user" :user-id]". The `Pane2 component might list ["pane1"]. Now, since the pane 1 component is currently nested as a target of the router underneath the settings component, we can derive that the full path to Pane 1 in this particular UI layout is ["settings" "pane1"]. This is the next critical step in our composition: Routers in a tree look for targets that can consume what remains of the path after parent targets have consumed the portion that matched those route segments.

Hopefully you can see how this directly matches the necessary logic for HTML5 URI routing. The following URIs are trivial to convert between the two forms:

"/settings/pane1"  <==>  ["settings" "pane1"]
"/user/1"          <==>  ["user" "1"]

This mechanism makes routing as simple as "read the URI, split the string, and call a function".

The function to cause a route change is:

(dr/change-route this ["user" "1"])

and it always starts from the root of your application and causes a full update of the correct route.

Notice that since the command to control the route is up to you, so is the path you pass to it. This makes it easy to do things like alias one path found in the URI to a different UI path, which is useful when you restructure the real UI but would like to maintain support for old paths that users may have bookmarked.

Additional useful functions are:

(current-route component-or-reconciler starting-component)

Returns a vector of the path components on the current (live) route starting at the given starting-component. If you use your root component it will be the absolute path, and using some other component router will give the relative path from there.

(change-route-relative this-or-reconciler relative-class-or-instance new-route timeouts)

Just like change-route, but can take a relative new-route and apply it starting and the given relative-class-or-instance. Thus, some module of a program can route in a relative manner which will further decouple the components, making it easier to use a module in a development card or refactor it to a different location in the app without breaking local concerns.

Note
This library will not have any code that connects HTML5 routing events to UI routing. That is a relatively simple exercise and there are plenty of libraries that can help with the task. The logic of transforming a URI to the correct vector and calling a function is trivial, and the concern of aliasing and legacy path transforms is something you will likely want to put in the middle of that.

14.5. Aborting a Route Change

The :will-leave is called when a target is going to leave the screen, and may return false. If it does so AND is active on the screen then it prevents the entire route change. This allows a screen to hold up routing in case edits would be lost, etc. Of course you should do something in this method to change the UI so the user knows what is going on. This is a non-static method and receives the component, so it can transact!, etc.

TODO: Needs to be a bit more capable: the "route being attempted" should be available somehow in case the component wants to save it for a later "continue" operation (e.g. "Are You Sure?", "Yes"). Perhaps as a retry lambda that is passed in as a parameter.

14.6. Deferred Routing

There are times when you want to delay a route change based on some I/O operation, like a load or mutation. A router can do this via the return value of the will-enter method:

(df/route-deferred ident f)

Record the fact that the route wants to change, but don’t actually apply it. The ident passed should be the ident of the component that should be routed to (of the current type). The function f will be called to allow you to issue instructions that will complete the route.

(df/route-immediate ident)

Immediately apply the route for this router.

Of course you should not do immediate routing if the ident you’re returning does not point to something that already exists in the database. Perhaps you need to load it.

Pending routes can be completed by calling the dr/target-ready mutation with a target parameter that matches the ident you passed with route-deferred. For example, say you wanted to load a user before routing to them:

(defsc User [this props]
  {:query     [:user/id :user/name]
   :ident     [:user/id :user/id]
   :route-segment ["user" :user-id])
   :route-cancelled (fn [{:keys [user-id]}] (my-abort-load (keyword "user" user-id)))
   :will-enter      (fn [app {:keys [user-id]}]
                      (when-let [user-id (some-> user-id (js/parseInt))]
                        (dr/route-deferred
                          [:user/id user-id]
                          #(df/load app [:user/id user-id] User {:post-mutation        `dr/target-ready
                                                                 :post-mutation-params {:target [:user/id user-id]}}))))]
  (dom/div ...))

Note that the route parameters come in via a map keyed by the keyword in your route-segment. Remember that the value of these elements is guaranteed to be a string, so be sure you coerce them if you need them to be a different type.

Important
The will-enter method MUST return the value of a call to either route-immediate or route-deferred.

14.6.1. Target Ready within Mutations

The mutation target-ready is meant for use with loads as a post-mutation. The assumption is that there will be some natural delay before it is called.

If you need to signal that a target is ready from within a mutation (instead of in the lambda of the route-deferred), please use the following:

(defmutation do-stuff-and-finish-routing [params]
  (action [{:keys [app]}]
    (dr/target-ready! app ident-of-target)))

earlier versions had a mutation helper, but that is insufficient in this case and that helper was useless (and it was removed).

14.6.2. Router Rendering of a Deferred UI

The router uses a state machine internally and sets two timeouts with respect to deferred routes: and :error-timeout, and a :deferred-timeout (which can be sent with your calls to change-route). The error timeout is how long a route can be deferred before it moves to the :failed state, and the deferred timeout is how long a route can be deferred before it moves to a :pending state.

The router can be defined with custom UI for these various states using the defrouter macro, which looks much like defsc, but only allows :router-targets in the options map:

(defrouter MyRouter [this {:keys [current-state pending-path-segment route-factory route-props]}]
  {:router-targets [A B C]}
  (case current-state
    :initial (dom/div "What to show when the router is on screen but has never been asked to route")
    :pending (dom/div "Loading...")
    :failed (dom/div "Oops")}))
Note
If you do not specify a body for the defrouter (e.g. (defrouter A [_ _] {:router-targets [X Y]})), then it will render whatever route is currently active instead of this "alternate" context-sensitive UI.

this is a real Fulcro component instance and turns into a defsc, but the body is only rendered in the initial/pending/failure states to do whatever you deem necessary. The options map will be passed through to defsc (though query/ident/protocols/initial-state will be overridden), so you can define React lifecycle methods and such if that is useful for your particular use-case. See the next section for critical notes, though.

The incoming props are actually generated to include a number of useful things:

  • :current-state - The current (transitory) state of the router (:initial, :pending, or :failed)

  • :pending-path-segment - Where the router is trying to go in the :pending and :failed state. This will be the concrete path segment that was requested (e.g. ["user" "1"] and not ["user" :user/id]).

  • :route-props - The props for the current (old, non-pending) route (if there was a route on-screen).

  • :route-factory - The function to call to render the current (old, non-pending) route (if there was a route on-screen).

The route factory/props are useful if you want to continue to render the "current" page even though a timeout has occurred, but perhaps you want to pass some computed data to indicate progress (e.g. (route-factory (prim/computed route-props {:waiting true}))). Of course, you can instead route-immediate, skip timeouts altogether, and do normal load markers on the target screen. The former method is useful on a deferred route that simply has nothing to render until the data is present. Regular named load markers are also a good way of giving user feedback.

You can specify overrides for the default timeouts when you call change-route:

(change-route this ["new" "path"] {:error-timeout 2000 :deferred-timeout 200})

They default to 5000ms and 100ms.

Note
A deferred route that resolves after an error timeout can still auto-recover if target-ready is called after the error timeout (it will move to the correct resolved route and stop showing the error).
Note
A request to change the route when a deferred route was in progress will cancel the timeouts and immediately attempt the new route.
Router React Lifecycle Methods

The props seen in react lifecycles will not be what you see in the props of the router body. The props of the router body are synthesized for your convenience, but raw react lifecycles will see the low-level internal props of the router instead. The id of the router is the same as the ID for the router’s UI state machine. Using uism/current-state on that ASM will give you the current route state, and looking in that ASM’s local store will give you things like the pending segment. The ID of the active state machine will be the keyword name of the router:

(defrouter MyRouter [this props]
  {:router-target ...
   :componentDidUpdate (fn [this pprops pstate]
      (let [current-state (uism/current-state this :MyRouter)
            route-props (:fulcro.incubator.dynamic-routing/current-route pprops)]
         ...))
   ...

14.6.3. Code Splitting

The route defer mechanism should be sufficient to implement code splitting, where the routing target is the "join point" for the dynamic code. Basically the component would not include the code-split child in the query or UI initially, but could trigger a code load and defer routing (storing the ident in a place where the loaded code could trigger the completion of the route, and a dynamic query change of the original component to point to the newly loaded component).

Something like:

(defsc CodeSplit [this props]
  {:ident     (fn [] [:CodeSplit 1])     ; constant ident
   :query     [{:loaded-component ['*]}] ; a placeholder join. Set dynamically after code load
   :initial-state {:loaded-component {}} ; placeholder state data
   :will-enter (fn [app route-params]
                 (dr/route-deferred [:CodeSplit 1]
                   (fn []
                     (loader/load :module)
                     ;; the loaded code would issue a target-ready mutation once loaded
                     ;; the loaded code would also use set-query to change the query of CodeSplit
                     )))}
   ...
   ;; The DOM can use the component registry to find the component that should be rendered
   (when-let [cls (comp/registry-key->class :some.loaded.ns/Component)
              factory (comp/factory cls)]
      (factory loaded-component)))

TODO: A dynamic code load means that there may be path segments in the current route that cannot be evaluated until the code load is complete. It may be necessary to "re-trigger" a route after a code load to ensure that the path segments have been fully evaluated. This would be a good use of a relative change route function, which could be run on the newly-loaded sub-components with the remaining path. I think it should be relatively easy to just defer the rest of the sub-routing until the given route is resolved…​that is probably best, as it doesn’t require user intervention. The problem with that is that sub-routes may also want to queue I/O, and getting it all queued at once might be preferable to delaying. We could support something like route-blocked which would resume routing after the ready signal, and allow the route-deferred to continue down the route resolving sub-paths and queuing I/O. Undecided.

14.7. Live Router Example

The router example below uses our people server to demonstrate the various concepts covered in this chapter. It loads people by ID (the buttons could be HTML URL segments). If the loading is slow, it shows a load marker. If the route changes before the user is loaded then a proper console message prints to show you that you could have handled that to cancel network operations.

Make sure you use the server controls in the upper-right corner to see the difference between a fast server (under 100ms) and a slow one. Also be sure to crank it up to a large number so you can try moving to a different route while the user is loading.

Finally, the Person route includes a checkbox that you can check. When checked the Person component will prevent all route changes to simulate what you might want to do in a dirty form situation.


(ns book.dynamic-router-example
  (:require
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.routing.dynamic-routing :as dr :refer [defrouter]]
    [com.fulcrologic.fulcro.dom :as dom]
    [taoensso.timbre :as log]
    [com.fulcrologic.fulcro.data-fetch :as df]
    [com.fulcrologic.fulcro.mutations :as m]))

(defsc Settings [this props]
  {:ident         (fn [] [:component/id ::settings])
   :query         [:settings]
   :initial-state {:settings "stuff"}
   :route-segment ["settings"]
   :will-enter    (fn [app route-params]
                    (log/info "Will enter settings with route params " route-params)
                    (dr/route-immediate [:component/id ::settings]))
   :will-leave    (fn [this props]
                    (js/console.log (comp/get-ident this) "props" props)
                    true)}
  (dom/div "Settings"))

(defsc Person [this {:ui/keys      [modified?]
                     :person/keys  [id name]
                     :address/keys [city state]
                     :as           props}]
  {:query           [:ui/modified? :person/id :person/name :address/city :address/state]
   :ident           :person/id
   :route-segment   ["person" :person/id]
   :route-cancelled (fn [{:person/keys [id]}]
                      (log/info "Routing cancelled to user " id))
   :will-leave      (fn [this {:ui/keys [modified?]}]
                      (when modified?
                        (js/alert "You cannot navigate until the user is not modified!"))
                      (not modified?))
   :will-enter      (fn [app {:person/keys [id] :as route-params}]
                      (log/info "Will enter user with route params " route-params)
                      ;; be sure to convert strings to int for this case
                      (let [id (if (string? id) (js/parseInt id) id)]
                        (dr/route-deferred [:person/id id]
                          #(df/load app [:person/id id] Person
                             {:post-mutation `dr/target-ready
                              :post-mutation-params
                                             {:target [:person/id id]}}))))}
  (dom/div
    (dom/h3 (str "Person " id))
    (dom/div (str name " from " city ", " state))
    (dom/div
      (dom/input {:type     "checkbox"
                  :onChange (fn []
                              (m/toggle! this :ui/modified?))
                  :checked  (boolean modified?)})
      "Modified (prevent routing)")))

(def ui-person (comp/factory Person {:keyfn :person/id}))

(defsc Main [this props]
  {:ident         (fn [] [:component/id ::main])
   :query         [:main]
   :initial-state {:main "stuff"}
   :route-segment ["main"]
   :will-enter    (fn [app route-params]
                    (log/info "Will enter main" route-params)
                    (dr/route-immediate [:component/id ::main]))
   :will-leave    (fn [this props]
                    (log/info (comp/get-ident this) "props" props)
                    true)}
  (dom/div "Main"))

(defrouter TopRouter [this {:keys [current-state pending-path-segment]}]
  {:router-targets [Main Settings Person]}
  (case current-state
    :pending (dom/div "Loading...")
    :failed (dom/div "Loading seems to have failed. Try another route.")
    (dom/div "Unknown route")))

(def ui-top-router (comp/factory TopRouter))

(defsc Root [this {:root/keys [router]}]
  {:query         [{:root/router (comp/get-query TopRouter)}]
   :initial-state {:root/router {}}}
  (dom/div
    (dom/button {:onClick #(dr/change-route this ["main"])} "Go to main")
    (dom/button {:onClick #(dr/change-route this ["settings"])} "Go to settings")
    (dom/button {:onClick #(dr/change-route this ["person" "1"])} "Go to person 1")
    (dom/button {:onClick #(dr/change-route this ["person" "2"])} "Go to person 2")
    (ui-top-router router)))

(defn client-did-mount
  "Must be used as :client-did-mount parameter of app creation, or called just after you mount the app."
  [app]
  (dr/change-route app ["main"]))

15. Fulcro and GraphQL

GraphQL is rapidly becoming the standard for writing graph-based servers. As a result there are many existing ways to deploy such an API, and many businesses have decided that their back-end will simply use the standard. There are BaaS (back-end as a service) providers that also let you quickly define and deploy a GraphQL API.

All of this makes GraphQL an attractive server-side solution for Fulcro.

Luckily, Pathom has all of the tools necessary to make this integration simple and easy!

You can see some of it in action in one of the Fulcro video series, and read about configuring it in the Pathom Documentation.

The simplest integration is to simply use Pathom’s GraphQL remote:

(ns app
  (:require
    ...
    [com.wsscode.pathom.fulcro.network :as pfn]))

(def client (fulcro/make-fulcro-client
              {:networking {:remote (pfn/graphql-network
                                      {::pfn/url (str "https://api.github.com/graphql?access_token=" token)})}})

The more powerful and extensible method allows you to combine GraphQL access with Pathom Connect. This gives you very powerful features, including many useful ones that GraphQL doesn’t support natively!

16. Dynamic Queries

Fulcro supports 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 necessary for code splitting since the parent component won’t be able to compose in the child’s query until it is loaded.

16.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 (comp/factory Thing)) ; query ID is based solely on the class itself (with no qualifier)
(def ui-thing-1 (comp/factory Thing {:qualifier :a})) ; query ID is derived from Thing plus the qualifier :a

(defsc Parent [this props]
  {:query (fn [] [{:child (comp/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:

Example 38. Dynamic Query

(ns book.queries.dynamic-queries
  (:require
    [com.fulcrologic.fulcro.dom :as dom]
    [goog.object]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.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 [] (comp/set-query! this ui-leaf {:query [:x]}))} "Set query to :x")
    (dom/button {:onClick (fn [] (comp/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 (comp/factory Leaf {:qualifier :x}))

(defsc Root [this {:keys [root/leaf] :as props}]
  {:initial-state (fn [p] {:root/leaf (comp/get-initial-state Leaf {})})
   :query         (fn [] [{:root/leaf (comp/get-query ui-leaf)}])}
  (dom/div (ui-leaf leaf)))

(ns book.queries.dynamic-query-parameters
  (:require
    [com.fulcrologic.fulcro.dom :as dom]
    [goog.object]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]))

; This component has a query parameter that can be set to whatever we want dynamically
(defsc Leaf [this {:keys [x y] :as props}]
  {:initial-state (fn [params] {:x 1 :y 99})
   :query         (fn [] '[:x ?additional-stuff])           ; the parameter ?additional-stuff starts out empty
   :ident         (fn [] [:LEAF :ID])}
  (dom/div
    (dom/button  {:onClick (fn [] (comp/set-query! this Leaf {:params {:additional-stuff :y}}))} "Add :y to query")
    (dom/button  {:onClick (fn [] (comp/set-query! this Leaf {:params {}}))} "Drop :y from query")
    (dom/ul
      (dom/li  "x: " x)
      (dom/li  "y: " y))))

(def ui-leaf (comp/factory Leaf))

(defsc Root [this {:keys [root/leaf] :as props}]
  {:initial-state (fn [p] {:root/leaf (comp/get-initial-state Leaf {})})
   :query         (fn [] [{:root/leaf (comp/get-query ui-leaf)}])}
  (dom/div  (ui-leaf leaf)))

16.2. Setting a Query

The whole point of a dynamic query is being able to set the query to something new on a component. This has one critical component that you must be very careful with: normalization. A component’s query is augmented with metadata the marks which component is used to normalize (via idents) the entity found for the given portion of the query that it handles. Therefore, you must still use get-query to pull the query for portions of your subquery that you wish to include when setting a new query. If done incorrectly then normalization will not work correctly on these components.

The top-level metadata will be added for you, so this is fine:

(defmutation change-a-query [_]
  (action [{:keys [state]}]
    (swap! state comp/set-query* B {:query [:MODIFIED]}))

but as soon as there’s a join you must ensure the subqueries have the correct metadata. This means that the following is incorrect (because [:x] probably goes with some other normalized component):

(defmutation change-a-query [_]
  (action [{:keys [state]}]
    (swap! state comp/set-query* B {:query [:MODIFIED {:subquery [:x]}]}))

instead, you should use get-query with a state parameter to pull the query for a given component. So, say you wanted to change the query for this:

(defsc X [_ _]
  {:query [:id :name]
   :ident [:x/id :id]})

(defsc A [_ _]
  {:query [:id {:x (comp/get-query X)}]
   :ident [:a/id :id]})

and you want to change both component’s queries. The correct form for the resulting mutation is:

(defmutation change-a-query [_]
  (action [{:keys [state]}]
    (swap! state (fn [s]
                    (as-> s state-map
                      (comp/set-query* state-map X {:query [:id :address]})
                      (comp/set-query* state-map A {:query [:id :new-prop {:x (comp/get-query X state-map}]}))))))

There is also a top-level API call comp/set-query! if you want to change a query outside of a mutation.

17. Legacy UI Routers

Note
This chapter describes routers from Fulcro 2 that are a bit harder to use and less feature-rich than the Dynamic Router. The basic union-based router is a great router for performance reasons, but is only concerned with swapping between queries/views at a single layer and nothing else.

UI Routing is an important task in Fulcro, as it is an important part of keeping your application running quickly. You see, any given sub-tree of your application can query for quite a bit of data and very often a good portion of the side branches are not currently on-screen. If your entire query runs on every render frame then things can become slow. The routers in Fulcro route not only the visible DOM, but also the subquery that needs to run. It does that by using either dynamic queries or union queries with to-one relations to keep the query targeted to just the active UI.

Unfortunately many people find hand-writing union components a little 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.

17.1. A Basic Router

A basic router looks like this:

(ns app
  (:require [com.fulcrologic.fulcro.routing.legacy-ui-routers :refer [defrouter defsc-router]]) ; requires 2.6.18 for defsc-router

(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}}
  ...)

;; Pre-2.6.18
(defrouter RootRouter :root/router
  ; OR (fn [t p] [(:router/page p) (:db/id p)])
  [:router/page :db/id]
  :PAGE/index Index
  :PAGE/settings Settings)

;; Post 2.6.18
(defsc-router RootRouter [this {:keys [router/page db/id]]
 {:router-id :root/router
  :ident (fn [] [page id]) ; props are destructured in the context of the route TARGET
  :default-route Index
  :router-targets {:PAGE/index Index
                  :PAGE/settings Settings}}
  (dom/div "What to render if the route is bad"))

The newer form lets you define React lifecycle methods, and also lets you define what to render when the route is invalid (the body is only rendered under that circumstance, otherwise is renders the routed component). It can also be set to resolve like defsc in Cursive, so you get proper symbol resolution.

See old versions of the book in Fulcro’s git history for docs on the old defrouter.

17.1.1. Details on defsc-router

The required parameters of defsc-router are:

:router-id

An ID to give the router

:ident

MUST be in fn form, and can use destructured parameters from props (which are in the context of the route target)

:default-route

The component that should be shown by default. Should have initial state so it exists on app start.

:router-targets

A map from the component’s Fulcro database table to the component that can be routed to. Routing switches on the FIRST element of the ident (the table name). This is because unions are used, and this allows sub-screens to have ID’s that vary. For example, you could router to [:user/id 44] by adding :user/id User as an entry to this map and making sure the ident function can resolve the ident correctly for the props of a user.

You may also include most other defsc options, other than :query and :initial-state, which are set internally.

The optional body of defsc-router is what to render if the route is bad (e.g. the ident in state for the route doesn’t point to a valid routing target).

Example 40. Simple Router

(ns book.simple-router-1
  (:require
    [com.fulcrologic.fulcro.routing.legacy-ui-routers :as r :refer-macros [defsc-router]]
    [com.fulcrologic.fulcro.dom :as dom]
    [com.fulcrologic.fulcro.data-fetch :as df]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.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"))

(defsc-router RootRouter [this {:keys [router/page db/id]}]
  {:router-id      :root/router
   :default-route  Index
   :ident          (fn [] [page id])
   :router-targets {:PAGE/index    Index
                    :PAGE/settings Settings}}
  (dom/div "Bad route"))

(def ui-root-router (comp/factory RootRouter))

(defsc Root [this {:keys [router]}]
  {:initial-state (fn [p] {:router (comp/get-initial-state RootRouter {})})
   :query         [{:router (comp/get-query RootRouter)}]}
  (dom/div
    (dom/a {:onClick #(comp/transact! this
                        `[(r/set-route {:router :root/router
                                        :target [:PAGE/index 1]})])} "Index") " | "
    (dom/a {:onClick #(comp/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.

17.1.2. Rendering a "Bad Route"

If the app state doesn’t point to a proper router target then the router won’t know what to render. This is the purpose of the body of the defsc-router.

(defsc-router Router [this props]
  {...}
  (dom/div "Bad route!"))

It is very important to note that the props arg can be destructured in the context of a route target, but when you’re rendering the "bad route" the props will be the props of the router itself. We recommend you use a pattern like this if you want to examine the "current route" in the body of the "bad route" rendering:

(defsc-router Router [this props]
  {...}
  (let [{:com.fulcrologic.fulcro.routing.legacy-ui-routers/keys [id current-route]} (comp/props this)]
    (dom/div "Bad route: " current-route)))

but most likely the current route is just going to be nil, since you’re looking at denormalized props, and the ident that is in state is pointing at something wrong. So, instead you can pull the state map and see what the bad ident is instead:

(ns ...
  (:require [com.fulcrologic.fulcro.routing.legacy-ui-routers :as r :refer [defsc-router]]))

(defsc-router Router [this props]
  {...}
  (let [state-map (comp/component->state-map this)
        {:com.fulcrologic.fulcro.routing.legacy-ui-routers/keys [id]} (comp/props this)]
        current-route (get-in state-map [r/routers-table ::r/id id ::r/current-route])]
    (dom/div "Bad route ident: " current-route)))

of course you probably don’t want to render that to the user, but in case you find it useful at least you know how to get it.

17.1.3. Composing Routers

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:

Example 41. Nested Router

(ns book.simple-router-2
  (:require
    [com.fulcrologic.fulcro.routing.legacy-ui-routers :as r :refer-macros [defsc-router]]
    [com.fulcrologic.fulcro.dom :as dom]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.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"))

(defsc-router SettingsRouter [this {:keys [router/page db/id]}]
  {:router-id      :settings/router
   :ident          (fn [] [page id])
   :router-targets {:PAGE/email EmailSettings
                    :PAGE/color ColorSettings}
   :default-route  EmailSettings}
  (dom/div "Bad route"))

(def ui-settings-router (comp/factory SettingsRouter))

(defsc Settings [this {:keys [router/page db/id subpage]}]
  {:query         [:db/id :router/page {:subpage (comp/get-query SettingsRouter)}]
   :ident         (fn [] [page id])
   :initial-state (fn [p]
                    {:db/id       1
                     :router/page :PAGE/settings
                     :subpage     (comp/get-initial-state SettingsRouter {})})}
  (dom/div
    (dom/a {:onClick #(comp/transact! this
                        `[(r/set-route {:router :settings/router
                                        :target [:PAGE/email 1]})])} "Email") " | "
    (dom/a {:onClick #(comp/transact! this
                        `[(r/set-route {:router :settings/router
                                        :target [:PAGE/color 1]})])} "Colors")
    (js/console.log :p (comp/props this))
    (ui-settings-router subpage)))

(defsc-router RootRouter [this {:keys [router/page db/id]}]
  {:router-id      :root/router
   :ident          (fn [] [page id])
   :default-route  Index
   :router-targets {:PAGE/index    Index
                    :PAGE/settings Settings}}
  (dom/div "Bad route"))

(def ui-root-router (comp/factory RootRouter))

(defsc Root [this {:keys [router]}]
  {:initial-state (fn [p] {:router (comp/get-initial-state RootRouter {})})
   :query         [{:router (comp/get-query RootRouter)}]}
  (dom/div
    (dom/a {:onClick #(comp/transact! this
                        `[(r/set-route {:router :root/router
                                        :target [:PAGE/index 1]})])} "Index") " | "
    (dom/a {:onClick #(comp/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:

(comp/transact! this `[(r/set-route ...) (r/set-route ...)])

17.2. Routing Tree Support

Note
Fulcro Incubator’s Dynamic Routers automatically compose and understand paths, so there is no need to declare a routing tree with them. You might want to try those as an alternative.

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.

17.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 com.fulcrologic.fulcro.routing.legacy-ui-routers to r
(comp/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.

17.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.

17.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 [com.fulcrologic.fulcro.routing.legacy-ui-routers :as r]
            [com.fulcrologic.fulcro.components :as comp]))

(r/defsc-router SomeRouter [this props]
 {:router-id :top-router
  :ident (fn [] ...)
  :default-route HomePage
  :router-targets {: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
     ...))

17.5. A Complete UI Routing Example

The following shows the example routing tree in a complete running demo:

Example 42. Routing Demo

(ns book.ui-routing
  (:require
    [com.fulcrologic.fulcro.routing.legacy-ui-routers :as r :refer-macros [defsc-router]]
    [com.fulcrologic.fulcro.dom :as dom]
    [com.fulcrologic.fulcro.data-fetch :as df]
    [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
    [com.fulcrologic.fulcro.mutations :as m]))

(defsc Main [this {:keys [label] :as props}]
  {:initial-state {:page :main :label "MAIN"}
   :ident         (fn [] [(:page props) :top])
   :query         [:page :label]}
  (dom/div {:style {:backgroundColor "red"}}
    label))

(defsc Login [this {:keys [label] :as props}]
  {:initial-state {:page :login :label "LOGIN"}
   :ident         (fn [] [(:page props) :top])
   :query         [:page :label]}
  (dom/div {:style {:backgroundColor "green"}}
    label))

(defsc NewUser [this {:keys [label] :as props}]
  {:initial-state {:page :new-user :label "New User"}
   :ident         (fn [] [(:page props) :top])
   :query         [:page :label]}
  (dom/div {:style {:backgroundColor "skyblue"}}
    label))

(defsc StatusReport [this {:keys [id page]}]
  {:initial-state {:id :a :page :status-report}
   :ident         (fn [] [page id])
   :query         [:id :page :label]}
  (dom/div {:style {:backgroundColor "yellow"}}
    (dom/div (str "Status " id))))

(defsc GraphingReport [this {:keys [id page]}]
  {:initial-state {:id :a :page :graphing-report}
   :ident         (fn [] [page id])
   :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))))

(defsc-router ReportRouter [this props]
  {:router-id      :report-router
   :ident          (fn [] [(:page props) (:id props)])
   :default-route  StatusReport
   :router-targets {:status-report   StatusReport
                    :graphing-report GraphingReport}})

(def ui-report-router (comp/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 [page 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 (comp/get-initial-state ReportRouter {})})
   :ident         (fn [] [page :top])
   :query         [:page {:report-router (comp/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)))

(defsc-router TopRouter [this props]
  {:router-id      :top-router
   :default-route  Main
   :ident          (fn [] [(:page props) :top])
   :router-targets {:main     Main
                    :login    Login
                    :new-user NewUser
                    :report   ReportsMain}})

(def ui-top (comp/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 com.fulcrologic.fulcro.routing.legacy-ui-routers as r.
  {:initial-state (fn [params] (merge routing-tree
                                 {:top-router (comp/get-initial-state TopRouter {})}))
   :query         [r/routing-tree-key
                   {:top-router (comp/get-query TopRouter)}]}
  (dom/div
    ; Sample nav mutations
    (dom/a {:onClick #(comp/transact! this `[(r/route-to {:handler :main})])} "Main") " | "
    (dom/a {:onClick #(comp/transact! this `[(r/route-to {:handler :new-user})])} "New User") " | "
    (dom/a {:onClick #(comp/transact! this `[(r/route-to {:handler :login})])} "Login") " | "
    (dom/a {:onClick #(comp/transact! this `[(r/route-to {:handler :status :route-params {:report-id :a}})])} "Status A") " | "
    (dom/a {:onClick #(comp/transact! this `[(r/route-to {:handler :graph :route-params {:report-id :a}})])} "Graph A")
    (ui-top top-router)))

17.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:

(comp/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/id report-id :load-time-ms] 0)
          is-missing? (= 0 when-loaded)
          now-ms (.getTime (js/Date.))
          age-ms (