Skip to content

fulcro-legacy/fulcro-getting-started

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Fulcro Getting Started Guide

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

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

This document assumes you’re working with Fulcro 2.5 and above. The differences are minor, but the DOM factories changed to be more succinct.
ℹ️
The solution files in this repository run, but have a slightly different naming/organization than the guide itself describes. Hopefully, I’ll have time to update them more later. TK

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

$ lein new fulcro app nodemo

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

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

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

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

(start-figwheel)

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

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

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

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

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

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

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

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

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

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

(enable-console-print!)

(log/set-level! :all)

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

(mount)

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

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

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

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

Save this in resources/public/index.html.

You can now run this project in various ways.

From the command line:

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

Within IntelliJ:

  • Run → Edit Configurations…​

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

    • Give it a name (like dev)

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

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

    • In Parameters add script/figwheel.clj

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

For Emacs + Cider:

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

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

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

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

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

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

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

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

Instead, edit your code and re-save.

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

cljs.user=> (reset-autobuild)

will typically get you back on track.

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

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

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

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

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

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

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

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

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

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

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

All versions of Fulcro:

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

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

Fulcro 2.5+:

(dom/div :.a#thing "Hi") ; keyword can contain any number of classes preceeded by dots, and an id with #
(dom/div :.a#thing {:data-prop 3} "Hi") ; props can still be supplied with the keyword
(dom/div {:className "a" :data-prop 3} "Hi") ; or it can all be done in props

The 2.5 versions are macros that obtain the same runtime speed as the older versions in the most cases.

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

... same as before

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

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

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

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

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

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

Now we can compose people into our root:

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

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.

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

Replace your content with this:

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

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

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

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

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

So that the UI graph looks like this:

      +--------+
      |  Root  |
      ++-----+-+
       |     |
 +-----+--+ ++-------+
 |  List  | |  List  |
 +---+----+ +----+---+
     |           |
 +---+----+ +----+---+
 | Person | | Person |
 |--------| |--------|
 | Person | | Person |
 +--------+ +--------+

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

{ :friends           { :person-list/people [PERSON ...]
;  ==to-one list=>      ==to-many people==>
  :enemies           { :person-list/people [PERSON ...] }
      +--------+
      |  Root  |
      ++-----+-+
enemies|     |friends
 +-----+--+ ++-------+
 |  List  | |  List  |
 +---+----+ +----+---+
     |people     |people
 +---+----+ +----+---+
 | Person | | Person | 0
 |--------| |--------|
 | Person | | Person | 1
 +--------+ +--------+

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?

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

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

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

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

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

It looks like this:

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

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

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

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

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

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

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

There are several benefits of this so far:

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

  2. That initial state becomes your initial application database.

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

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

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

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

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

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

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

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

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

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

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

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

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

So, a data tree like this:

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

would have a query that looks like this:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

again, nothing else changes.

Finally, we compose to Root:

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

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

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

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

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

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

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

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

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

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

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

  2. Adding the callback into the props (WRONG)

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

  4. Invoking the callback when delete is pressed.

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

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

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

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

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

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

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

Now the real fun begins: Making things dynamic.

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

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

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

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

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

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

  • Abstract the operation (like a function)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  2. Change the callback to run the transaction

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

...

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

  2. Running the transaction in the callback.

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

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

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.

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:

                                 +-------------------------------------+
                                 |                                     |
PersonList                       |     Person                          |
+---------------------------+    |     +----------------------------+  |
| ID  | Label               |    |     |ID | Name         | List ID |  |
|---------------------------|    |     |----------------------------|  |
| 1   | Friends             |<---+     |1  | Joe          |    1    |--+
+---------------------------+          |----------------------------|  |
                                       |2  | Sally        |    1    |--+
                                       +----------------------------+

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

PersonList                             Person
+---------------------------+          +------------------+
| ID  | Label   | People    |          |ID | Name         |
|---------------------------|          |------------------|
| 1   | Friends | #{1,2}    |----+---->|1  | Joe          |
+---------------------------+    |     |------------------|
                                 +---->|2  | Sally        |
                                       +------------------+

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

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

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

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

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

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

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

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

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

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

The program will now look like this:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

For example, try this at the REPL:

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

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

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

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

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

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

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

  • Transactions invoke abstract mutations

    • Mutations modify the (normalized) db

    • The transaction’s subtree of components re-renders

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.

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

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

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

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

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

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

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

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

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

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

OK, back to the main story!

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

Your template already has one :)

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

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

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

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

The server code itself is very light:

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

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

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

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

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

defaults.edn:

{:port 3000}

dev.edn:

{}

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

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

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

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

user=> (go)

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

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

user=> (restart)

will do this.

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

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

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

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

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

So your network topology was:

+----------+
| Browser  |                  +-------------------+
|  app     +-----+            |                   |
|          |     |            |  port 3449        |
+----------+     | http load  |  +-------------+  |
                 +----------->|  | Figwheel    |  |
                 |            |  |             |  |
                 +----------->|  |             |  |
                ws hot code   |  +-------------+  |
                              +-------------------+

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

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

                           localhost
                           +-------------------+
                           |                   |
                           |  port 3000        |
              app requests |  +-------------+  |
+----------+     +-------->|  |Your Server  |  |
| Browser  |     |         |  +-------------+  |
|  app     +-----+         |                   |
|          |     |         |  port 3449        |
+----------+     |         |  +-------------+  |
                 +-------->|  | Figwheel    |  |
             ws hot code   |  +-------------+  |
                           |                   |
                           +-------------------+

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

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

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

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

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

Thus, there really are not very many scenarios!

The three basic scenarios are:

  • Load something into the root of the application state

  • Load something into a particular field of an existing thing

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

user=> (restart)

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

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

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

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

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

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

  2. Pull of from the props

  3. Show something about it in the UI

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

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

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

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

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

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

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

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

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

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

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

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

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

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

user=> (restart)

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

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

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

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

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

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

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

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

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

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

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

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

(df/load this [:person/by-id 3] Person)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

This has the identical syntax to the client version!

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

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

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

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

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

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

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

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

The syntax for the addition is:

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

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

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

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

Fulcro is really meant to be a full-stack solution. That said, it isn’t really that hard to make it talk to other kinds of servers. As an example, this addendum talks you through what it takes to talk to a legacy REST service.

Working with legacy REST APIs is a simple, though tedious, task. Basically you need to add an additional remote to the Fulcro Client that knows how to talk via JSON instead of EDN.

The basic steps are:

  1. Implement FulcroNetwork. See the fulcro.client.network namespace for the protocol and built-in implementation.

    1. Your send method will be passed the query/mutations the client wants to do. You must translate them to a REST call and translate the REST response into the desired tree of client data, which you then pass to the ok callback that send is given.

  2. Install your network handler on the client (using the :networking option)

  3. Add the :remote option to your loads, or use your remote name as the remote side of a mutation

For this example we’re going to use the following public REST API endpoint: http://jsonplaceholder.typicode.com/posts which returns a list of posts (try it to make sure it is working).

It should return an array of JSON maps, with strings as keys.

Basically, when you run a transaction (read or write) the raw transaction that is intended to go remote is passed into the send method of a networking protocol. The networking can send that unchanged, or it can choose to modify it in some way. Since REST servers don’t understand our Fulcro requests, we have to add a layer at the network to convert one to the other, and back (for the response).

First, let’s talk about the UI code for dealing with these posts, since the UI defines the queries. Here is a very simple UI we can add to our program:

(defsc Post [this {:keys [post/title post/body]}]           ; (1)
  {:ident [:posts/by-id :db/id]
   :query [:db/id :post/user-id :post/body :post/title]}
  (dom/div
    (dom/h4 title)
    (dom/p body)))

(def ui-post (prim/factory Post {:keyfn :db/id}))

(defsc Posts [this {:keys [posts]}]                         ; (2)
  {:initial-state {:posts []}
   :ident         (fn [] [:post-list/by-id :the-one])
   :query         [{:posts (prim/get-query Post)}]}
  (dom/ul
    (map ui-post posts)))

(def ui-posts (prim/factory Posts))

; ...

(defsc Root [this {:keys [ui/react-key blog-posts current-user friends enemies]}] ; (5)
  {:query         [:ui/react-key
                   :ui/person-id
                   {:current-user (prim/get-query Person)}
                   {:blog-posts (prim/get-query Posts)}     ; (3)
                   {:friends (prim/get-query PersonList)}
                   {:enemies (prim/get-query PersonList)}]
   :initial-state (fn [params] {:blog-posts (prim/get-initial-state Posts {}) ; (4)
                                :friends    (prim/get-initial-state PersonList {:id :friends :label "Friends"})
                                :enemies    (prim/get-initial-state PersonList {:id :enemies :label "Enemies"})})}
  (dom/div
    (dom/h4 (str "Current User: " (:person/name current-user)))
    (dom/button {:onClick (fn [] (df/load this [:person/by-id 3] Person))} "Refresh User with ID 3")
    (ui-person-list friends)
    (ui-person-list enemies)
    (dom/h4 "Blog Posts")                               ; (6)
    (ui-posts blog-posts)))
  1. A component to represent the post itself

  2. A component to represent the list of the posts

  3. Composing the Posts UI into root query

  4. Composing the Posts UI into root initial data

  5. Pull the resulting app db data from props

  6. Render the list

Of course, there are no posts yet, so all you’ll see is the heading. Notice that there is nothing new here. The UI is completely network agnostic, as it should be.

Now for the networking code. This bit is a little longer, but most of it is the details around network communcation itself, rather than the work you have to do. Create a new namespace src/main/app/rest.cljs:

(ns app.rest
  (:refer-clojure :exclude [send])
  (:require [fulcro.logging :as log]
            [fulcro.client.network :as net]
            [cognitect.transit :as ct]
            [goog.events :as events]
            [fulcro.transit :as t]
            [clojure.string :as str]
            [clojure.set :as set]
            [fulcro.client.primitives :as prim])
  (:import [goog.net XhrIo EventType]))

(defn make-xhrio [] (XhrIo.))

(defrecord Network [url request-transform global-error-callback complete-app transit-handlers]
  net/NetworkBehavior
  (serialize-requests? [this] true)
  net/IXhrIOCallbacks
  (response-ok [this xhr-io valid-data-callback]
    ;; Implies:  everything went well and we have a good response
    ;; (i.e., got a 200).
    (try
      (let [read-handlers (:read transit-handlers)
            ; STEP 3: Convert the JSON response into a proper tree structure to match the query
            response      (.getResponseJson xhr-io)
            edn           (js->clj response) ; convert it to clojure
            ; Rename the keys from strings to the desired UI keywords
            posts         (mapv #(set/rename-keys % {"id"     :db/id
                                                     "title"  :post/title
                                                     "userId" :post/user-id
                                                     "body"   :post/body})
                            edn)
            ; IMPORTANT: structure of the final data we send to the callback must match the nesting structure of the query
            ; [{:posts [...]}] or it won't merge correctly:
            fixed-response      {:posts posts}]
        (js/console.log :converted-response fixed-response)
        ; STEP 4; Send the fixed up response back to the client DB
        (when (and response valid-data-callback) (valid-data-callback fixed-response)))
      (finally (.dispose xhr-io))))
  (response-error [this xhr-io error-callback]
    ;; Implies:  request was sent.
    ;; *Always* called if completed (even in the face of network errors).
    ;; Used to detect errors.
    (try
      (let [status                 (.getStatus xhr-io)
            log-and-dispatch-error (fn [str error]
                                     ;; note that impl.application/initialize will partially apply the
                                     ;; app-state as the first arg to global-error-callback
                                     (log/error str)
                                     (error-callback error)
                                     (when @global-error-callback
                                       (@global-error-callback status error)))]
        (if (zero? status)
          (log-and-dispatch-error
            (str "NETWORK ERROR: No connection established.")
            {:type :network})
          (log-and-dispatch-error (str "SERVER ERROR CODE: " status) {})))
      (finally (.dispose xhr-io))))
  net/FulcroNetwork
  (send [this edn ok error]
    (let [xhrio       (make-xhrio)
          ; STEP 1: Convert the request(s) from query notation to REST...
          ; some logic to morph the incoming request into REST (assume you'd factor this out to handle numerous kinds)
          request-ast (-> (prim/query->ast edn) :children first)
          uri         (str "/" (name (:key request-ast)))   ; in this case, posts
          url         (str "http://jsonplaceholder.typicode.com" uri)]
      (js/console.log :REQUEST request-ast :URI uri)
      ; STEP 2: Send the request
      (.send xhrio url "GET")
      ; STEP 3 (see response-ok above)
      (events/listen xhrio (.-SUCCESS EventType) #(net/response-ok this xhrio ok))
      (events/listen xhrio (.-ERROR EventType) #(net/response-error this xhrio error))))
  (start [this] this))

(defn make-rest-network [] (map->Network {}))

The steps you need to customize are annotated in the comments of the code. There are just a few basic steps:

  1. Fulcro comes with a handy function that can convert a query into an AST, which is easier to process. We don’t really care too much about the whole query, we just want to detect what is being asked for (we’re going to ask for :posts).

  2. Once we’ve understood what is wanted, we create a REST URL and GET the data from the REST server.

  3. When we get a successful response we need to convert the JSON into the proper EDN that the client expects. In this case we’re looking for { :posts [ {:db/id 1 :post/body "…​" :post/title "…​" ] …​ }.

  4. Once we have the properly structure tree of data to match the query, we simply pass it to the ok callback that our send was given.

In a more complete program, you’d put hooks at steps (2) and (3) to handle all of the different REST requests, so that the majority of this code would be a one-time thing.

Fulcro lets you set up networking yourself. We’d still like to talk to our server, but now we also want to be able to talk to the REST server. The modification is done in our client options. For example, our devcard playground could be changed to this:

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

(defcard-fulcro sample-app
  root/Root
  {}
  {:inspect-data true
   :fulcro       {
                  :networking {:remote (net/make-fulcro-network "/api" :global-error-callback (constantly nil))
                               :rest   (rest/make-rest-network)}
                  :started-callback
                              (fn [app] (df/load app :current-user root/Person)
                                (df/load app :my-friends root/Person {:target        [:person-list/by-id :friends :person-list/people]
                                                                      :post-mutation `api/sort-friends})
                                (df/load app :my-enemies root/Person {:target [:person-list/by-id :enemies :person-list/people]}))}})

IMPORTANT NOTE: If you’re using the dev cards, you might want to change :inspect-data true to false. Devcards get a bit slow if you put a lot of data in the app and ask the card to format it all in the inspector. In those cases it can be better to use Fulcro Inspect instead).

All the hard stuff is done. Loading is now triggered just like you would have before, except with a :remote option to specify which network to talk over:

                       :started-callback (fn [app]

                                           (df/load app :posts root/Post {:remote :rest :target [:post-list/by-id :the-one :posts]})

                                           ... as before ...

The same technique is used. Everything you’ve read is accurate for mutations as well (you’ll see the mutation come into the send function). To trigger a mutation, just add another section to your client mutation (a mutation can be sent to any number of remotes, in fact):

(defmutation delete-post
  [{:keys [id]}]
  (action [env] ...stuff to affect local db...)
  ; you could also include this: (remote [env] true)
  (rest [env] true)) ; tell the :rest networking to send this mutation

So, action names the local (optimistic) effect. Each other method name must match a remote’s name as configured in the :networking of the client. If you return true (or an AST) from one of these "remote" sections, it will trigger the mutation to be sent to that network handler.

For your convenience this complete application is at https://github.com/fulcrologic/fulcro-getting-started