React 16 has been out for a couple years now, and we recently upgraded our Reagent application at work from 15.4 to the latest. I thought I would share one of the best quality of life improvements I’ve found since upgrading: Error Boundaries.
The way I first found them useful was when hacking on some new visualizations that we were going to use throughout our app. Like usual, I immediately created a new namespace, with DevCards loaded in it, to give me a quick visual REPL experience while using hot reloading.
Because I’m not very experienced with charting / data viz libraries, though, I quickly blew up something inside of my component’s render cycle and was left with a blank screen. I had to do a full refresh of the page, as DevCards did not handle the error gracefully. Boo!
This happened a few more times before I finally got fed up with it. Remembering some experiments I had done with error boundaries before, I whipped up a quick and dirty Form-3 component:
(defn err-boundary
[& children]
(let [err-state (r/atom nil)]
(r/create-class
{:display-name "ErrBoundary"
:component-did-catch (fn [err info]
(reset! err-state [err info]))
:reagent-render (fn [& children]
(if (nil? @err-state)
(into [:<>] children)
(let [[_ info] @err-state]
[:pre [:code (pr-str info)]])))})))
This component uses the componentDidCatch
method introduced in React 16.0.
How it works to recover from errors is, whenever a component throws an error
while rendering, React will go up the component tree and see if any components
implement componentDidCatch
. When it finds one, it will run that method and
stop propagating the error, just like a try/catch
.
In this case, our componentDidCatch
will update some local state in our
Reagent component with the error information and render the error on the page as
a string.
To see how it’s used, let’s say we had a silly component defined like so:
(defn my-component []
(throw (js/Error. "Oops! 👻")))
And were iterating on it in a devcard:
(dc/defcard-rg
my-component-card
[err-boundary
[my-component]])
Now, instead of breaking our page and forcing us to do a full refresh, it will print the error as a string in the page and, when we make a change to our code that triggers a hot reload, it will instantly clear itself and render with the fixed component. Woohoo!
A quick gotcha I ran into: you have to make sure that the error occurs within
the err-boundary
render method. For instance, the following won’t work:
(dc/defcard-rg
another-card
[err-boundary [:div (throw (js/Error. "Not caught!! 👿"))]])
Our error boundary is working just fine, the problem is that it hasn’t been rendered yet when the error is thrown. This means it’s not been instantiated by React yet, which means it can’t handle the thrown error!
Moving the error into a sub-component (like we did in our first example) will ensure that the error isn’t thrown until our error boundary is instantiated, which gives us the desired effect.
While error boundaries have been great for increasing my development workflow, I also anticipate them to be very useful for application logic as well. My guess is that a few carefully placed error boundaries can help our application clean up some of it’s tedious error handling code, and increase reuse of such logic across components. I’m excited to experiment with it!