Skip to content

Latest commit

 

History

History
389 lines (310 loc) · 21.8 KB

PORTING-FROM-2.x.adoc

File metadata and controls

389 lines (310 loc) · 21.8 KB

Porting From Fulcro 2.x

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 receive this or props from the argument list. This is the biggest change.

defrouter

The union-based router is now in legacy-ui-routers as defsc-router. The old defrouter 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

Creating An Application

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.

Significant Changes

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.

Defsc

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.

Transaction Changes

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 a transact! in some part of the result-action (see below) of your mutation. ptransact! still exists for easy porting. There is an options map that can be passed to the new transact!. The optimistic? flag can be turned to false to get the exact behavior of ptransact! 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.

Mutation Generalizations

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 a result-action, then the defmutation macro supplies a default that behaves a bit like Fulcro 2 with some Incubator features added in (there is now an ok-action and error-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).

Using Inspect

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]}}

Namespaces and Name Changes

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.

API Improvements that Should Not Hurt

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 on shouldComponentUpdate 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 within swap!, 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 backing defmulti 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 an env 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 to default-result-action, which delegates to ok-action and error-action using the defition of remote-error? supplied to the app on startup.

  • df/load! and comp/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.

Breaking API Changes

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 and error-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 use defmethod 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 to fulcro-app, and you can turn on/off auto inclusion of initial state from root via mount!.

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, and with-params helpers take env now. The return value of client-side mutations now support returning the env in addition to the original boolean or AST. The append-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, as pathom 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.