Logo of crpto

JC's Writing

My personal space to publish my thoughts

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.

Example of the current editor

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.

aravena.me