Error boundaries and cached components
In my journey to build this blog, as I mentioned in the first post, I created a custom editor where I can write my articles, using MDX
, a mix of Markdown
and JSX
, this way I can write some powerful and dynamic articles, with code and live execution of that code.
That image above is an example of how it looks right now, not too many options, but as you can see there's a live preview on the right side, and this is what I want to talk about now. As a live preview, it updates on every change made in the editor on the left, even when writing JSX
code. This was challenging to me, because the component crashes if there is invalid MDX on the editor, for example, when you're just typing some code, like this incomplete button: <butt
. It completely crashes, without any option to recover.
To fix this, I thought on a way to "cache" the component, in the last valid state, if there's any error in the future state, instead of throwing an error, it will render the cached version, until the MDX
in the editor is valid.
Error Boundaries
In React there is a concept called Error Boundaries, that let's you create a class component that will handle wherever "crash" happens on your code. Imagine it like a try/catch
feature. I won't go into depth in that here, instead I recommend you to read that piece of documentation.
If you were paying attention, you saw that I said class component previously and that's because there's no way, natively, to do this using functional components, but there's a library that let's us do this called react-error-boundary
. Read this article by Kent C. Dodds that explains how it works.
Caching a component
In the example presented on the previous article I linked, he uses the ErrorBoundary
component that you can import from the library, here I'm going to use a HOC
(High Order Component) that will let us do the same, this is called withErrorBoundary
.
The following example is the exact same thing that I'm using right now for my preview component:
const Component: FunctionComponent<ComponentProps> = ({ content }) => {const [validState, setValidState] = useState<string>('');const WithErrorComponent = withErrorBoundary(ComponentThatCanThrow, {FallbackComponent: () => CacheComponent,onError() {console.log('onError');},});const CacheComponent = useMemo(() => (<ComponentThatCanThrow state={validState} setValidState={setValidState} />), [validState]);return <WithErrorComponent state={content} setValidState={setValidState} />};
Here we're defining Component
as a component that will render the ComponentThatCanThrow
component. To avoid it from throwing an error and crashing the page, we manage an state validState
and, with the help of the useMemo
API, we cache a version of the ComponentThatCanThrow
with the valid state.
On line 4 we use the withErrorBoundary
HOC that I mentioned previously:
const WithErrorComponent = withErrorBoundary(ComponentThatCanThrow, {FallbackComponent: () => CacheComponent,onError() {console.log('onError');},});
This is the actual component that we end up returning on our component and its definition is pretty straightforward. The first argument is the component that can throw and then we pass some custom props to it, the first is the fallback that we want to render, in case of an error and the second is just a callback that will be executed when we have an error.
In our case, following the first code above, the fallback component is the same component that can throw, but with a validState
, so our component will render the same cached one until it finds itself with a valid state again. We pass the validState
and setValidState
props to it, so it will update the state when is a valid one.
Real component
Until now we only know that the component that can throw is called ComponentThatCanThrow
, so let's take a look at how it should be to work with our implementation:
const ComponentThatCanThrow: FunctionComponent<ComponentProps> = ({ state, setValidState }) => {useEffect(() => {setValidState(state);}, [state]);return (<MDX>{state}</MDX>);};
Here the part that can throw is the MDX
component. We update the validState
of the parent component after the first render, with the useEffect
API. This will only be executed if the component doesn't throw with the current state
, so we know its a valid one. If it throws, it won't be updated and our CacheComponent
will keep the previous valid state.
I don't know exactly who needs something like this on the usual react project, but it certainly is useful to me on my website. Feel free to ask if there's is anything you want to know about it.