Fork me on GitHub
#fulcro
<
2020-07-09
>
Eric Ihli04:07:25

Struggling to resolve these errors.

core.cljs:159 ERROR [com.fulcrologic.fulcro.routing.dynamic-routing:167] - There is a router in state that is missing an ID. This indicates that you forgot to compose it into your initial state! It will fail to operate properly.
ERROR [com.fulcrologic.fulcro.routing.dynamic-routing:188] - dr/target-ready! was called but there was no router waiting for the target listed:  [:component/id :user-form] This could mean you sent one ident, and indicated ready on another.
I added a log to router-for-pending-target but haven't yet figured out what causes the router in state to not have an id. The result of the log is below in case it makes it clear to someone. I assume it's related to http://book.fulcrologic.com/#_initial_state_2 but I have an initial-state and pre-merge.
(defn router-for-pending-target [state-map target]
  (let [routers (some-> state-map ::id vals)]
    (log/info routers))
  (let [routers   (some-> state-map ::id vals)
        router-id (reduce (fn [_ r]
                            (when (and #?(:clj true :cljs goog.DEBUG) (nil? (::id r)))
                              (log/error "There is a router in state that is missing an ID. This indicates that"
                                "you forgot to compose it into your initial state! It will fail to operate properly."))
                            (when (= target (some-> r ::pending-route :target))
                              (reduced (::id r))))
                    nil
                    routers)]
    router-id))
INFO [com.fulcrologic.fulcro.routing.dynamic-routing:3] - ({:com.fulcrologic.fulcro.routing.dynamic-routing/current-route [:component/id :home], :com.fulcrologic.fulcro.routing.dynamic-routing/pending-route {:error-timeout 5000, :deferred-timeout 20, :path-segment ["profile"], :router [:com.fulcrologic.fulcro.routing.dynamic-routing/id :com.scratchoff-odds.ui/RootRouter], :target [:component/id :user-form]}})
(defsc Root [this
             {:keys [current-user]
              ::keys [sign-up-form login-form navbar user-form]
              :root/keys [router]}]
  {:query [{::sign-up-form (comp/get-query SignUpForm)}
           {::login-form (comp/get-query LoginForm)}
           {::user-form (comp/get-query ui-user/UserForm)}
           {:root/router (comp/get-query RootRouter)}
           {::navbar (comp/get-query menu/Navbar)}
           :current-user]
   :initial-state (fn [_] {::user-form (comp/get-initial-state SignUpForm)
                           ::login-form (comp/get-initial-state LoginForm)
                           ::navbar (comp/get-initial-state menu/Navbar)
                           :current-user {}
                           :root/router {}})
   :pre-merge (fn [{:keys [data-tree]}]
                (merge (comp/get-initial-state Root)
                       data-tree))}

Eric Ihli04:07:49

Ahh. Happens so often that I spend an hour on something just to figure it out 5 minutes after asking. Looking at the definition for defrouter I see where it puts ::id in initial state. The example in the book tricked me up. I think it's supposed to be :initial-state {:settings/panes-router (comp/get-initial-state SettingPanesRouter)} .

(defsc Settings [this {:settings/keys [panes-router]}]
  {:query         [{:settings/panes-router (comp/get-query SettingPanesRouter)} ...]
   :initial-state (:settings/panes-router {}) ;; include in init. state
   :pre-merge (fn [{:keys [data-tree]}]
                (merge (comp/get-initial-state Settings) ;; include in dynamically-loaded state
                       data-tree)) ...}
    (ui-settings-panes-router panes-router))

Eric Ihli12:07:41

Can you confirm that the only issue there is the syntax issue? The defrouter macro has the following code:

initial-state-map      (into {::id            id
                                         ::current-route `(comp/get-initial-state ~(first router-targets) ~'params)}
                                    (map-indexed
                                      (fn [idx s] [(keyword (str "alt" idx)) `(comp/get-initial-state ~s {})])
                                      (rest router-targets)))
           ident-method           (apply list `(fn [] [::id ~id]))
A router's initial state includes a ::id key. If we don't call get-initial-state of the router from the parent, then won't the router's initial state be missing that ::id key?

Jakub Holý (HolyJak)16:07:43

Ie {} means calling get-init-state

bbss05:07:36

I must admit I also struggle quite a bit with the router 😓

bbss05:07:30

I don't understand how to nest routers and the docs mostly consisting of pseudo code doesn't help. I couldn't find a good example of nested routing, so when I get errors like

Jakub Holý (HolyJak)07:07:45

this typically happens due to the router not being included in the state of its parent. Check the parent in the DB, is the router data there? If not, use init. state or pre-merge to get it in as appropriate

bbss05:07:48

WARN [com.fulcrologic.fulcro.components:621] - get-ident was invoked on  nil  with nil props (this could mean it wasn't yet mounted):  [object Object]
eval @ core.cljs:159
eval @ timbre.cljs:502
eval @ core.cljs:6973
eval @ core.cljs:6973
cljs$core$_kv_reduce @ core.cljs:713
cljs$core$reduce_kv @ core.cljs:2578
eval @ timbre.cljs:442
eval @ components.cljc:621
eval @ components.cljc:935
eval @ components.cljc:933
eval @ components.cljc:939
eval @ ui_state_machines.cljc:911
eval @ ui_state_machines.cljc:901
...
It's hard to find out whether it's me (probably) or a bug.

lgessler05:07:26

@bbss I had a similar struggle this week and figured it out eventually, see root.cljs and project.core.cljs https://github.com/lgessler/glam/tree/master/src/main/glam/client/ui

lgessler05:07:35

not sure it's 100% golden code but it works

bbss05:07:44

@lgessler thanks! I'll check it out!

zilti15:07:29

Okay, really mysterious thing: I load data for a component, but the component actually gets the props nested in a vector, so I get a vector with one entry, and that one entry is the props map. Instead of getting the props map directly.

magra15:07:02

If you are on RAD, I do not know anything about that. Props get passed in from the parent. So your DB ansered with a to-many instead of to-one. So fix that, or if it actually is to many than call (map ui-component data) instead of (ui-component data). On the db side that is often ?e . (with the dot) to get datomic to answer with one entity instead of a seq with one.

magra15:07:34

The query can does not control to-one vs. to-many.

zilti16:07:21

Well that is clearly not what happens here, Because I load notification/all-notifications into :component/id ::Dashboard :activity-feed which correctly creates, according to the component query merge, a list of notifications in :component/id ::Dashboard :activity-feed :notifications. But it also makes it so :activity-feed itself is a list, containing one map with the keys :notifications and ::authorization/current-session

zilti15:07:20

Maybe to give the whole context: Parent component:

(defsc Dashboard [this props]
  {:query             [{:introform (comp/get-query introduction-forms/OneClickIntroForm)}
                       {:activity-feed (comp/get-query notification-forms/ActivityFeed)}]
   :ident             (fn [] [:component/id ::Dashboard])
   :initial-state     {:introform {}
                       :activity-feed {}}
   :route-segment     ["Dashboard"]
   :route-has-sidebar true
   :allowed-roles     #{:account.role/sysadmin
                        :account.role/tenant-admin
                        :account.role/company-admin}
   :will-enter      (fn [app args]
                      (dr/route-deferred
                       [:component/id ::Dashboard]
                       (df/load! app :notification/all-notifications notification-forms/ActivityFeed
                                 {:params {:account-uuid (-> (lib-form/session app) :session/account :account/id)}
                                  :post-mutation `dr/target-ready
                                  :post-mutation-params {:target [:component/id ::Dashboard]}
                                  :target [:component/id ::Dashboard :activity-feed]})))}
  #?(:cljs
      (let [session (lib-form/session this)]
        (dom/div :.ui.container
         (div :.ui.basic.vertical.segment
              (introduction-forms/ui-one-click-intro-form (:introform props)))
         (div :.ui.basic.vertical.segment
              (dom/h1 "Dashboard"))
         (div :.ui.basic.vertical.segment
              (notification-forms/ui-activity-feed (first (:activity-feed props))))
         (div :.ui.basic.vertical.segment
              (when (lib-form/has-role this :account.role/sysadmin)
                (ui-visualize-database-struct props)))))))
The next lower one:
(defsc ActivityFeed [this {:keys                [notifications]
                           :as                  props}]
  {:query [{:notifications (comp/get-query ActivityFeedRow)}]
   :ident (fn [] [:component/id ::ActivityFeed])
   :initial-state {:notifications {}}
   :allowed-roles #{:account.role/sysadmin
                    :account.role/tenant-admin
                    :account.role/company-admin}
   :componentDidMount (fn [this]
                        (df/load! this :notification/all-notifications ActivityFeedRow
                                  {:params {:account-uuid (-> this lib-form/session :session/account :account/id)}
                                   :post-action (fn [_] (log/info "I hope someone sees this"))
                                   :target [:component/id ::ActivityFeed :notifications]}))}
  (log/info "Props:" props)
  (div :.ui.container
       (div :.ui.four.column.grid
            (div :.row {:style {:border "1px solid #d7d8d8"}}
                 (div :.column "Time")
                 (div :.column "From")
                 (div :.column "Company")
                 (div :.column "Subject"))
            (mapv ui-activity-feed-row notifications))))
(def ui-activity-feed (comp/factory ActivityFeed))
And finally the row component:
(defsc ActivityFeedRow [this props]
  {:query [:notification/id :notification/title :time/created
           {:notification/triggering-user [:account/id :account/first-name :account/last-name {:account/company [:company/id :company/name]}]}]
   :ident :notification/id}
  (div :.row {:style {:border "1px solid #d7d8d8"
                      :marginTop "1em"}}
       (div :.column (str (:time/created props)))
       (div :.column (str (:account/first-name (:notification/triggering-user props)) " "
                          (:account/last-name (:notification/triggering-user props))))
       (div :.column (str (-> props :notification/triggering-user :account/company :company/name)))
       (div :.column (:notification/title props))))
(def ui-activity-feed-row (comp/factory ActivityFeedRow {:keyfn :notification/id}))

lgessler20:07:14

haven't read the whole thing but I noticed something odd in Dashboard: in :query you're using get-query for :introform and :activity-feed but in :initial-state you're not using get-initial state . any reason why you're not?

zilti08:07:05

...there's a comp/get-initial-state?

zilti08:07:58

What I've learned is to use an empty map in initial-state for keys with join queries

zilti16:07:52

I think it is wrong to begin with to have both df/load! instead of just one. But having just one doesn't work. Furthermore when I route to Dashboard while still on Dashboard, all data rows disappear. Seems really erratic.

tony.kay19:07:58

I don’t have time to read and understand your code, but I will reiterate a guiding principle that always helps me: Rendering in Fulcro is a pure function of state. Get the state right, and rendering will follow. The query/ident/composition has a shape that exactly matches the expected structure in app state. You do have to reason over that in time, but you need to learn to adapt to look at the data patterns. For example, just glancing (literally a scan that took 2 seconds) at your code I see you named the thing “notifications”, yet your initial state initializes it with a map. The prior indicates to-many, but the former initializes to-one. Anytime I have a “bug” I first look at query/ident/initial state and see if they make sense, THEN I look at app state and see if it matches what makes sense, THEN I look at my logic and see if it is also working with that same data shape. It’s all about the data.

❤️ 6