Minimalist Fulcro Tutorial

Author: Jakub Holý & contributors

This is a minimalistic introduction to Fulcro that focuses on HOW and not WHY (aside of a short section). The goal is to enable you to read, understand, and tinker with the code of a Fulcro application.

Your learning journey

You have now set out on a three part journey:

  1. Study this tutorial to gain enough essential knowledge to be able to read, understand, and tinker with Fulcro applications and solidify it by doing the accompanying minimalist Fulcro tutorial exercises.

  2. Learn by modifying an existing app:

  3. Build your own app!

Prerequisites

This tutorial expects that you have gone through an EQL Tutorial (don’t worry, it is simple!) and are familiar with EQL, queries, joins (property and ident ones). It will not work without that knowledge. You also need to have an idea about Pathom v2 (see below).

It also assumes that you are already somewhat familiar with React and thus with concepts such as elements, components and props, factories, handling of events, styling of elements, a tree of components, passing "props" from the root component, UI as a function of data. (If you know what these are then you are good. A cursory knowledge of React should suffice for now, unless you want to do something fancy. Later on you might also want to learn about local state and lifecycle methods and hooks but you can get on without these for a while.)

It is helpful to know a little about the principles of GraphQL (see this 3 Minute Introduction to GraphQL and, if curious, also this GraphQL is the better REST for more insight into the value proposition of GraphQL and EQL).

Briefly about Pathom

It is useful to read the Pathom v2 introduction and learn about Pathom resolvers but if you are short on time, the minimum necessary knowledge is presented here.

Fulcro fetches data via EQL queries and changes them via EQL mutations. Pathom is the server-side part that understands and answers these queries and applies these mutations. Its key element are resolvers that return a (possibly nested) map representing the data they have been asked for and possibly take an input and/or parameters. For our purposes, we care only about global resolvers and ident resolvers, as demonstrated below:

;; A *global resolver* has no input; we can simply query for [:all-people] and get them
(pc/defresolver all-people [env _]
    {::pc/input  #{} ; declare inputs
     ::pc/output [{:all-people [:person/id :person/name]}]} ; declare the output map's props
    ; OR: {:all-people (db/query env "select id, name from person" :as-maps)}
    {:all-people [{:person/id 1 :person/name "Diane"}, {:person/id 2 :person/name "BoJack"}]})

;; An *ident resolver* needs and ID as its input
(pc/defresolver person-by-id [env {:keys [person/id]}]
    {::pc/input  #{:person/id}
     ::pc/output [:person/id :person/name]}
    (case id
      1 {:person/id 1 :person/name "Diane"}
      2 {:person/id 2 :person/name "BoJack"}
      ;; for any other id, fetch from the DB using a hypothetical fn:
      (fetch-person-from-db env id)))

All the resolvers must be registered with a Pathom "parser" and thus available to be used to fulfill queries. All resolvers can also take additional, optional parameters, which are used to implement concerns such as pagination. (And you don’t really need to know more than that it exists.)

A word of warning

Fulcro is likely very different from any other web framework you have worked with before, even though there are intersections with various technologies (React, GraphQL). According to our experience, it is most advisable that you familiarize yourself well with its key concepts, presented below. Jumping into "learning by doing" straight away does not tend to work all that well, even if it otherwise is your preferred mode of learning. But don’t worry, we will try to get you coding as soon as possible.

Why Fulcro?

Fulcro was born out of the search for sustainable software development of full-stack, non-trivial (web) applications. In other words, it aims to keep complexity under control so that it does not explode as your codebase grows and time goes. It also distinguishes itself by the focus on developer-friendliness: related things are located together and it is easy to navigate to all important code artifacts (via Command/Control-click, instead of being forced to search for string IDs or keywords).

This led to the following decisions:

  • UI = f(state) - React came with the idea that the UI is just a pure function of data but Fulcro really means it. When you have a problem, look at the data, not the UI. 99% of the time it is there.

  • Components declare their data needs ("query") - because nobody else knows or should care about what data the component needs. And these queries are composable so that we can fetch the data needed by the whole UI (sub)tree at once.

  • Graph API: The UI is a a tree (i.e. a graph) of components and therefore the composed query is also a tree. The server can understand and fulfill such a graph query with a tree of data - exactly the data the UI needs. Not the mess of N separate REST endpoints that you need to query individually and combine and prune the data on the frontend. (Reportedly, a perfectly designed REST APIs do not suffer from this problem. But they are rarer than unicorns.)

  • Web applications are inherently full-stack and thus a framework should provide an integrated solution for fetching data from the server - including the ability to track its status - and for triggering actions ("mutations") with potentially both local and remote constituents. We should not pretend that this is not our problem, as many frameworks do.

  • Normalized client-side DB: Even though the UI needs a tree of data and the server returns just that, we want the data cached in a normalized cache - which we call client DB - on the frontend. For decades, this has been the established best practice for data access in databases, and for good reasons. In particular, it prevents a whole class of issues with out-of-sync data. When we mutate a particular piece of data, we want the new value reflected everywhere where it is used, without having to manually go through all those places. And such a normalized database also makes it trivial to find just the piece of data you want to change (all you need is the entity’s name, its ID value, and the property name).

Fulcro is also quite well designed. It is based on a small set of orthogonal building blocks and it doesn’t hide anything from you - you can always go a level deeper into its internals to achieve what you need (as long as you are aligned with its overall philosophy). Its flexibility and customizability is surprising - all of the critical features from network interaction to rendering optimization are easily customizable. It is also very careful about providing only the tools that are generally applicable and avoiding "features" that might help some people but stand in the way of others. But thanks to the aforementioned flexibility, you can implement what you need for your unique use case yourself.

To learn more about the reasoning behind Fulcro, listen to the ClojureScript Podcast S4 E6 Fulcro with Tony Kay (Part 1) (2020), which explores the origins of and key motivation for Fulcro, and read the Ch. 4. Getting Started of the Fulcro Developers Guide, which demonstrates how various problems are made easier thanks to the way Fulcro is. To learn to use Fulcro, read on :-).

An overview of Fulcro

Fulcro is a full-stack web framework. These are the main components:

Fulcro system view
Figure 1. Fulcro system view
  1. Frontend

    1. UI - Fulcro/React components render a DOM and submit mutations (= action name + parameters) to the transaction (Tx) subsystem

    2. Tx (transaction subsystem) - asynchronously executes local mutations and sends remote mutations and queries to the remote backend

    3. Client DB - data from the backend is normalized into the client-side DB (data cache); Tx typically schedules a re-render afterwards

  2. Backend

    1. Pathom receives EQL queries and mutations and responds with a data tree

Key concepts and elements

We will briefly describe the key terms we are going to use a lot. Some are later explained in more detail. Skim over them and then come back to this section after having read about Fulcro lifecycle and whenever you are unsure what any of these terms means.

App

A reference to the current Fulcro application, containing configuration, the client DB, etc. Produced by app/fulcro-app and used when calling transact! or load! when a component’s this is not available. Referred to as app in code samples.

Client DB

The client-side cache of data. It is a map of maps:

Entity name → entity id value → properties of the entity

For convenience, we use the name of the id property as the "name" of the entity - thus :person/id, :user/username. An example of a client DB:

{:person/id  {123 {:person/id 123, :person/fname "Jo", :person/address [:address/id 3]}
              ...}
 :address/id {...}
 ...}

It is fed by initial data and by loading data from the backend and can be changed by mutations.

To really understand how and why Fulcro stores data and the need for component queries, watch the excellent Grokking Fulcro – Part 3a Stuffing Data Into a UI (without choking)
Component (a.k.a. UI Component)

A Fulcro component is a React component with optional meta data, such as query and ident. It encapsulates a part of the user interface and often contains other components. We call it stateful if it actually has a query, otherwise we say it is stateless or UI-only.

Data Entity

A part of your frontend data model with an identity of its own and a set of properties. Example: a "person" with :person/id and :person/age. Data entities (or their parts) are displayed by - and, through their query and ident, defined by - UI components and stored in the client DB.

EQL (EDN Query Language) server

The backend includes an EQL server - namely Pathom - that can process EQL queries and mutations and respond with data (nested, tree-shaped).

Ident

Of a data entity: the identifier of a data entity composed of the ID property name and value. Ex.: [:person/id 123]. Of a component: a function that returns an ident (discussed later).

Mutation (Fulcro)

When components want to "change" something - update data, upload a file - they submit a mutation describing the desired change to the transaction subsystem. Mutations can be local and/or remote. In the context of Fulcro, a request to load data from the backend is also a mutation (while to Pathom it is sent as a plain EQL query). Remote mutations are sent as EQL mutations.

Normalization of data

Data in the client DB is mostly stored in a normalized form in the database sense. I.e. entities do not include other entities but only their idents. The normalization depends on components declaring their idents.

Query

Each stateful component declares what data it needs using an EQL query (or, more exactly, a query fragment). If it has stateful children, it also includes their query in its own.

Resolver, Pathom

A Pathom resolver takes typically 0 or 1 inputs and optional parameters and outputs a tree of data. F.ex. an input-less resolver can declare: ::pc/output [{:all-blogs [:blog/id :blog/title :blog/content]}] and return {:all-blogs [{:blog/id 1, ..}, ..]}. Thus any query that asks for :all-blogs can be "parsed" and answered.

Root component

The top component of the UI, customary called Root.

Transaction subsystem

A part of Fulcro. Components submit mutations to the transaction subsystem for execution with transact!. You can think of it as an asynchronous queue.

Fulcro lifecycle

Let’s have a look at what is happening in a Fulcro application:

Fulcro lifecycle
Figure 2. Fulcro lifecycle

The core of the Fulcro lifecycle is simple:

  1. Something happens that requires a refresh of the UI, e.g. mounting the Root component, loading data from the backend, or receiving a data response from a mutation submitted to the backend

    1. When data arrives from the backend:

      1. Get the query from the relevant component (f.ex. MyBlogList)

      2. Use the query to normalize the data into the client DB

  2. Fulcro asks the Root component for its query (which includes the queries of its children and thus describes all the data the whole page needs)

  3. Fulcro uses the query and the client DB to construct the props data tree for the Root component

  4. The props are passed to the Root component (which passes the relevant parts on to its children) and it is rendered

Zooming in on components and mutations

You will learn:

  • That a Fulcro component defines a React component class

  • How a component query declares its data needs

  • How a component ident is used to normalize its data to avoid duplication (and simplify data updates)

  • How transact! is used to submit mutations from the UI

  • How load! submits a mutation that loads data from the backend, normalizes them, and stores them into the client database

  • How data is stored in the normalized (de-duplicated) client database

The anatomy of a Fulcro component: props, query, ident, body

Fulcro components, which are also React components, are the heart of a Fulcro application. Let’s explore them:

Example 1. A Fulcro component
;; Assume `defsc Address` and its factory `ui-address` exist:
(defsc Person
  [this {:person/keys [fname email address] :as props}] ; (1)
  {:query [:person/id :person/fname :person/email       ; (2)
           {:person/address (comp/get-query Address)}]
   :ident (fn [] [:person/id (:person/id props)])}      ; (3)
  (div                                                  ; (4)
    (p "Name: " fname ", email: " email)
    (ui-address address)))

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

(defsc Person …​) ("define stateful component") defines a new React class-based component. After the declaration of arguments ((1), this and props) comes a map with meta data of the component (here :query (2) and :ident (3), the two most common). Finally comes the body (4) (which will become the render method of the React component) that actually produces React DOM elements. You could read it like this:

(defsc <Name> [<arguments>]
  {<meta data>}
  <body to be rendered>)

Notice that defsc produces a JS class, which we turn into a React factory with comp/factory (customary we kebab-case its name and prefix it with ui-). The factory can then be used to create React elements (as is demonstrated with the ui-address factory). (JSX does this for you so that you can use classes directly. Here we want more control.)

Also notice that :query and props mirror each other. Fulcro will actually warn you if there is a mismatch between the two, thus preventing many errors.

Component’s props

The second argument to a defsc is the props (short for properties) - map of data passed in by the parent component, and normally derived based on the component’s query. They are the same as React props, with few noteworthy additions:

  • While React props must be a JavaScript map with string keys, Fulcro props - both for defsc components, dom/<tag> components, and vanilla JS components wrapped with interop/react-factory - can be and typically are a Clojure map (possibly containing nested Clojure data structures) with (typically qualified) keyword keys. (Fulcro actually stores its props under "fulcro$value" in the React JS map, but that is transparent to you.)

  • You can use lazy sequences of children (produced by map etc.).

On qualified keywords

We use qualified keywords a lot to avoid name conflicts and communicate ownership. If it is new to you, here is a brief summary of how to use them.

Destructuring:

;; Given:
(def props {:car/make "Škoda", :ui/selected? false})
;; 1. Destructure using (multiple) :<ns>/keys [..]:
(let [{:car/keys [make], :ui/keys [selected?]} props]
  (println make selected?))
;; 2. Destructure using :keys [<ns1>/key1, <ns2>/key2, ...]:
(let [{:keys [care/make ui/selected?]} props]
  (println make selected?))
;; 3. Destructure manually using <symbol> <qualified keyword> pairs:
(let [{care-make :care/make, selected? :ui/selected?]} props]
  (println care-make selected?))
;; all of the above print the same result: `Škoda false`

Aliases:

When we require a namespace, we can give it an alias as in (:require [my.long.ns :as myns]]) and we can use the alias in the qualified keywords - the trick is to use double instead of single colon in front of it, i.e. ::<ns alias>/kwd. And if we leave out the alias, as in ::kwd, it expands to the current ns, i.e. to :<current ns>/kwd.

(ns myns (:require [my.domain.car :as car]))
(def props {::car/make "Škoda", :my.domain.car/year 2020, ::sold? true})
(let [{::car/keys [make year], ::keys [sold?], sold2? :myns/sold?} props]
  (println make year sold? sold2?))
; OUT> Škoda 2020 true true

Namespaced maps:

Instead of writing {:person/id 1, :person/name "Jo"} we can also write #:person{:id 1, :name "Jo"}. The reader literal #:<ns><map> simply produces a map with all keys namespaced to the given ns.

You can read more in the Clojure Destructuring Guide.

Component’s :query

From the component example presented earlier:
(defsc Person
  [_ _]
  {:query [:person/id :person/fname :person/email
           {:person/address (comp/get-query Address)}]
   ..} ..)

The query declares what props the component needs, including the needs of its child components. (We saw how Person includes the query of Address via comp/get-query.)

Thus the root component’s query will describe the UI needs of the whole UI tree. The query is in EQL, which you are already familiar with, containing the properties the component itself needs and joins for the nested data needed by child components.

The figure below shows how the query fragments of all components that have a query are composed into the Root component’s query and "sent" to the client DB (1.), which responds with a tree of data (2.), which is then propagated down from Root to its children and so on. (Later we will learn how to send a query to the backend to load data into the client DB via df/load!.)

fulcro ui query data
Figure 3. Components, query, and data: UI → query → data → UI
Notice that the query sent and the data returned are only a subset of the UI tree, skipping over the stateless, UI-only components (the empty squares) that have no query of their own, such as the middle one in the leftmost branch. While "data entities" and "UI components" often correspond 1:1, it is not always the case. You can learn more in Fulcro Explained: When UI Components and Data Entities Diverge.

Beware: You must not copy and paste the child’s query into the parent’s but rather use (comp/get-query <Child>) as demonstrated. Both for DRY and because get-query also adds important metadata to the composed query about the origin of the individual fragments so that Fulcro can later use it to normalize data from load! or merge-component! correctly.

Fulcro combines the query and the (normalized) client database to produce the tree of data that is passed as props to the Root component. Which, in turn, will pass the relevant parts to its children, as we did with address.

Don’t be mislead, the query is not a standalone query that could be "run" directly against the database (as you know from SQL or re-frame subscriptions). It is rather a query fragment, which only makes sense in the context of its parent’s query. Only the root component’s properties are resolved directly against the client database. A query such as [:person/id :person/fname] is meaningless on its own - which person? Only in the context of a parent, such as [{:all-people [<insert here>]}] (in an imaginary AllPeopleList component) does it make sense.

Component’s :ident

From the component example presented earlier:
(defsc Person
  [_ props]
  {..
   ;; There are three ways to specify an ident, here all are equal:
   :ident (fn [] [:person/id (:person/id props)])} ..) ; lambda form
   ;; = the template form: [:person/id :person/id]
   ;; = the keyword  form: :person/id

For a data entity, ident(ifier) is akin to a self-contained foreign key in SQL: it contains the (unique) name of an entity’s ID property and its value, in a 2-element vector. For example: [:person/id 123].

For a component, its :ident is a function that returns the ident of the associated data entity, typically based on its props (captured from the component’s arguments): (fn [] [:person/id (:person/id props)]). We call this the lambda form but there are also shortcuts for common cases such as the keyword form and the template form.

For singleton components we use, by convention, the "property name" :component/id and a hardcoded value specific to the component - typically its name as a keyword. For instance :ident (fn [] [:component/id :AllPeopleList]).

Why do we need component idents? To tell Fulcro what is the ID property of an entity so that it can normalize its data into the client database.

Component’s :initial-state

A component can also specify :initial-state (fn [params] <some data matching the expected props>) to declare the props it wants to get passed on the first "frame", i.e. the first render. The data will be normalized based on idents and stored into the client DB. You can use it to define the state of the application before any data is loaded from the server-side.

When do you need to define initial state?

  • When you want to make sure that the component has particular props before any data is loaded from the backend. (Advanced) F.ex. router targets must not have nil props but are OK with {} and thus declare at least an empty initial state

  • When the component has no state of its own and only queries for global data using Link Queries

  • When a child component has an initial state, to compose it up (f.ex. dynamic routers do)

  • (Advanced) To pre-establish connections between data entities such as a form-like component and its form state

Rendering DOM: the body of a component

From the component example presented earlier:
(defsc Person
  [_ {:person/keys [fname email address]}]
  {..}
  (div
    (p "Name: " fname ", email: " email)
    (ui-address address)))

The body of the defsc macro becomes the render method of the React class.

Instead of JSX, we use functions from the dom namespace for rendering HTML tags and React factories for rendering React components.

This is what a complete call looks like:

(dom/h2 :.ui.message#about
  {:style {:background "1px solid black"}
   :classes ["my-heading" (when (:important? props) "important")]}
  "About")

and here is a minimal example:

(dom/p "Hello " (:fname props) "!")

The signature is:

(dom/<tag> ; or <ns>/<Fulcro component factory name> for components
  <[optional] keyword encoding classes and an element ID> ; (1)
  <[optional] map of the tag's attributes (or React props)> ; (2)
  <[optional] children>) ; (3)
1 A shorthand for declaring CSS classes and ID: add as many .<class name> as you want and optionally a single #<id>. Equivalent to {:classes [<class name> …​], :id <id>}.
2 A Clojure map of the element’s attributes/props. In addition to what React supports, you can specify :classes as a vector of class names, which can contain nil - those will be removed. It is merged with any classes specified in the keyword shorthand form.
3 Zero or more children
Additional notes
Returning multiple elements from the body

To return multiple child elements, wrap them either in a Clojure sequence or comp/fragment. React demands that every one must have a unique :key. Ex.: (defsc X [_ _] [(dom/p {:key "a"} "a") (dom/p {:key "b"} "b")]).

Assigning a unique :key to every instance of a Fulcro component

If a Fulcro component is being rendered in a sequence, f.ex. because you do something like (map ui-employee (:department/employees props)), it must have a unique :key prop. Leverage the second, optional argument to comp/factory to specify a function of the component’s props that will return the unique key:

(def ui-employee (comp/factory Employee {:keyfn :employee/id}))
;; assuming the Employee component has the (unique) :employee/id prop
Passing additional ("computed") props from the parent

What if the parent needs to pass on some additional props other than those that come from the query resolution, for example callbacks? You should not just stick them into the props map because it would be then missing if Fulcro does a targeted re-render of just the child component. Instead, you should pass it on as computed props either manually or leveraging comp/computed-factory and the optional third argument of defsc. This is demonstrated below:

Example 2. Passing computed props
(defsc Person [this props {::keys [callback]}]
 {..}
 (dom/div
   (dom/p "Person " (:person/name props))
   (dom/button {:onClick callback} "Delete")))

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

(defsc Parent [_ {:parent/keys [spouse]}]
  {..}
  (ui-person spouse {::callback #(js/alert "I won't give her up!")}))
Note on raw React components

We saw how to render a child Fulcro component, the Address, via its factory function, ui-address. But what about raw React classes from JS libraries?

It is similar, only instead of comp/factory we use interop/react-factory, which will take care of converting Cljs data to JS etc.

Changing global data and performing remote calls: mutations

When a component needs to change something outside of itself - such as uploading a file, changing data in the client DB, or changing data on the server-side - it does so through submitting mutations to the transaction subsystem via comp/transact!. A mutation is essentially a request to change something, represented as data and handled over to Fulcro for (asynchronous) processing.

Mutations can be local (client-side) only or local and remote (though there does not need to be any local behavior defined). Even though mutation usage looks like a function call, it is not. What transact! expects is a sequence of data:

(comp/transact! app-or-component
  [(<fully qualified symbol> <params map>), ...])

That is so that the mutation can be submitted over the wire to the backend as-is. Of course both Fulcro and Pathom expect that there actually is a defmutation corresponding to the provided "fully qualified symbol". So how do we define a mutation on the client and server side? (Assuming standard Fulcro and Pathom namespace aliases.)

Example 3. A Fulcro and Pathom mutation
#?(:cljs
    ;; client-side
    (m/defmutation delete-employee [{id :employee/id :as params}] ; (1)
      (action [{:keys [app state] :as env}]          ; (2)
        (swap! state update :employee/id dissoc id))
      (remote [env] true)                            ; (3)
      (ok-action [{:keys [app state result]}]        ; (4)
        (println "It worked!")))
  :clj
    ;; server-side
    (pc/defmutation delete-employee [env {id :employee/id :as params}]) ; (5)
      {::pc/params #{:employee/id}}
      (db/delete-employee id)
      nil))

;; Somewhere in a component:
(comp/transact! this [(delete-employee {:employee/id id})])   ; (6)
;; or:
(comp/transact! this `[(delete-employee {:employee/id ~id})]) ; (7)
1 The client-side mutation takes a map of parameters (see (6) for usage) and has zero or more named parts that look like protocol method implementations
2 action is the client-side, local part of the mutation and happens first. Here we can directly change the client DB (stored in the atom state)
3 if remote is present and returns something truthy, then the mutation is also sent to the backend as an EQL mutation. It could also modify the EQL before sending it or declare what data the server-side mutation returns. Omit for a client-side-only mutation. (Note: here the name remote must match against a remote registered with the Fulcro app; by default it is called "remote" but you could also register additional remotes and thus add here sections for those.)
4 ok-action is called after the remote mutation succeeded. Notice that Fulcro mutations and queries generally never "fail" and rather return data indicating that something went wrong so they would do trigger this action. You can use this action for example to submit other mutations.
5 The server-side mutation is a Pathom mutation (taking Pathom environment and the same params as the client-side). Typically it would update some kind of a data store.
6 As demonstrated, we submit a mutation for processing using comp/transact! and passing in the params. We can call the mutation as a function, which will simply return the call as data (example: (my-mutation {x: 1})'(my.ns/my-mutation {x: 1}))
7 …​or we provide the symbol directly
There are ways to avoid a circular dependency between a component and a mutation, such as quoting and using the component registry to look up a component class based on its name.
The mutations namespace has a bunch of helper functions for transacting built-in mutations to set the props of the current component, such as set-value!, set-integer!, toggle! (for booleans), etc.
com.fulcrologic.fulcro.algorithms.normalized-state has useful helpers for changing the client DB, such as remove-entity (which also removes all references to it), integrate-ident (for adding references), remove-ident (for removing from a list), swap!→, etc.

transact!-ing multiple mutations

If you transact! multiple mutations then their action will be processed in order. However, if a mutation has a remote part, Fulcro just sends it without waiting for it to finish before going on to process the next mutation. If you want to only issue a follow-up mutation after the remote part of the initial mutation has finished, do so from its ok-action.

load!-ing data

Pre-study: Merging data into the client DB with merge-component!

Load! does primarily two things: it retrieves a tree of data from the server and then normalizes it and merges it into the client DB. (Remember that the client DB is the only place where Fulcro ever looks, it does not get handed the retrieved data directly.) To understand this second part better, we will have a look at merge-component!. It is not called by load! but it is very similar to what it does internally (and they both delegate a lot to merge*). It is also a useful tool on its own, for example when you want to get hardcoded data into Fulcro during development.

Given these two components:

(defsc Address [_ _]
  {:query [:address/id :address/street]
   :ident :address/id})
   ;; reminder: `:address/id` is a shorthand for
   ;; (fn [:address/id (:address/id props)])

(defsc Person [_ _]
  {:query [:person/id :person/fname
           {:person/address (comp/get-query Address)}]
   :ident :person/id})

and this data:

(def person-tree
  {:person/id 1
   :person/fname "Jo"
   :person/address {:address/id 11
                    :address/street "Elm Street 7"}})

we can merge the data into the client DB like this:

(merge/merge-component!
  app
  Person
  person-tree)

to get the following client DB:

{:person/id  {1  {:person/id 1   :person/fname "Jo" :person/address [:address/id 11]}}
 :address/id {11 {:address/id 11 :address/street "Elm Street 7"}}}

Notice that idents of both Person and Address were used to put the data into the correct "tables". If Address lacked an ident, its data would stay denormalized inside the person just as it is in the input data. (The top component passed to merge-component! always must have an ident.)

After having modified the client DB, merge-component! will also schedule re-rendering of the UI.

The signature of merge-component! is:

(merge/merge-component!
  app-or-component
  <Component>
  <data tree>
  <[optional] key-value pairs of options>)

merge-component! extracts the ident and query of the given component (and leverages the metadata of the child query fragments to get the other relevant idents, such as Address') and uses those to normalize the data into the DB. Notice that the data is really merged into the DB in a smart way and not just blindly overwriting it, i.e. pre-existing data is preserved (see its docstring for details).

Targeting - Adding references to the new data to existing entities

Now, what if we don’t only want to add the data itself but also want to add reference(s) to the newly added data to some other, existing data entities in the client DB? :append, :prepend, and :replace to the rescue! We can specify as many of these as we want, providing full paths to the target property in the client DB. The paths have three (four, in the case of :replace of a to-many element) parts - entity name, entity ID value, the target property.

Example 4. Data targeting: append, prepend, replace
;; Given an app with this client DB:
(def app
  (app/fulcro-app
    {:initial-db
     {:list/id   {:friends    {:list/people [[:person/id :me]]}
                  :partygoers {:list/people [[:person/id :me]]}}
      :person/id {:me         {:person/id :me :person/fname "Me"
                              :person/bff [[:person/id :me]]}}}}))

;; and this call (reusing the person-tree defined earlier):
(merge/merge-component!
  app
  Person ; = Jo, id 1
  person-tree
  :append  [:list/id :friends :list/people]
  :prepend [:list/id :partygoers :list/people]
  :replace [:person/id :me :person/bff 0]
  :replace [:best-person])

;; we get this Client DB:
{:list/id
 {:friends    {:list/people [[:person/id :me] [:person/id 1]]} ; (1)
  :partygoers {:list/people [[:person/id 1] [:person/id :me]]}}; (2)
 :person/id
 {:me #:person{:id :me, :fname "Me", :bff [[:person/id 1]]}, ; (3)
  1   #:person{:id 1,   :fname "Jo", :address [:address/id 11]}},
 :address/id {11 #:address{:id 11, :street "Elm Street 7"}},
 :best-person [:person/id 1]}                                ; (4)
1 :append inserts the ident of the data at the last place of the target to-many property (vector of idents) (unless the vector already includes it anywhere)
2 :prepend inserts the ident of the data at the first place of the target to-many property (vector of idents) (unless the vector already includes it anywhere)
3 :replace can replace an element of a to-many vector given a path ending with an index and provided it already exists
4 and :replace can also insert the ident at the given path (which even does not need to be an entity-id-property triplet)

We have seen that in addition to merging data into the client DB we can also append and prepend references to it to to-many reference properties on other entities (such as :list/people), insert them into to-one properties with :replace etc. And we can do as many such operations as we want at once.

Outside of the context of merge-component!, when you are changing data directly in a mutation and want to append/prepend/replace a reference to it, you can use targeting/integrate-ident*. It takes the same keyword-value argument pairs as merge-component!.

Loading remote data

Now that you understand the merging of data into the client DB, you can load data with df/load!. Instead of the data tree it takes a property or an ident that Pathom can resolve (i.e. there needs to be a Pathom resolver available to provide the data you are asking for). Using that and the component’s query, it obtains a data tree from Pathom and then normalizes and merges it.

The signature of load! is:

(df/load! app-or-comp          ; (1)
          src-keyword-or-ident ; (2)
          component-class      ; (3)
          options)             ; (4)
1 Pass in a reference to the component’s this (the first argument of defsc) if you have it, otherwise pass in the global app singleton
2 Specify the server-side property (attribute) that Pathom can resolve - either a keyword, i.e. a property name output by a global Pathom resolver, or an ident such as [:person/id 1], supported by a Pathom resolver taking the corresponding input (e.g. ::pc/input #{:person/id})
3 The component whose query defines which of the available properties to get and that is used when merging the returned data with merge-component!
4 load! takes plenty of options, a number of them very useful. We will explore those in more detail later

(Notice that load! will actually transact! a predefined mutation. It just provides a convenient wrapper around the mutation and common additional actions.)

A couple of examples:

Example 5. load! variants
;; Assuming a global Pathom resolver `:all-people`
;; (with `::pc/output [:all-people [..]]` and no ::pc/input)
(df/load! app :all-people Person) ; (1)
;; => client db gets:
;; :all-people [[:person/id 1], [:person/id 2], ...]
;; :person/id {1 {:person/id 1, :person/propX ".."}, 2 {...}}

;; Loading by ident - assuming a Pathom resolver
;; with `::pc/input #{:person/id}`:
(df/load! this [:person/id 123] Person) ; (2)
;; => client db gets:
;; :person/id {..., 123 {:person/id 123, :person/propX ".."}}

;; As above, but also adding the loaded entity to
;; a list in a related entity
(df/load! app [:employee/id 123] Employee ; (3)
  {:target (targeting/append-to [:department/id :sales :department/employees])})
;; => client db gets:
;; :employee/id {..., 123 {:employee/id 123, ...}}
;; :department/id {:sales {:department/id :sales,
;;                         :department/employees [..., [:employee/id 123]]}}
1 Load an entity or list of entities from a global (input-less) resolver
2 Load an entity by ident
3 Load an entity by ident and add a reference to another entity, leveraging the :target option and the helpers in the targeting namespace

How to…​

Here we will learn how to solve a number of common needs by leveraging the rich set of options that load! supports. See its docstring for the full list and documentation.

  1. How to provide params to parametrized Pathom resolvers?

    Use the option :params to provide extra parameters to the target Pathom resolver, such as pagination and filtering. Ex.: (df/load this :current-user User {:params {:username u :password p}}).

  2. How can I add a reference to the loaded data entity to another entity present in the client DB?

    Similarly as with merge-component! but instead of specifying directly :append, :prepend, and :replace, you specify the :target option with a target from the targeting namespace such as (append-to <path>), (prepend-to <path>), (replace-at <path>) or any combination of these by leveraging (multiple-targets …​). See the example above.

  3. How to exclude a costly prop(s) from being loaded?

    Imagine you want to load a Blog entity but exclude its comments so that you can load them asynchronously or e.g. when the user scrolls down. You can leverage :without for that: (load! app [:blog/id 42] Blog {:without #{:blog/comments}}). Notice that it removes the property no matter how deep in the query it is so (load! app :all-blogs BlogList {:without #{:blog/comments}}) will also do this. Learn more in the chapter on Incremental Loading.

  4. How to load only a subtree of data (f.ex. the one excluded earlier with :without)?

    The opposite of the :without option is the function df/load-field!, which loads 1+ props of a component. Inside the Blog component: (df/load-field! this [:blog/comments] {}). Learn more in the chapter on Incremental Loading. Alternatively, you can use the load! option :focus, which requires more work but is more flexible.

  5. How to track the loading status, i.e. loading x loaded x failed?

    Use the option :marker <your custom keyword or data> to add a "marker" that will track the status for you. See the example below.

  6. How to execute a follow-up action after the load is finished?

    What if you need to do an additional activity after the data arrives? You can use the options :post-mutation, optionally with :post-mutation-params, to submit a mutation. Or you can use the more flexible option :post-action (fn [env] ..), which can call transact!.

When to load!?

When to call load!? Fulcro does not load any data from the server for you, you have to load! them yourself. The main options for when to do that are:

  1. When your application is starting

  2. In an event handler (e.g. onClick)

  3. When a component is mounted, using React’s :componentDidMount - though this is suboptimal and can result in loading cascades (A mounts and loads its data; after it gets them, its child B is mounted and loads its data, …​); a better option is leveraging Fulcro’s deferred routing

  4. When a component is scheduled to be displayed, i.e. when using Fulcro’s Dynamic Routers with Deferred Routing. However this is an advanced and non-trivial topic so we will not delve into it here.

What to load!?

Even though the Root’s query represents the data needs of the whole UI, you essentially never use it to load! the data from the backend. It does not make sense, as we will see briefly. What you do instead is that you load distinct sub-trees of the data that actually correspond to top-level "entry points" (global Pathom resolvers) in your data model. Remember that you invoke load! with:

(df/load! app :some-server-property SomeComponent)

and it will construct and send the following EQL over the wire:

EQL query sent to the backend
[{:some-server-property <the query of SomeComponent>}]

and finally store the returned data in the client DB like this:

Client DB
{...
 :some-server-property <the (normalized) data>}

Loading Root’s query in this way does not make sense because it would put the data under a key in the client DB (such as :some-server-property above) while the Root needs its props to be directly at the root of the client DB. You could bypass load! and send the Root’s query directly, as-is to avoid this problem. But normally you simply issue 1+ loads for the data of interest, with proper targeting, upon some event (such as app start).

Let’s have a look at a simple banking application that shows two lists - an overview of all accounts and an overview of all customers:

Example 6. A banking application
(defsc Account [_ props]
 {:ident :account/id
  :query [:account/id :account/owner :account/balance]}
 (p (str props)))
(def ui-account (comp/factory Account))

(defsc AccountList [_ props]
 ;; Note: In practice, this would be UI-only comp. with no query
 ;; and we would put the list of accounts directly under Root
 {:ident (fn [] [:component/id ::AccountList])
  :query [{:account-list/accounts (comp/get-query Account)}]}
 (div
   (h2 "Accounts")
   (map ui-account accounts)))
(def ui-account-list (comp/factory AccountList))

;; LEFT OUT Customer, CustomerList, their ui-* ;;

(defsc Root [_ {:root/keys [accounts customers]}]
 {:query [{:root/accounts (comp/get-query AccountList)}
          {:root/customers (comp/get-query CustomerList)}]}
 (div
   (h1 "Your bank")
   (ui-account-list accounts)
   (ui-customer-list customers)))

(comment
  ;; Somewhere during app startup, we would do:
  (do
    (df/load! app :all-accounts Account ; (1)
      {:target (targeting/replace-at [:component/id :AccountList :account-list/accounts])}) ; (2)
    (df/load! app :all-customers Customer
      {:target (targeting/replace-at [:component/id :CustomerList :customer-list/customers])}))

  ;; which assumes that on the server-side we have something like:
  (pc/defresolver xyz [env _]
    {::pc/input  #{}
     ::pc/output [{:all-accounts [:account/id :account/owner :account/balance]}]}
    (jdbc/execute! env "select id, owner, balance from account"))
  ;; and similarly for :all-customers
  )
1 We load! using Account’s query because this one defines what it is we want for each of the accounts
2 We instruct load! to place the data where our UI expects them, i.e. inside the AccountList component instead of at the root of the client DB

Bonus: Tracking loading state with load markers

You can ask load! to track the status of loading using a "load marker" and you can query for the marker to use it in your component. See the chapter Tracking Specific Loads in the book for details. A simple example:

Example 7. Tracking the status of a load! with a load marker
;; Somewhere during the app lifecycle, assuming a
;; parametrized global resolver for `:friends`:
(df/load! this :friends Person
          {:params {:friends-of [:person/id 1]}
           :target (targeting/replace-at [:list/id :friends :list/people])
           :marker :friends-list}) ; (1)

;; The component:
(defsc FriendsList [_ props]
  {:query [:list/people [df/marker-table :friends-list]]    ; (2)
   :ident (fn [] [:list/id :friends])}
  (let [marker (get props [df/marker-table :friends-list])] ; (3)
    (cond
      (df/loading? marker) (dom/div "Loading...")           ; (4)
      (df/failed?  marker) (dom/div "Failed to load :-(")
      :else (dom/div
              (dom/h3 "Friends")
              (map ui-person (:list/people props))))))
1 Ask load! to track the load with a marker called e.g. :friends-list
2 Add [df/marker-table <your custom id>] to your query to access the marker (notice that this is an ident and will load the marker with the given ID from the Fulcro-managed marker table in the client DB)
3 Get the marker from the props. Notice this is get and not get-in because the whole ident is used as the key.
4 Use the provided functions to check the status of the load and display corresponding UI

Briefly about pre-merge

What if your component needs not only the data provided by the server but also some UI-only data to function properly? When you load! a new entity - for example [:person/id 1] - only the data returned from the backend will be stored into the client DB. If you need to enhance those data with some additional UI-only data before it is merged there - for example router or form state - you can do so in its :pre-merge. This is an advanced topic so we will not explore it here but you need to know that this is possible so that you know where to look when the time comes.

Review

You have now learned about the key building block of a Fulcro application, the component, with its query and ident. About effecting changes through local and remote mutations, about loading data, and about normalizing data into the client DB. This following figure demonstrates where each of the pieces fit in the application (the namespace names are just simple examples; in practice they would be more domain-oriented; also, there should perhaps be an ↔ arrow between mutations and the Back End):

fulcro interfaces
Figure 4. Where do key Fulcro constructs fit into the system? (Courtesy of Timofey Sitnikov)

FAQ

  1. Can different components have the same ident?

    Yes. Typically these components are different (sub)views of the same data entity. So you could have a "person" data entity and the components PersonOverview with the query [:person/id :person/fname :person/image-small] and PersonDetails with the query [:person/id :person/fname :person/age :person/image-large], both with :ident :person/id. The combined data of both would be stored at the same place in the client DB. You can learn more in Fulcro Explained: When UI Components and Data Entities Diverge.

  2. When, where does Fulcro load data from the backend?

    It does not. You have to load! the data yourself, upon a user action or at a suitable point in the lifecycle of the application, as discussed above. (Fulcro RAD is an exception, its reports and forms do load its data but they just do what you would do manually, trigger load from their :will-enter. Though that is beyond the scope of this tutorial.)

  3. What is in the client DB? Is it only Fulcro-managed, load!-ed data of components?

    No! The client DB is just a map and you can put there (typically through a mutation) whatever data you want, in whatever form you see fit. (I.e. it does not need to be in the entity type → entity id → entity props form.) While Fulcro will use it to store any data load!-ed from the backend - normalized, if you defined idents - you can also put there any additional data that you need. You could put there e.g. :auth-token "XYZ…​", :selected-customer [:customer/id "Volvo"], or :my-custom-tree {:I {:have [:kid1 :kid2 'etc]}}.

Next steps

OK, you have completed the tutorial. What now?

  1. Install Fulcro Inspect and enable custom formatters in Chrome to display Clojure data nicely in the Console - trust me, these two are indispensable!

  2. Do the minimalist Fulcro tutorial exercises to check and challenge your theoretical knowledge in practice.

  3. Come back and re-read this tutorial. You will likely understand it much better and get things you overlooked upon the first reading.

  4. Have a look at the Guide to learning Fulcro.

  5. Clone fulcro-template, study its code, delete parts and try to recreate them from scratch, extend it. Refer to the Fulcro Troubleshooting Decision Tree when things do not work out and to Fulcro Explained: When UI Components and Data Entities Diverge when you need to figure out how to map your frontend data to your components tree.

  6. Go back to Fulcro Developers Guide and read the introductory chapters to gain a deeper understanding