Fork me on GitHub
#reagent
<
2021-04-06
>
wombawomba22:04:51

So I'm using error boundaries together with hot code reloading when developing with Reagent, and I've noticed that 'triggered' error boundary often don't get refreshed when my code gets reloaded. Is there a way to force them to get re-rendered on reloads?

wombawomba22:04:56

FWIW I've tried sprinkling calls to force-update-all in my 'on-reload' fn, but to no avail:

(reagent.dom/force-update-all)
  (reagent.dom/render [views/app-root] (.getElementById js/document "app"))
  (reagent.dom/force-update-all)

p-himik22:04:39

How exactly does the error boundary code look like?

wombawomba22:04:14

@U2FRKM4TW

(defn <error-boundary>
  [& _]
  (let [*info (r/atom nil)]
    (r/create-class
     {:constructor                  (fn [this _props]
                                      (set! (.-state this) #js {:error nil}))
      :component-did-catch          (fn [_this _e info] (reset! *info info))
      :display-name                 "error-boundary"
      :get-derived-state-from-error (fn [error] #js {:error error})
      :render                       (fn [this]
                                      (r/as-element
                                       (if-let [error (.. this -state -error)]
                                         [<error-message>
                                          error
                                          (some-> @*info .-componentStack)]
                                         (into [:<>] (r/children this)))))})))

p-himik22:04:07

I don't see anything necessarily wrong with it, but try this one. It works perfectly in my case.

(defn component []
  (let [error (reagent/atom nil)]
    (reagent/create-class
      {:component-did-catch
       (fn [_this _e _info])

       :get-derived-state-from-error
       (fn [e]
         (reset! error e)
         #js {})

       :reagent-render
       (fn []
         (if @error
           [error-component @error]
           [main-component]))})))

wombawomba22:04:32

yeah, no go 😞

wombawomba22:04:00

I'm using shadow-cljs btw... although I don't think this should affect this sort of thing?

p-himik22:04:05

Hmm. And how exactly do you re-render the app on code reload.

p-himik22:04:11

I use shadow-cljs as well.

wombawomba22:04:57

I have a fn that looks like

(defn ^:dev/after-load re-init
  "(Re)initializes the state. Called on page load and code reload."
  []
  (log/debug "Re-initing...")
  (effect-handlers/init!)
  (routes/init!)
  (sub-handlers/init!)
  (reagent.dom/force-update-all)
  (reagent.dom/render [views/app-root] (.getElementById js/document "app"))
  (reagent.dom/force-update-all)
  (css/init-static-classes!)
  (set-config!))

wombawomba22:04:32

wait a second

wombawomba22:04:44

I think I see something weird in my shadow-cljs config

wombawomba23:04:06

yeah okay fixing that thing didn't help

wombawomba23:04:35

my shadow-cljs config is basically

{...
 :build {:app {...
               :modules  {:base {:init-fn site.core/init
                                 :entries [site.core]}}
               :devtools {:after-load  site.core/re-init
                          :loader-mode :script
                          :preloads    [shadow.remote.runtime.cljs.browser]}}}

wombawomba23:04:14

I just confirmed that init and re-init get called on page load, and re-init gets called on reload

wombawomba23:04:46

weirdly I have no problems with any other elements not getting reloaded

p-himik23:04:43

Huh. No clue, sorry.

wombawomba23:04:31

yeah very strange

wombawomba23:04:52

I'm gonna take another look at my init code to see if there's anything weird

wombawomba23:04:04

perhaps I'm getting my re-frame effects mixed up

p-himik23:04:19

Now that you mention re-frame - do you really store *info in a ratom or was it just to replace some subscription?

wombawomba00:04:44

I store it in an atom 🙂

wombawomba00:04:16

I use plain reagent for 'intra-component' state

wombawomba00:04:34

and yeah as it turns out, that's the problem

wombawomba00:04:04

reagent doesn't reset *info when I reload

wombawomba00:04:33

I had the same problem with the error-boundary component you proposed, because it also keeps state in an atom

wombawomba00:04:46

I'm honestly kind of stumped as to how I should approach this

wombawomba00:04:44

basically the call order seems to be 1. get-derived-state-from-error 2. render 3. component-did-catch

wombawomba00:04:23

I want to use the info arg, that's only provided to component-did-catch to trigger a re-render (so I can display that state)

wombawomba00:04:02

is there a way to do this without wrapping the component in (let [*info (r/atom nil)] )?

wombawomba00:04:30

alternatively, is there a way to force that atom to be cleared when the page gets reloaded?

wombawomba01:04:31

....okay, I finally got it working the way I want

wombawomba01:04:40

(defn <error-boundary>
  "Util for inserting an error boundaries in the React tree. When a child throws
  an error, just renders that error instead of crashing."
  [& _]
  (let [*error (atom nil)]
    (r/create-class
     {:component-did-catch          (fn [this e i]
                                      (reset! *error [e (.-componentStack i)])
                                      (.forceUpdate this))
      :display-name                 "error-boundary"
      :get-derived-state-from-error #(reset! *error [%])
      :render                       #(r/as-element
                                      (if-let [[error stack] @*error]
                                        [<error-message> error stack]
                                        (into [:<>] (r/children %))))})))

wombawomba01:04:55

plain old non-reagent atoms (and forceUpdate) to the rescue 🙂

p-himik08:04:53

Nice! Yeah, now reading your messages, I realize that I probably have the same problem. And it's just that I've added an "Undo" button to the re-frame app specifically to undo whatever action has led to the error that prevented me from noticing it.

👍 3