Skip to main content

Using Error Boundaries with Hooks (Part 1) #141

📖 Introduction

We've established that Error Boundaries must be class components due to their reliance on static getDerivedStateFromError() and componentDidCatch(). However, the React ecosystem is increasingly dominated by functional components and Hooks. So, how do we bridge this gap? This article, Part 1 of a two-part exploration, discusses how to use our class-based ErrorBoundary component to protect functional components and introduces the popular react-error-boundary library, which offers a more Hooks-idiomatic way to handle errors.


📚 Prerequisites

Before we begin, ensure you have:

  • A reusable class-based ErrorBoundary component (as built in Articles 138-139).
  • Understanding of functional components and React Hooks (useState, useEffect).
  • Familiarity with the concept of Higher-Order Components (HOCs) can be helpful but is not strictly required.

🎯 Article Outline: What You'll Master

In this article, you will learn:

  • The Challenge: Why functional components can't be Error Boundaries directly.
  • Strategy 1: Wrapping Functional Components: Using your existing class-based ErrorBoundary as a wrapper.
  • Strategy 2: Introduction to react-error-boundary: Overview of this popular third-party library.
  • Basic Usage of react-error-boundary: The <ErrorBoundary> component and fallbackRender prop.
  • Benefits of using react-error-boundary: Simpler API for common use cases, built-in reset capabilities.

🧠 Section 1: The Challenge - Functional Components and Error Boundary Lifecycles

As a quick recap from Article 137 ("What are Error Boundaries?"):

  • Error Boundaries rely on two specific class lifecycle methods:
    • static getDerivedStateFromError(error): To update state and render a fallback UI when an error occurs in a child.
    • componentDidCatch(error, errorInfo): To log error information (a side effect).

Currently, React Hooks do not provide direct equivalents for these two lifecycle methods. This means a functional component cannot, by itself, act as an Error Boundary in the same way a class component can.

So, if your application is primarily built with functional components, you still need a class component somewhere to serve as the Error Boundary.


💻 Section 2: Strategy 1 - Wrapping Functional Components with Your Class ErrorBoundary

The most straightforward way to protect your functional components is to simply wrap them with the class-based ErrorBoundary component we built in the previous articles. This approach requires no new libraries and leverages the work we've already done.

Example: Let's assume we have our ErrorBoundary.jsx from Article 139:

// src/components/ErrorBoundary.jsx (Our class component)
// ... (implementation from Article 139 with constructor, getDerivedStateFromError, componentDidCatch, resetErrorBoundary, render) ...
// export default ErrorBoundary;

And a functional component that might throw an error:

// src/components/FunctionalWidget.jsx
import React, { useState } from 'react';

const FunctionalWidget = ({ title }) => {
const [count, setCount] = useState(0);
const [explode, setExplode] = useState(false);

if (explode) {
throw new Error(`Error deliberately thrown from ${title}!`);
}

return (
<div style={{ border: '1px solid blue', padding: '10px', margin: '10px' }}>
<h3>{title}</h3>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<button onClick={() => setExplode(true)} style={{ marginLeft: '10px', backgroundColor: 'orangered', color: 'white' }}>
Trigger Error
</button>
</div>
);
};

export default FunctionalWidget;

Now, we can use ErrorBoundary to wrap FunctionalWidget in our App.js:

// src/App.js
import React from 'react';
import ErrorBoundary from './components/ErrorBoundary'; // Our class component
import FunctionalWidget from './components/FunctionalWidget';
import CustomErrorFallback from './components/CustomErrorFallback'; // From Article 139

function App() {
const [widget1Key, setWidget1Key] = React.useState(1);
const [widget2Key, setWidget2Key] = React.useState(1);

return (
<div style={{ padding: '20px' }}>
<h1>Using Class ErrorBoundary with Functional Components</h1>

<ErrorBoundary
key={`eb1-${widget1Key}`} // Key for resettability
fallbackComponent={CustomErrorFallback}
onReset={() => setWidget1Key(k => k + 1)}
>
<FunctionalWidget title="Widget 1 (Protected)" />
</ErrorBoundary>

<ErrorBoundary
key={`eb2-${widget2Key}`} // Key for resettability
fallbackRender={({ error, resetErrorBoundary: resetError }) => ( // Using fallbackRender (if we adapted our EB or use react-error-boundary)
<div style={{color: 'red', border: '1px solid red', padding: '10px', margin: '10px'}}>
<p>Alternative Fallback for Widget 2:</p>
<p>{error.message}</p>
<button onClick={resetError}>Try Widget 2 Again</button>
</div>
)}
onReset={() => setWidget2Key(k => k + 1)}
>
{/* Our current ErrorBoundary uses fallbackComponent or a default.
To use fallbackRender like this, we'd need to modify our ErrorBoundary
or use react-error-boundary. For now, let's assume we pass a component.
*/}
<FunctionalWidget title="Widget 2 (Also Protected)" />
</ErrorBoundary>

<div>
<h2>Unprotected Widget (for comparison)</h2>
{/* <FunctionalWidget title="Widget 3 (Unprotected)" /> */}
{/* Uncommenting the above would crash the app if it errors */}
<p><em>Unprotected widget commented out to prevent app crash.</em></p>
</div>
</div>
);
}

export default App;

How it Works:

  • FunctionalWidget is a child of ErrorBoundary.
  • If FunctionalWidget throws an error (when its "Trigger Error" button is clicked), the ErrorBoundary instance wrapping it will catch the error.
  • ErrorBoundary's static getDerivedStateFromError and componentDidCatch will function as designed, and it will render its fallback UI (either the default, a fallback element, or a fallbackComponent).
  • The onReset prop and changing the key allow the FunctionalWidget to be reset and re-rendered.

Pros:

  • Simple and direct.
  • Leverages your existing ErrorBoundary component.
  • No need for additional libraries for this basic use case.

Cons:

  • You still need to maintain that class component (ErrorBoundary).
  • The error handling logic is somewhat "outside" your functional components. Some developers prefer solutions that feel more integrated with the Hooks paradigm.

This strategy is perfectly valid and often sufficient.


🛠️ Section 3: Introduction to react-error-boundary Library

For those who prefer a more Hooks-centric approach or want more features out-of-the-box, the react-error-boundary library by Brian Vaughn (from the React core team) is an excellent choice.

What it is: react-error-boundary is a small, focused library that provides a reusable <ErrorBoundary> component (yes, it's also a class component under the hood, because it has to be!) but with a more flexible API that feels very natural to use with functional components and Hooks.

Key Features/Benefits:

  • Flexible Fallback Rendering: Supports FallbackComponent prop (similar to our fallbackComponent), a fallbackRender render prop, and a simple fallback element prop.
  • Error and Reset Props: Automatically passes error and resetErrorBoundary function to your fallback component/render prop.
  • onReset Callback: Similar to our implementation, allows you to specify what happens when resetErrorBoundary is called.
  • resetKeys Prop: Allows you to specify an array of keys. If any of these keys change, the error boundary will automatically reset itself. This is very useful for scenarios where an error might be resolved if underlying data changes.
  • onError Callback: A prop function that is called with error and errorInfo (similar to componentDidCatch).

Installation:

npm install react-error-boundary
# or
yarn add react-error-boundary

🔬 Section 4: Basic Usage of react-error-boundary

Let's see how to use the <ErrorBoundary> component provided by this library.

// src/AppWithReactErrorBoundary.jsx
import React, { useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary'; // Import from the library
import FunctionalWidget from './components/FunctionalWidget'; // Our same problematic component

// Fallback component specifically for react-error-boundary
function ErrorFallbackUI({ error, resetErrorBoundary }) {
return (
<div role="alert" style={{ padding: '20px', border: '1px solid red', margin: '10px', backgroundColor: '#ffe0e0' }}>
<h3>Something went wrong (using react-error-boundary):</h3>
<pre style={{ color: 'red' }}>{error.message}</pre>
<button onClick={resetErrorBoundary} style={{ padding: '8px 15px' }}>
Try again
</button>
</div>
);
}

// Optional: A simple logger for the onError prop
const myErrorHandler = (error, info) => {
console.error("react-error-boundary caught an error:", error);
console.error("Component stack (from react-error-boundary):", info.componentStack);
// Send to Sentry, LogRocket, etc.
// myErrorTrackingService.log({ error, componentStack: info.componentStack });
};

function AppWithReactErrorBoundary() {
const [widgetKey, setWidgetKey] = useState(0); // Used with onReset

const handleReset = () => {
console.log('Resetting error boundary and widget state...');
setWidgetKey(prevKey => prevKey + 1);
};

return (
<div style={{ padding: '20px' }}>
<h1>Using `react-error-boundary` Library</h1>

<ErrorBoundary
FallbackComponent={ErrorFallbackUI}
onReset={handleReset} // Called when resetErrorBoundary (from ErrorFallbackUI) is invoked
resetKeys={[widgetKey]} // If widgetKey changes, boundary resets
onError={myErrorHandler} // For logging
>
<FunctionalWidget title={`Widget (key: ${widgetKey})`} />
</ErrorBoundary>

<button onClick={handleReset} style={{marginTop: '10px'}}>Force Reset Widget Externally</button>
</div>
);
}

export default AppWithReactErrorBoundary;

Explanation:

  1. import { ErrorBoundary } from 'react-error-boundary';: We import the component from the library.
  2. ErrorFallbackUI Component:
    • This functional component is designed to be used with react-error-boundary's FallbackComponent prop.
    • It automatically receives error (the error object) and resetErrorBoundary (a function to reset the boundary) as props.
  3. <ErrorBoundary ... /> Usage:
    • FallbackComponent={ErrorFallbackUI}: We provide our custom fallback UI.
    • onReset={handleReset}: When resetErrorBoundary is called from within ErrorFallbackUI, our handleReset function will also be executed. Here, we change widgetKey.
    • resetKeys={[widgetKey]}: This is a powerful feature. If any value in the resetKeys array changes, the ErrorBoundary will automatically reset its error state. So, when handleReset changes widgetKey, the boundary resets. This is often simpler than manually passing a key prop to the ErrorBoundary itself for reset purposes.
    • onError={myErrorHandler}: This prop takes a function that will be called with error and info (containing componentStack), similar to componentDidCatch. It's the ideal place for logging.
  4. FunctionalWidget: Our same widget is used. We pass the widgetKey in its title just for demonstration, but the actual reset mechanism for FunctionalWidget (if it has internal state causing the error) would still rely on it being remounted (due to ErrorBoundary resetting and its children re-rendering, or if FunctionalWidget itself had a key prop tied to widgetKey).

Behavior:

  • When FunctionalWidget throws an error, react-error-boundary catches it.
  • ErrorFallbackUI is rendered, displaying the error message and a "Try again" button.
  • myErrorHandler is called, logging the error.
  • Clicking "Try again" calls resetErrorBoundary (passed by the library), which in turn calls our onReset prop (handleReset).
  • handleReset changes widgetKey. Because widgetKey is in resetKeys, the ErrorBoundary automatically resets its error state.
  • FunctionalWidget re-renders (potentially remounts if its own key was tied to widgetKey or if the error boundary's reset forces a full child re-render).

✨ Section 5: Benefits of react-error-boundary

While you can use your own class-based ErrorBoundary, react-error-boundary offers several advantages, especially for projects heavily using functional components:

  • More Idiomatic API for Hooks: Props like FallbackComponent, fallbackRender, resetKeys, and onError feel very natural when working in a Hooks-based codebase.
  • Simplified Reset Logic: The resetKeys prop provides a declarative way to reset the boundary when relevant data changes, which can be cleaner than manually managing key props on the boundary or its children for reset purposes.
  • Well-Tested and Maintained: Being a popular library, it's well-tested and kept up-to-date by the community and a React team member.
  • Reduces Boilerplate: You don't need to write and maintain the class component logic yourself if the library meets your needs.
  • Clear Separation of Concerns: The library handles the "boundary" mechanics, letting you focus on your fallback UI and logging logic.

For most new projects or when refactoring, react-error-boundary is often the recommended approach for integrating error boundary functionality in a Hooks-first world.


💡 Conclusion & Key Takeaways (Part 1)

Functional components cannot be Error Boundaries themselves, but they can be effectively protected. You can wrap them with your own class-based ErrorBoundary or leverage a library like react-error-boundary for a more Hooks-friendly API. The react-error-boundary library simplifies providing fallbacks, handling resets, and logging errors, making it a strong contender for most projects.

Key Takeaways So Far:

  • Functional components need a class-based Error Boundary parent to be protected.
  • You can use your custom class ErrorBoundary to wrap functional components.
  • react-error-boundary provides an <ErrorBoundary> component with a convenient API (FallbackComponent, onReset, resetKeys, onError) that integrates well with functional components.

➡️ Next Steps

In "Using Error Boundaries with Hooks (Part 2)", we will explore:

  • Using the useErrorHandler hook provided by react-error-boundary to programmatically trigger an error boundary from within a functional component (e.g., after a failed async operation).
  • More advanced patterns and recipes for using react-error-boundary.
  • Comparing different approaches and when to choose which.

Stay tuned to further enhance your error handling strategies in React!


glossary

  • react-error-boundary: A popular third-party React library that provides a flexible and Hooks-friendly Error Boundary component and related utilities.
  • FallbackComponent (prop): A prop used by react-error-boundary (and our enhanced class component) to specify a React component to render as the fallback UI. It receives error and resetErrorBoundary as props.
  • fallbackRender (prop): A prop used by react-error-boundary that accepts a function. This function receives error and resetErrorBoundary and should return a React element for the fallback UI.
  • resetKeys (prop): A prop used by react-error-boundary that accepts an array of values. If any of these values change, the error boundary automatically resets.
  • onError (prop): A prop used by react-error-boundary that accepts a callback function, invoked with error and info (containing componentStack) when an error is caught, suitable for logging.

Further Reading