(Photo by Markus Spiske on Unsplash)
Today I used react-error-boundary and thought I'd blog about it - just it's usage in a project.
Basically, I had something like the following in a component:
{selectIsRetryInitialDataFetchRequired(state) ? (
<X {...propsA} />
) : selectIsLoginAgainRequired(state) ? (
<X {...propsB} />
) : selectIsContactCustomerSupportRequired(state) ? (
<X {...propsC} />
) :
// etc... conditions to know when data loaded successfully
// render main UI
// fallback render spinner
The select functions are deriving values from state to know - based off of the responses of an HTTP client - what error UI to render (e.g. a 500 or JS fetch error in any of the requests made to get the data to be able to sensibly show the main UI would be an isRetryInitialDataFetchRequired
of true
).
Today I got the requirement that one of the HTTP requests being made will have another error response I'm to handle - still an error UI, but resolving it differs so it requires a different UI.
To be fair, this component (to me), is still quite easy to understand and reason about, especially since X
, the error handling UI component is just the same component with variations on text content and button actions. The meat is in the "happy path" which brings together the main components used by the micro app (a small React app that is loaded by another app in a very specific use case).
The addition to the error UIs, though, has a little more going on and it's starting to get annoying to keep everything there.
Long story short, I ended up replacing those error branches with:
useErrorHandler(selectIsErrorFallbackRenderRequired(state));
where selectIsErrorFallbackRenderRequired
is just a combination of the previous selectors (and soon to have another):
export const selectIsErrorFallbackRenderRequired = (state) =>
selectIsRetryInitialDataFetchRequired(state) ||
selectIsLoginAgainRequired(state) ||
selectIsContactCustomerSupportRequired(state);
useErrorHandler
is from react-error-boundary. When the selector returns true, it ends up rendering its closest ErrorBoundary
… so obviously, I also had to add one of those at the root level:
<ErrorBoundary
fallback={<ErrorFallback state={state} {...otherProps} />}
resetKeys={[selectIsErrorFallbackRenderRequired(state)]}
>
<App state={state} />
</ErrorBoundary>
ErrorFallback
is now responsible for rendering any error UI the micro app may have.
The resetKeys
also gets passed selectIsErrorFallbackRenderRequired(state)
i.e.
From my state - calculate whether the app needs to render one of it's error UIs - and if this calculated value ever changes, reset i.e. render the
ErrorBoundary
's children instead of the fallback.
How I'm thinking about it is - this is my way in the ErrorFallback
:
useErrorHandler(selectIsErrorFallbackRenderRequired(state));
… and this is my way out:
resetKeys={[selectIsErrorFallbackRenderRequired(state)]}
ErrorFallback
gets passed state
so it can do its own selections and render appropriately.
Seems to be working so far 🙂
Apart from separation of concerns, it has the added benefit of catching errors which React's error boundaries can catch (and default to the "contact support" error UI) - not that I'm expecting that to happen with tests and Typescript thrown in the mix 😛
Kudos to Kent C. Dodds and the OS community for another great tool 👏
Top comments (0)