Fork me on GitHub
#reagent
<
2019-07-30
>
lilactown16:07:43

I spent a couple hours yesterday debugging an app that’s for serializing arbitrary data and sending it over a wire for introspection. For some reason my EDN decoding was breaking on the other side. I finally realized that a Reagent Reaction’s IPrintWithWriter isn’t valid EDN: #<Reaction: ...>

Alex22:07:46

I'm currently using a combination of re-frame, reagent, and my own component library written in React. I have a React table component that takes a renderRow prop. renderRow is a function that follows the React function-as-a-child pattern; the table component passes some internal state and primitive table components to the renderRow function, which allows my consuming app to have control over rendering of the table rows. In CLJS land, I'm keeping track of some state via re-frame. Inside of my renderRow definition, I have code that looks something like the following

:renderRow
      (fn [props]
        (let [{:keys [isExpanded handleExpandableRowClick row]}
              (js->clj props :keywordize-keys true)
              user-id (:id row)
              roles @(rf/subscribe [::subs/roles-for-user row])
              roles (sort-roles-by-app-name roles)
              assigned-application-ids (map #(:application_id %) roles)]
        (print "Roles: " roles)
        (into [:<>] (for [role roles] [:div role]))
Even though the renderRow function is dereferencing a re-frame subscription, renderRow does not seem to get called when the data returned by the subscription changes, and as a result nothing in the renderRow function gets triggered. My guess is that unless props changes, the React table is not re-rendering and calling renderRow. Is there a way to force a re-render there? Or perhaps design my component in a way where I don't have to manually force re-renders?

lilactown22:07:01

whether or not dereferences cause re-renders is all about how you call the function

lilactown22:07:17

how are you actually rendering :renderRow?

Alex22:07:07

Inside of the React table, I'm calling something like

customRenderRow({
            ExpandableTableRowLink,
            NonExpandableColCell,
            isExpanded,
            handleExpandableRowClick,
            row,
            ExpandableTableRow,
          })

lilactown22:07:38

where is :renderRow?

lilactown22:07:44

ah OK, it doesn't use function-as-a-child but a render prop

lilactown22:07:52

the :renderRow is the name of a prop

Alex22:07:17

[:> DataTable
     {:expandable true
      :background "#FAFAFA"
      :displayExpandableColumn false
      :renderRow
      (fn [props]
        (let [{:keys [isExpanded handleExpandableRowClick row]}
              (js->clj props :keywordize-keys true)
              user-id (:id row)
              roles @(rf/subscribe [::subs/roles-for-user row])
              roles (sort-roles-by-app-name roles)
              assigned-application-ids (map #(:application_id %) roles)
    ...

Alex22:07:20

Correct it's a prop

Alex22:07:51

Ah yes sorry it is a render prop

lilactown22:07:17

you need to put your render-row definition inside of another component

lilactown22:07:07

the way that reagent works, is that the rerender-when-a-subscription-has-changed-that-I've-dereferenced magic is wired up when your component is parsed in a hiccup form

lilactown22:07:32

if you have a component like:

(defn render-row []
  (let [roles @(rf/subscribe [::subs/roles-for-user row])]
   [:div (pr-str roles)]))
and you call it like:
(render-row)
it will render only once. however, if you render it in a hiccup vector inside of a React element tree, it will reactively re-render:
(r/as-element [render-row])

lilactown22:07:32

so what I would do is pull out your render row definition into it's a named function, and then to your DataTable component do something like:

:renderRow (fn [props] (r/as-element [render-row (js->clj props :keywordize-keys true)]))

Alex22:07:39

Ahh thanks so much

Alex22:07:03

> rerender-when-a-subscription-has-changed-that-I've-dereferenced magic 😄

Alex22:07:26

so the key is to have the subscription inside of the component that's going to be rendered in hiccup form

Alex23:07:58

So I took your suggestion and moved my original renderRow function into a render-row reagent component. Now I'm passing the following into renderRow prop.

:renderRow (fn [props]
        (r/as-element [render-row props]))
And inside of the render-row function, I have something like...
(when isExpanded
       [:<>
        [render-unassigned-applications
         (merge common-props {:app-ids app-ids-to-assign})]
        [render-row-footer
         (merge common-props
                {:handleExpandableRowClick handleExpandableRowClick
                 :user-id user-id
                 :roles-to-update roles-to-update
                 :initial-roles initial-roles
                 })]])
where render-row-footer is
(defn render-row-footer
  [{:keys [expandable-row handleExpandableRowClick user-id  initial-roles]}]
  (let [roles-to-update @(rf/subscribe [::subs/roles-to-update.ui {:user-id user-id}])
        is-dirty? (not= initial-roles roles-to-update)]
    [:> expandable-row {:style {:height "72px"}}
     [:td {:colSpan "3"
           :style {:padding-left "16px"}}
      [:> Button
....
I'm expecting the render-row-footer to rerender-when-a-subscription-has-changed-that-I've-dereferenced but it still does not seem to be happening

lilactown23:07:03

I agree, I would expect it to re-render

Alex23:07:25

It's possibly my subscription inside of render-row-footer is incorrect and it's not updating. I'll double-check that

Alex23:07:06

Ah what do you know.. it is incorrect 🙂

Alex23:07:18

Anyways all makes sense with the world again.. thank you again!