Fulcro 3 is intended to be as API-friendly to Fulcro 2 applications as possible but internal cleanup and changes mean that existing applications will have to at least make some name changes, and in many cases clean up arguments and logic. This document covers the known porting tasks. If you run into an issue when porting that is not documented here please ask on the Fulcro Slack channel in Clojurians.
defsc
-
The new
defsc
looks like the old one, and even does some improved error checking; However,defsc’s option map now supports user-additions. Anything you include in that map now appears (unaltered) in `component-options
. This allows library authors to co-locate information on a component without having to modify the macro. The "magic" behaviors of query/ident/initial-state (and error checking) are still present for developer aid and bw compatibility. IMPORTANT: The remaining methods (e.g. component lifecycle methods) no longer receivethis
orprops
from the argument list. This is the biggest change. defrouter
-
The union-based router is now in legacy-ui-routers as
defsc-router
. The olddefrouter
syntax is no longer supported. See the docstring of the new function. easy-server
-
Removed. Copy source from 2.x if you need any of it.
- Configuration
-
The EDN config file stuff is now in a ns. See Namespaces.
- Namespaces
-
All of the namespaces changed:
├── com
│ └── fulcrologic
│ └── fulcro
│ ├── algorithms
│ │ ├── data_targeting.cljc ; targeting for loads/mutations
│ │ ├── denormalize.cljc ; db->tree
│ │ ├── form_state.cljc ; from 2.x
│ │ ├── indexing.cljc ; internal
│ │ ├── lookup.cljc
│ │ ├── merge.cljc ; merge-component!, etc.
│ │ ├── normalize.cljc ; tree->db
│ │ ├── normalized_state.cljc ; Helpers for manipulating the normalized state db in mutations.
│ │ ├── react_interop.cljc ; Helpers for using raw js React components
│ │ ├── scheduling.cljc ; internal
│ │ ├── server_render.cljc ; SSR support
│ │ ├── tempid.cljc ; from 2.x
│ │ ├── timbre_support.cljs ; Logging config to make timbre logging nicer with Fulcro
│ │ ├── transit.cljc ; from 2.x
│ │ ├── tx_processing.cljc ; internals of new transact!
│ │ └── tx_processing_debug.cljc ; debugging util for internals of new transact!
│ ├── application.cljc ; App constructor: fulcro-app
│ ├── components.cljc ; get-query, get-ident, etc.
│ ├── data_fetch.cljc ; from 2.x: load, load-data, etc.
│ ├── dom
│ │ ├── events.cljc ; various helpers from 2.x
│ │ ├── html_entities.cljc
│ │ └── icons.cljc
│ ├── dom.clj ; DOM from 2.x
│ ├── dom.cljs
│ ├── dom_common.cljc
│ ├── dom_server.clj ; DOM for SSR
│ ├── inspect ; package of nses for talking to Chrome Inspect plugin
│ │ ├── preload.cljs ; Use THIS in preloads for using Inspect. Do NOT include inspect as a dependency.
│ │ └── websocket_preload.cljs ; Use THIS in preloads for using Inspect Electron. Do NOT include inspect as a dependency.
│ ├── mutations.cljc ; client-side defmutation
│ ├── networking
│ │ ├── file_upload.clj ; Middleware for handling file upload support
│ │ ├── file_upload.cljs ; Client middleware for handling file upload support
│ │ ├── file_url.cljc ; Utilities that can convert a binary return value from a mutation into a data url.
│ │ ├── http_remote.cljs ; normal client remote (no longer uses protocols)
│ │ └── mock_server_remote.cljs ; mock server for use in cljs
│ ├── rendering
│ │ ├── ident_optimized_render.cljc ; Default rendering optimization. Not perfect yet, but fast.
│ │ └── keyframe_render.cljc ; More like Fulcro 2.x rendering. Relies on shouldComponentUpdate for performance.
│ ├── routing
│ │ ├── dynamic_routing.cljc ; New UI Router promoted from Incubator
│ │ └── legacy_ui_routers.cljc ; Fulcro 2.x routers (old dynamic router untested, possibly broken)
│ ├── server
│ │ ├── api_middleware.clj ; Central API middleware from 2.x
│ │ └── config.clj ; Config file support from 2.x
│ ├── specs.cljc
│ └── ui_state_machines.cljc
└── data_readers.clj
The internals have been completely rewritten. There is no reconciler, (almost) no protocols, etc. A Fulcro application
is implemented as a map, and that app
is usable everywhere the reconciler was in 2.x.
Important
|
There is no need to store an app in an atom because it uses atoms internally to deal with state. DO NOT
reset! the value of the app from the return of mount! , as mount! no longer returns the app!
|
For hot code reload, you should use defonce
to make your application in some central location that can be used from
any other namespace:
(ns my.app
(:require
[com.fulcrologic.fulcro.networking.http-remote :as fhr]
[com.fulcrologic.fulcro.application :as app]
[com.fulcrologic.fulcro.components :as comp]
[com.fulcrologic.fulcro.data-fetch :as df]
[my.app.ui :as ui]
[taoensso.timbre :as log]))
(defonce app (app/fulcro-app {:remotes {:remote (fhr/fulcro-http-remote {:url "/api"})}}))
At some point in your logic you will want to associate the root of your UI with the application via app/mount!
:
(app/mount! app ui/Root "app")
Calling this function on a mounted app will simply refresh the mounted app’s UI.
See also the porting guide in the main repo root at PORTING-FROM-2.3.adoc.
I call these significant more for their long-term implications than their impact on existing code. Most existing code will be relatively easy to port to Fulcro 3, and should operate without much further change; however, some of the "hard edges" of Fulcro 2 are solved by these changes, and as such they are "significant" in that sense.
As mentioned earlier: defsc
no longer uses protocols at all. The options map is "beefed up" by the defsc
macro,
but in fact you can simply create a "contructor function" and call configure-component!
on it and pass a (non-magic)
options map to create a component. The macro just helps you with typos and is easier to read.
This also means things like CSS can now be a pure library concern. In fact, the fulcro-garden-css
library is where CSS
functionality lives now.
BREAKING CHANGE: All React lifecycle methods must now declare this
as an option. Carefully examine the docstring from
defsc
in 2.x vs. 3.x.
The most significant change is in the internal plumbing of transact!
, which is now in the component
namespace. Transactions are now safe
to submit from anywhere in the code base.
The transact!
function just puts the tx on a submission queue. That’s it. At some point (very soon) after submission
Fulcro will process the current submissions into an active queue.
Note
|
My intention is to make the transaction plumbing "pluggable" (it is already structured to be) so that various approaches to transaction semantics can be implemented as standard or even library concerns. |
This simplifies a lot of things:
-
You no longer need
ptransact!
. Just embed atransact!
in some part of theresult-action
(see below) of your mutation.ptransact!
still exists for easy porting. There is anoptions
map that can be passed to the newtransact!
. Theoptimistic?
flag can be turned to false to get the exact behavior ofptransact!
if your application is written to use it. -
Timing issues in dynamic routing and ui state machines should be easier to avoid/solve.
-
You can submit transactions without using
setTimeout
and be sure they will activate in the order submitted.
Mutations have become an even more central notion in the library. All versions of Fulcro have actually treated loads internally as mutations, because in fact a load is a combination of some state changes (recording the fact that something is loading, i.e. load markers) and fetching the actual data.
Prior versions of Fulcro had Om Next structure in the middle. Version 3 does not. The logic in 3 is much more direct:
-
A transaction is written as it always has been
-
Each element of the transaction (mutations) can choose local and remote behaviors
-
Optimistic actions run first
-
Remote actions go on a queue and run in order
All of that should sound pretty much identical to what you’ve been doing all along. The big difference is what happens next:
-
Network results are delivered to a new
result-action
section of the mutation. If the user does not supply aresult-action
, then thedefmutation
macro supplies a default that behaves a bit like Fulcro 2 with some Incubator features added in (there is now anok-action
anderror-action
section as well).
As a result any full-stack operation is completely under your control, and you can even "invent" new sections of
the mutation that will appear as dispatch
in the env
:
(defmutation do-thing [params]
(action [env] ...optimistic actions...)
(remote [env] true)
(ok-action [env] ...your custom action type!...)
(result-action [{:keys [result app dispatch] :as env}]
(let [{:keys [status-code body]} result
{:keys [ok-action]} dispatch]
(if (= 200 status-code)
(ok-action env)
...))))
This maintains backward compatibility while also giving you the power to implement things like
pmutate
from incubator without having to resort to magical transaction transforms. The fact that
you can trigger new transactions from any part of that code means that chaining behaviors is now
trivial and no longer needs the concept of ptransact!
(though there is an :optimistic? false
option
of the new transact!
that emulates that behavior.
Important
|
Incubator’s pessimistic mutations place the return value of mutations at a special key in app state during processing,
which can facilitate component-local UI rendering of things like errors. Fulcro 3
allows you to define that behavior, but instead makes the complete network result available in the mutation env . Thus, you
could make things look more like incubator just be replacing the default mutation action.
|
Interestingly, this also makes it super easy to generalize the implementation of loads even more than before. Loads are now implemented internally something like this (simplified for ease of understanding):
(defmutation internal-load! [{:keys [query marker] :as params}]
(action [{:keys [app]}] (set-load-marker! app marker :loading))
(result-action [{:keys [result app] :as env}]
(if (load-error? result)
(load-failed! env params)
(finish-load! env params))))
(remote [{:keys [ast]}] (eql/query->ast query)))
Note
|
The data-fecth API (e.g. load , renamed to load! ) still exists, and is pretty much like it was. The primary change is boolean/in-place
load markers are no longer supported.
|
Warning
|
The multimethod mutate is still at the center of this; however, the arguments have changed. The multimethod
is sent only an env , which contains (→ env :ast :params) .
|
Do NOT include Fulcro Inspect as a dependency. Instead, Fulcro now includes the client-side code necessary to talk to the Chrome extension without pulling in all of inspect’s dependencies. Just add the following preload:
:builds {:app {:target :browser
...
:devtools {:preloads [com.fulcrologic.fulcro.inspect.preload]}}
Since the API changed, we thought it a good opportunity to clean up some naming and split things into smaller files. This will help with long-term maintenance of the project.
If you look at the com.fulcrologic.fulco
package you should be able
to guess the location of the function you need. There are some functions
that were moved out of Fulcro completely or were dropped because they
were deprecated or no longer made sense.
The following list documents some common ones that you might be using, and where to find them now:
ident?
-
Most EQL-related functions like this are in the
edn-query-language.core
namespace. merge-*
-
Merge-related logic is now in the
com.fulcrologic.fulcro.algorithms.merge
namespace.
There are many other renames, but a quick grep of the source should make it obvious where the new one is.
- Default query transform
-
When issuing loads the new code elides
:ui/…
keywords and also the form-state config join. - Rendering
-
The default renderer no longer needs "follow-on reads". Performance testing showed that the process of trying to figure out UI updates from the indexes added more overhead than they saved. The rendering optimizations are actually pluggable, and two versions of the algorithm are supplied: one that always renders from root (
keyframe-render
) and relies onshouldComponentUpdate
for performance, and one that uses database analysis to find the minimum number of components to update. Different applications might find one better than the other depending on usage patterns. Both should be faster than Fulcro 2 for various internal reasons. - Pessimistic Transactions
-
These were always a bit of a "hard edge" in Fulcro 2. In the JS world the ability to "chain" operations in callbacks with async/await is just sometimes desirable. Fulcro 3 allows this sort of thing more directly (though it still keeps it out of the UI layer). The new mutation abilities allow you to chain your next operation from the network result of a prior one right in the mutation’s declaration.
ptransact!
is technically still supported, and actually should even have a bug or two fixed, but should probably not be used in new applications. - Transactions
-
The new transact API uses a proper submission queue. This gets rid of internal uses of core async, makes the internals more visible to APIs and tooling, and even allows for the entire transaction processing system to be "pluggable". The new system allows
transact!
to be safely called from anywhere. Semantically speaking it should not be called from withinswap!
, though in js (single-threaded) even that would not hurt anything with the new implementation. defmutation
-
The
defmutation
macro on the client side "looks" the same as the old one; however, it is quite a bit more powerful. Lessons learned in incubator and with pessimistic mutations led to a complete redesign. Dumping the internal structure of Om Next simplified the whole process greatly. The backingdefmulti
is still there, but the arguments changed (it takes only and env now), and you are guaranteed that the built-in tx processing will only ever call the method once. Remotes are now truly lambdas (instead of values), and receive an env that allows them to see app state as it existed before the optimistic update. IMPORTANT: The return value of remotes can now be a boolean, ast , or env. All of the mutation return value helpers (e.g.m/returning
) now accept anenv
so you can thread them together more easily.
(defmutation f [params]
(remote [env]
(-> env
(m/with-params {:x 1})
(m/returning AThing))))
Here is a summary of mutation changes:
-
env
no longer has the key:reconciler
, but it does have:app
. -
result-action
is the catch-all action block for network results, and defaults todefault-result-action
, which delegates took-action
anderror-action
using the defition ofremote-error?
supplied to the app on startup. -
df/load!
andcomp/transact!
can be used within mutation bodies.load-action
is gone, and you don’t need to make a remote section for loads anymore. -
Remote can return env, boolean, or AST. The helpers like
returning
take (and return)env
.
- Full-Stack
-
The happy path is mostly the same for full-stack operation; however, The overall network error handling is completely different. There is still a global error handler, but it gets a more detailed environment (and a new name). Mutation fallbacks are no longer supported. See the new mutations, which have
result-action
anderror-action
sections to handle per-mutation errors. Load fallbacks are still supported, as they were already targeted to the load in question. Global network activity and error markers have changed. See the developer’s guide for more details. - React Lifecycle Methods
-
The non-static React lifecycle methods (e.g. componentDidMount, shouldComponentUpdate, etc.) now all require an explicit
this
. Everything in the options map in Fulcro 3 except query/ident/initial-state are completely literal (e.g. lambdas or data). - Mutation Multimethod
-
The
defmulti
for mutations is still present, but the API it presents and the return value it expects have changed. If you directly usedefmethod m/mutate
you will need to adapt your code. - Initial State
-
The initial state story is the same if your initial state is purely on your root component. If you were passing an initial state to the
reconciler
, then that option has changed. You can pass a normalized database tofulcro-app
, and you can turn on/off auto inclusion of initial state from root viamount!
. - App vs. Reconciler
-
There is no longer a separate reconciler or indexer. Everything is in the app, and is held in atoms such that there is no need to do top-level swaps. Your app can be declared once in a namespace of it’s own and then used directly everywhere the reconciler could have been. There are no protocols involved.
- Returning and Targeting
-
The
returning
,with-target
, andwith-params
helpers takeenv
now. The return value of client-side mutations now support returning theenv
in addition to the original boolean or AST. Theappend-to
and related targeting wrappers are now in the targeting namespace. - Remotes
-
Remotes were protocol based, and are now simply maps. The primary "method" to implement is a function under the
:transmit!
key, which now receives a send node and should return a result that includes both the status code and EDN result. There are new versions of pre-supplied HTTP and Websocket remotes that should be top-level API compatible with your existing code. See their code for more details. - Server
-
Easy server is gone. Supported server middleware helpers and config support are in namespaces within the
com.fulcrologic.fulcro.server
package. Fulcro 3 no longer supplies server-side macros for mutations and reads, aspathom
is a much better choice for EQL service. Porting to Pathom is relatively minor, and if you want a "no source change" solution you can write macros like this (and change your requires to use them instead):
(def resolvers (atom []))
(s/def ::root-value (s/cat
:value-name (fn [sym] (= sym 'value))
:value-args (fn [a] (and (vector? a) (= 2 (count a))))
:value-body (s/+ (constantly true))))
(s/def ::query-root-args (s/cat
:kw keyword?
:doc (s/? string?)
:value #(and (list? %) (= 'value (first %)) (vector? (second %)))))
(defn defquery-root* [env args]
(let [target-sym (::sym env)
;; conform! is just an exception-throwing version of s/conform
{:keys [kw doc value]} (util/conform! ::query-root-args args)
{:keys [value-args value-body]} (util/conform! ::root-value value)
env-sym (first value-args)
params-sym (second value-args)]
`(do
(pc/defresolver ~target-sym [~'env__internal ~'_]
~(cond-> {::pc/output [kw]}
doc (assoc :doc doc))
(let [~env-sym ~'env__internal
~params-sym (-> ~'env__internal :ast :params)]
{~kw (do ~@value-body)}))
(swap! resolvers conj ~target-sym))))
(defmacro defquery-root [& args]
(defquery-root* (assoc &env ::sym (gensym "query")) args))
- UI State Machines
-
The names of a few parameters on API for doing loads and mutations were updated. The load
::uism/post-event
was renamed to::uism/ok-event
, fallbacks to error, etc. They match the API for triggering remote mutations now. The targeting namespace on the target for mutations was change to data-targeting, and the namespace for returning was change to normal mutations ns. The return value of mutations appears in ::uism/mutation-result now, and is the Fulcro 3 raw network result (status code, body, etc.). - Incubator Pessimistic Mutations
-
Incubator does not work with F3. The new mutations make it possible to implement the exact pmutations from incubator, but we did not adopt all of their functionality in the default mutation handler. See the developer’s guide for instructions on how to expand how mutations work on the client.