Home > Software design >  With React 18 and suspense, how to display specific errors, not just fallback in ErrorBoundary
With React 18 and suspense, how to display specific errors, not just fallback in ErrorBoundary

Time:01-11

I have a component structured like this with my ErrorBoundary wrapping my Suspense element.

function App() {
  return (
    <ErrorBoundary fallback={<h2>Could not fetch cities.</h2>}>
      <Suspense fallback={<div>Loading..</div>}>
        <MyList />
      </Suspense>
    </ErrorBoundary>
  );
}

The MyList component includes an SWR data fetching hook as follows:

const { data } = useSwr(`/api/mydata`, fetcher, {
      suspense: true,
    });

My fetcher method throws an error as follows:

  const rsp = await fetch(url);
  if (rsp.ok) {
    return await rsp.json();
  } else {
    const MyError = function (message, status) {
      this.message = `${message} from url ${url} status code:${status}`;
      this.status = status;
    };
    throw new MyError(rsp.statusText, rsp.status);
  }
}

When the error happens, I don't know how to have my UI show the values thrown (that is, what is in the MyError class)

CodePudding user response:

I'm not sure if there's some library you're using with a component named ErrorBoundary, but the way you would write your own to do this is something like the following:

class MyErrorBoundary extends React.Component {
  state = { error: null }

  static getDerivedStateFromError(error) {
    return { error };
  }

  render() {
    if (this.state.error) {
      // render whatever you like for the error case
      return <h2>{this.state.error.message}</h2>
    } else {
      return this.props.children
    }
  }
}

CodePudding user response:

You should use this lifecycle in your ErrorBoundary component:

https://reactjs.org/docs/react-component.html#componentdidcatch

So something like (adapted from the documentation, to explain how to trigger the error in a way that is intercepted by ErrorBoundary):

// In ErrorBoundary

componentDidCatch(error, errorInfo) {
  this.setState({
    error: error,
    errorInfo: errorInfo
  });
}

// In MyList

buggyMethod() {
  fetch("something-wrong").then(error => this.setState({ error }));
}

render() {
 if(this.state.error){
   throw this.state.error;
 }
 return <span>Something cool!</span>;
}

Note

Seems little bit wired, but is the same technique used in the official documentation, "Live Demo" section:

https://reactjs.org/docs/error-boundaries.html#live-demo

CodePudding user response:

According to the docs, you have access to error and errorInfo in componentDidCatch. You can set it to state of the ErrorBoundary. What you can do is use a third-party library (react-json-tree) to view the error nicely.

import JSONTree from 'react-json-tree';

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null, errorInfo: null };
  }

  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // You can also log the error to an error reporting service
    this.setState({ error, errorInfo });
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <JSONTree data={this.state.error}/>;
    }

    return this.props.children; 
  }
}

CodePudding user response:

Here is the answer I was looking for:

fetcher.js

export async function fetcher(url) {
  const rsp = await fetch(url);
  if (rsp.ok) {
    return await rsp.json();
  } else {
    const MyError = function (message, status) {
      this.message = `${message} from url ${url} status code:${status}`;
      this.status = status;
    };
    throw new MyError(rsp.statusText, rsp.status);
  }
}

ErrorBoundary.js

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

  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return { hasError: true, message: error?.message, status: error?.status };
  }

  render() {
    function addExtraProps(Component, extraProps) {
      return <Component.type {...Component.props} {...extraProps} />;
    }

    if (this.state.hasError) {
      return addExtraProps(this.props.fallback, {
        errorMessage: this.state.message,
        errorStatus: this.state.status,
      });
    }
    return this.props.children;
  }
}

And then the usage is something like this:

function CityLayout(props) {
  const { setSelectedCityId } = useContext(CityContext);
  return (
    <>
      <CityListMaxDDL />
      <CityList displayCount={5} />
      <CityDetail cityId={setSelectedCityId} />
    </>
  );
}

function App() {
  function MyErrorBoundaryFallback({ errorMessage, errorStatus }) {
    return (
      <div className="container">
        <h1>Error</h1>
        <div className="row">
          Error Status: <b>{errorStatus}</b>
        </div>
        <div className="row">
          ErrorMessage: <b>{errorMessage}</b>
        </div>
      </div>
    );
  }

  return (
    <ErrorBoundary fallback={<MyErrorBoundaryFallback />}>
      <Suspense fallback={<div>Loading..</div>}>
        <div className="container">
          <CityProvider>
            <CityLayout />
          </CityProvider>
        </div>
      </Suspense>
    </ErrorBoundary>
  );
  •  Tags:  
  • Related