DEV Community

Abhinav Singh
Abhinav Singh

Posted on • Originally published at imabhinav.dev

React's Dirty Little Secret: Error Boundaries in Functional Components

Errors are inevitable in any application, and handling them gracefully is crucial for providing a seamless user experience. In React, error boundaries are a robust mechanism for capturing and managing errors in a component tree. Traditionally, error boundaries were implemented using class components, but with the advent of React hooks, it's now possible to create error boundaries in functional components as well. This blog will guide you through the concepts of error boundaries in React functional components, using easy-to-understand examples and storytelling to make the topic approachable.

What Are Error Boundaries?

The Concept

Imagine you're building a house. You'd want to ensure that any structural issues in one part of the house don't cause the entire building to collapse. Similarly, in a React application, an error in one part of the UI should not bring down the entire application. Error boundaries act as protective barriers that catch errors in their child components and display fallback UI instead of crashing the whole application.

Traditional Class Component Error Boundaries

Before diving into functional components, let's briefly revisit how error boundaries are traditionally implemented using class components:

import React, { Component } from 'react';

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    console.log(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children; 
  }
}

export default ErrorBoundary;
Enter fullscreen mode Exit fullscreen mode

The Story of Error Boundaries

Meet Jane, the Frontend Developer

Jane is a talented frontend developer working on a complex React application. One day, her team faces a challenge: the app occasionally crashes due to errors in various components. Jane needs a solution to handle these errors gracefully and ensure a smooth user experience. She remembers hearing about error boundaries but wants to implement them in functional components since her team prefers hooks over class components.

Error Boundaries in Functional Components

The Challenge

Implementing error boundaries in functional components isn't straightforward because React hooks don't support the same lifecycle methods available in class components. However, with some creativity and the use of custom hooks, Jane can achieve the same functionality.

Creating a Custom Hook for Error Boundaries

Jane decides to create a custom hook, useErrorBoundary, to manage error boundaries in functional components. This hook will leverage React's context API to provide error boundary functionality.

Here's how Jane implements it:

import React, { createContext, useContext, useState, useEffect } from 'react';

const ErrorBoundaryContext = createContext();

export const useErrorBoundary = () => {
  return useContext(ErrorBoundaryContext);
};

export const ErrorBoundaryProvider = ({ children }) => {
  const [hasError, setHasError] = useState(false);
  const [error, setError] = useState(null);

  const value = {
    hasError,
    setHasError,
    error,
    setError,
  };

  return (
    <ErrorBoundaryContext.Provider value={value}>
      {children}
    </ErrorBoundaryContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

Implementing Error Boundaries in Functional Components

Step-by-Step Guide

  1. Set Up the Context:

Jane sets up a context to manage the error state and provides functions to update this state. This context will be used by components to access error boundary functionality.

   import React, { createContext, useContext, useState } from 'react';

   const ErrorBoundaryContext = createContext();

   export const useErrorBoundary = () => {
     return useContext(ErrorBoundaryContext);
   };

   export const ErrorBoundaryProvider = ({ children }) => {
     const [hasError, setHasError] = useState(false);
     const [error, setError] = useState(null);

     const value = {
       hasError,
       setHasError,
       error,
       setError,
     };

     return (
       <ErrorBoundaryContext.Provider value={value}>
         {children}
       </ErrorBoundaryContext.Provider>
     );
   };
Enter fullscreen mode Exit fullscreen mode
  1. Create a Component to Handle Errors:

Jane creates a component, ErrorBoundary, that uses the custom hook to manage errors and display a fallback UI when an error occurs.

   import React from 'react';
   import { useErrorBoundary } from './ErrorBoundaryContext';

   const ErrorBoundary = ({ children, fallback }) => {
     const { hasError, error } = useErrorBoundary();

     if (hasError) {
       return fallback ? fallback : <h1>Something went wrong.</h1>;
     }

     return children;
   };

   export default ErrorBoundary;
Enter fullscreen mode Exit fullscreen mode
  1. Wrap the Application with the Error Boundary Provider:

Jane wraps her application with the ErrorBoundaryProvider to provide error boundary functionality to the entire component tree.

   import React from 'react';
   import ReactDOM from 'react-dom';
   import App from './App';
   import { ErrorBoundaryProvider } from './ErrorBoundaryContext';

   ReactDOM.render(
     <ErrorBoundaryProvider>
       <App />
     </ErrorBoundaryProvider>,
     document.getElementById('root')
   );
Enter fullscreen mode Exit fullscreen mode
  1. Use the Error Boundary in a Component:

Jane uses the ErrorBoundary component to wrap any part of the UI that might throw an error. She also provides a custom fallback UI.

   import React from 'react';
   import ErrorBoundary from './ErrorBoundary';

   const ProblematicComponent = () => {
     throw new Error('Oops!');

     return <div>This will never be displayed.</div>;
   };

   const App = () => {
     return (
       <div>
         <h1>My Application</h1>
         <ErrorBoundary fallback={<div>Custom Error Message</div>}>
           <ProblematicComponent />
         </ErrorBoundary>
       </div>
     );
   };

   export default App;
Enter fullscreen mode Exit fullscreen mode

Advanced Error Handling

Logging Errors

Jane realizes that simply displaying a fallback UI isn't enough. She needs to log errors for further analysis. She modifies the useErrorBoundary hook to include error logging.

import React, { useEffect } from 'react';
import { useErrorBoundary } from './ErrorBoundaryContext';

const ErrorLogger = () => {
  const { error } = useErrorBoundary();

  useEffect(() => {
    if (error) {
      // Log error to an external service
      console.error('Logging error:', error);
    }
  }, [error]);

  return null;
};

export default ErrorLogger;
Enter fullscreen mode Exit fullscreen mode

Jane then includes the ErrorLogger component in her application:

import React from 'react';
import ErrorBoundary from './ErrorBoundary';
import ErrorLogger from './ErrorLogger';

const ProblematicComponent = () => {
  throw new Error('Oops!');

  return <div>This will never be displayed.</div>;
};

const App = () => {
  return (
    <div>
      <h1>My Application</h1>
      <ErrorLogger />
      <ErrorBoundary fallback={<div>Custom Error Message</div>}>
        <ProblematicComponent />
      </ErrorBoundary>
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Resetting Error Boundaries

Sometimes, Jane needs to reset the error boundary after an error occurs. She adds a reset function to the context to allow components to reset the error state.

import React from 'react';
import { useErrorBoundary } from './ErrorBoundaryContext';

const ErrorBoundary = ({ children, fallback }) => {
  const { hasError, error, setHasError, setError } = useErrorBoundary();

  const resetErrorBoundary = () => {
    setHasError(false);
    setError(null);
  };

  if (hasError) {
    return (
      <div>
        {fallback ? fallback : <h1>Something went wrong.</h1>}
        <button onClick={resetErrorBoundary}>Try Again</button>
      </div>
    );
  }

  return children;
};

export default ErrorBoundary;
Enter fullscreen mode Exit fullscreen mode

Real-World Example

The Shopping Cart Application

To solidify her understanding, Jane decides to implement error boundaries in a real-world scenario: a shopping cart application. She sets up a simple app with components that might throw errors and uses error boundaries to handle them.

import React, { useState } from 'react';
import ErrorBoundary from './ErrorBoundary';

const ProductList = () => {
  const [products] = useState(['Apple', 'Banana', 'Cherry']);
  const [selectedProduct, setSelectedProduct] = useState(null);

  const handleSelectProduct = (product) => {
    if (product === 'Banana') {
      throw new Error('Banana is out of stock!');
    }
    setSelectedProduct(product);
  };

  return (
    <div>
      <h2>Product List</h2>
      <ul>
        {products.map((product) => (
          <li key={product} onClick={() => handleSelectProduct(product)}>
            {product}
          </li>
        ))}
      </ul>
      {selectedProduct && <div>Selected Product: {selectedProduct}</div>}
    </div>
  );
};

const App = () => {
  return (
    <div>
      <h1>Shopping Cart</h1>
      <ErrorBoundary fallback={<div>Sorry, something went wrong!</div>}>
        <ProductList />
      </ErrorBoundary>
    </div>
  );
};

export default App;


Enter fullscreen mode Exit fullscreen mode

Conclusion

Jane successfully implemented error boundaries in functional components using custom hooks and context. She learned how to display fallback UI, log errors, and reset error boundaries. Her shopping cart application now handles errors gracefully, providing a better user experience.

By understanding and implementing error boundaries, you too can build resilient React applications that handle errors gracefully and maintain a smooth user experience. Happy coding!

Top comments (0)