DEV Community

roggc
roggc

Posted on

How to fetch data in React with Suspense and ErrorBoundary

When we want to fetch data, we are dealing with a promise, so we have three possible scenarios:

  • the promise resolves and it's a success

  • the promise resolves and it's a failure (dealt by ErrorBoundary component)

  • the promise is in a pending state (dealt by Suspense component)

So this is how our code will look at a high level:

function App() {
  return (
    <>
    <ErrorBoundary>
      <Suspense fallback={<h1>Loading blog...</h1>}>
        <BlogPost resource={resource1} />
        <Share />
      </Suspense>
      </ErrorBoundary>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

BlogPost it's a component which reads on the state of a promise:

function BlogPost({resource:{read}}) {
  const {title,body} = read();
  // ...
Enter fullscreen mode Exit fullscreen mode

where resource is an object created by function createResource:

const resource1=createResource(fetchPosts(0))
Enter fullscreen mode Exit fullscreen mode

Let's look at the definition of createResource:

function createResource(promise){
    let error,response
    promise.then(r=>response=r).catch(e=>(error=e))
    return {
        read(){
            if(error)throw error
            if(response) return response
            throw promise
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

As you see the read function can throw a promise, an error or return a successful response. It's like if the promise would be catch by the Suspense component and renders the fallback prop. The error would be catch by the ErrorBoundary component, which we are going to see in a sec, and will render an ErrorScreen component. When the promised is resolved successfully, the children of Suspense will be rendered.

This is the ErrorBoundary component:

import {Component} from 'react'

function ErrorScreen({error:{message}}){
    return <div>
        Something went wrong: {message}
        </div>
}

class ErrorBoundary extends Component{
    state={error:null}
    static getDerivedStateFromError(e){
        return {error:e}
    }
    render(){
        const {children,fallback}=this.props
        const {error}=this.state
        if(error&&!fallback)return <ErrorScreen error={error} />
        if(error)return <fallback error={error} />
        return children
    }
}

export default ErrorBoundary
Enter fullscreen mode Exit fullscreen mode

As you see it works similarly to the Suspense one, accepting a fallback property, which in this case defaults to the render of ErrorScreen component.

Let's see at the definition of fetchPosts() function:


  let blogPosts = [
    {
      id: 1,
      title: 'qui est esse',
      body: 'est rerum tempore vitae\nsequi sint nihil reprehenderit'
    },
  ];

  export function fetchPosts(id) {
    let post = blogPosts[id];
    return new Promise((resolve) => {
      setTimeout(() => {
        console.log("fetched blogs");
        resolve(post);
      }, 2000);
    });
  }
Enter fullscreen mode Exit fullscreen mode

As we can see, it returns a promise.

For completion, let's see at the definition of BlogPost and Share components:

function Share() {
  useEffect(() => {
    console.log("Effect Share");

    return () => {
      console.log("Cleanup Share");
    };
  });

  console.log("Render Share");
  return (
    <div>Share:&nbsp;
      <span> twitter</span>
      <span> reddit</span>
    </div>
  )
}

function BlogPost({resource:{read}}) {
  const {title,body} = read();

  useEffect(() => {
    console.log("Effect BlogPost");
    return () => {
      console.log("Cleanup BlogPost");
    };
  });

  return (
    <div>
      <h1>Blog Post</h1>
      <h3>{title}</h3>
      <span>{body}</span>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

And this is the output on the console when executing the app:

Image description

As you can see, with react 18 the Share component doesn't render until the BlogPost is ready, because they all belong to the children of the Suspense component. If we used react 17 instead, this would be the output in the console:

Image description

As you can see, it renders first the Share component before resolving the promise, and then, after the promise has been resolved successfully, it renders the BlogPost component.

Top comments (0)