Please help me to improve this tutorial. If you find anything that can/should be removed, or something unclear, or have any other comment or suggestion, please do not hesitate to ping @holyjak
in #fulcro at the Clojurians Slack (or Zulip).
This is a minimalistic introduction to Fulcro that focuses on HOW and not WHY. For the latter, read the awesome Fulcro Developers Guide, especially the introductory chapters 2-4. The goal is to enable you to read, understand, and tinker with the code of a Fulcro application.
This tutorial expects that you have gone through an EQL Tutorial and are familiar with EQL, queries, joins. It will not work without that knowledge. You also need to have an idea about Pathom v2 and Pathom resolvers (global, ident, parameters). (But you don’t need to care about Pathom readers.)
It also assumes that you are already familiar with React and thus with concepts such as components, factories, elements, a tree of components, passing "props" from the root component, UI as a function of data.
It is helpful to know a little about the principles of GraphQL (see this 3 Minute Introduction to GraphQL and perhaps also this GraphQL is the better REST for more insight into the vlue proposition of GraphQL and EQL).
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). 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.
Fulcro is a full-stack web framework. These are the main components:
-
Frontend
-
UI - Fulcro/React components render a DOM and submit mutations (= action name + parameters) to the transaction (Tx) subsystem
-
Tx (transaction subsystem) - asynchronously executes local mutations and sends remote mutations and queries to the remote backend
-
Client DB - data from the backend is normalized into the client-side DB (data cache); Tx typically schedules a re-render afterwards
-
-
Backend
-
Pathom receives EQL queries and mutations and responds with a data tree
-
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 this terms means.
- App
-
A reference to the current Fulcro application, containing configuration, the client DB, etc. Produced by
app/fulcro-app
and used when callingtransact!
orload!
when a component’sthis
is not available. Referred to asapp
in code samples. - Client DB
-
The client-side cache of data. It is a map of maps: entity name (e.g.
:person/id
) → entity id value (e.g.123
) → properties of the entity (e.g.{:person/id 123, :person/fname "Jo", :person/address [:address/id 3]}
). (For convenience, we use the name of the id property as the "name" of the entity - thus:person/id
,:user/username
.) It is fed by initial data and loading data from the backend and can be changed by mutations. - Component
-
A Fulcro component is a React component with extra meta data, such as query and ident.
- EQL (EDN Query Language) server
-
The backend includes an EQL server - 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, 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 rather 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
-
Components submit mutations to the transaction subsystem for execution with
transact!
. You can think of it as an asnychronous queue.
Let’s have a look at what is happening in a Fulcro application:
The core of the Fulcro lifecycle is simple:
-
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
-
When data arrives from the backend:
-
Get the query from the relevant component (f.ex.
MyBlogList
) -
Use the query to normalize the data into the client DB
-
-
-
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)
-
Fulcro uses the query and the client DB to construct the props data tree for the Root component
-
The props are passed to the Root component and it is rendered
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
Fulcro components, which are also React components, are the heart of a Fulcro application. Let’s explore them:
;; Assume `defsc Address` and its factory `ui-address` exist:
(defsc Person
[this {:person/keys [fname email address] :as props}]
{:query [:person/id :person/fname :person/email
{:person/address (comp/get-query Address)}]
:ident (fn [] [:person/id (:person/id props)])}
(div
(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 (this
and props
) comes a map with meta data of the component (here :query
and :ident
, the two most common). Finally comes the body (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.
(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 backend (1), which responds with a tree of data (2), which is then propagated down from Root to its children and so on. (We omit the role of the client DB here for simplicity.)
Beware: You must not copy and paste the child’s query directly 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
. How does the data get into the client database, you ask? See the discussion of load!
later on.
Tip
|
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 componet’s properties are resolved directly against the client database or, when load! -ed, against global Pathom resolvers. 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.
|
(defsc Person
[_ props]
{..
:ident (fn [] [:person/id (:person/id props)])} ..)
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 could simplify the whole thing to :person/id
using the keyword ident form but we can ignore that for now.)
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.
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
-
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 (f.ex. dynamic routers do)
-
(?) When the component is used as a target of a dynamic router
(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>
<[optional] keyword encoding classes and an element ID> ; (1)
<[optional] map of the tag's attributes (or React props)>; (2)
<[optional] children>) ; (3)
-
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>}
. -
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 containnil
- those will be removed. It is merged with any classes specified in the keyword shorthand form. -
Zero or more children
- 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 tocomp/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 ofdefsc
. 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!")}))
Read more in Passing Callbacks and Other Parent-computed Data.
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.
When a component needs to change something outside of itself, it does so through submitting mutations to the transaction subsystem via comp/transact!
.
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.)
#?(: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)
-
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
-
action
is what should happen first. Here we can directly change the client DB (state
, an atom) -
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 nameremote
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.) -
ok-action
is called after the remote mutation succeeds. Notice that in Fulcro mutations and queries generally never "fail" and rather return data indicating that something went wrong. You can submit other mutations etc. from here. -
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.
-
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})
) -
…or we provide the symbol directly
If you transact!
multiple mutations then their action
will be processed in order. However, if they have a remote part, Fulcro does only send it but does not wait 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
.
Before looking into loading remote data, we must understand how a (denormalized) tree of data can be merged and normalized into the client DB. There is no point in loading data unless we can put them into the client DB, the only place where Fulcro ever looks.
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 (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 in 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!
gets the ident and query of the given component (and leverages the metadata on 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 the docstring for details).
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.
;; 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
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)
-
: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) -
: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) -
:replace
can replace an element of a to-many vector given a path ending with an index and provided it already exists -
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.
Now that you understand the merging of data into the client DB, you can load data with df/load!
, which is just merge-component!
that - given a property or an ident that Pathom can resolve - obtains data from the remote. (Needless to say, there need to be Pathom resolvers being able to provide the data you are asking for.)
The signature of load!
is:
(df/load! app-or-comp ; (1)
src-keyword-or-ident ; (2)
component-class ; (3)
options) ; (4)
-
Pass in a reference to the Fulcro
app
or a component’sthis
(the first argument ofdefsc
) -
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}
) -
The component whose query defines which of the available properties to get and that is used when merging the returned data with
merge-component!
. -
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:
;; 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]]}}
-
Load an entity or list of entities from a global (input-less) resolver
-
Load an entity by ident
-
Load an entity by ident and add a reference to another entity, leveraging the
:target
option and the helpers in thetargeting
namespace
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.
-
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 :current-user User {:params {:username u :password p}})
. -
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 thetargeting
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. -
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. -
How to load only a subtree of data (f.ex. the one excluded earlier with
:without
)?The opposite of the
:without
option is the functiondf/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. -
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. -
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 calltransact!
.
When to call load!
? The main options are:
-
When your application is starting
-
In an event handler (e.g. onClick)
-
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 -
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.
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:
;; Somewhere during the app lifecycle:
(df/load! [:list/id :friends] Person {: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))))))
-
Ask
load!
to track the load with a marker called e.g.:friends-list
-
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) -
Get the marker from the props. Notice this is
get
and notget-in
because the whole ident is used as the key. -
Use the provided functions to check the status of the load and display corresponding UI
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 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.
-
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]
andPersonDetails
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.
OK, you have completed the tutorial. What now?
-
Install Fulcro Inspect and enable custom formatters in Chrome to display Clojure data nicely in the Console - trust me, these two are indispensable!
-
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.
-
Go back to Fulcro Developers Guide and read the introductory chapters to gain a deeper understanding