How to Handle Loading State in Astro.js When Data Fetching

2024-06-09 / 3 min read
Last updated: 2024-06-09

Blog


Fetching data from an API and displaying a loading state while the data is being retrieved is a common task in modern JavaScript frameworks. However, I encountered an issue when I started using Astro.js, despite my three years of experience with frameworks.

The Issue

In the example below, I make a basic call to my WordPress site to fetch the latest blog post. However, as shown in the GIF, the component remains absent until the data is fetched, then it suddenly appears with the data.

A Component fetching API and magically appearing after succeeding

---
import Tile from '../base/Tile.astro';
import Heading from '../base/Heading.astro';

const content = async() => {
    const resPost = await fetch("https://luke.iremadze.com/wp-json/wp/v2/posts");
    const posts = await resPost.json();
    const resPostPhoto = await fetch(`https://luke.iremadze.com/wp-json/wp/v2/media/${posts[0].featured_media}`);
    const postPhoto = await resPostPhoto.json();

    return {posts, postPhoto};
}

const photoClasses = [...]

const {posts, postPhoto} = await content();
---

<Tile
    href={posts[0].guid.rendered}
    size='small'
>
    <Heading>
        Latest Blog Post
    </Heading>
    <div class="relative">
        <Heading headingLevel='h3' customClasses={["absolute", "bg-primary", "dark:bg-primaryDark", "text-white", "dark:text-primary", "rounded-t-2xl", "px-4", "w-full", "z-50", "whitespace-nowrap", "overflow-hidden", "text-ellipsis"]}>
            {posts[0].title.rendered.toUpperCase()}
        </Heading>
        <Fragment set:html={postPhoto} />
    </div>
</Tile>

My second attempt involved adding a loading state to Astro.js, similar to how it's done in frameworks like React and Vue.

{loading ? <Loading>Loading</Loading> : 
<Tile
    href={posts[0].guid.rendered}
    size='small'
>
    <Heading>
        Latest Blog Post
    </Heading>
    <div class="relative">
        <Heading headingLevel='h3' customClasses={["absolute", "bg-primary", "dark:bg-primaryDark", "text-white", "dark:text-primary", "rounded-t-2xl", "px-4", "w-full", "z-50", "whitespace-nowrap", "overflow-hidden", "text-ellipsis"]}>
            {posts[0].title.rendered.toUpperCase()}
        </Heading>
        <Fragment set:html={postPhotoHtml} />
    </div>
</Tile>
}

This approach didn't work either. Since Astro.js is server-side generated, it won't render anything until all the steps are completed, leaving no opportunity to display a loading state.

The Solution

A straightforward solution is to use a client-side framework to handle state changes, such as React:

return loading ? (
    <p>Loading!</p>
  ) : (
    <div>
      <h2>Latest Blog Post</h2>
      <h3>{posts[0]?.title.rendered}</h3>
      <a href={posts[0]?.guid.rendered}>Read more</a>
    </div>
  );

However, this would require rewriting my Astro components, which I want to avoid. Ideally, I could import an Astro component into a React one, but this isn't possible.

A Workaround

A workaround is to pass Astro components as children to the React component:

...
    <LatestPost client:load>
      <Loading size="small" slot='loading' />
    </LatestPost>
...
// Calling the React component in my Astro file

Then render it in the React component as a child:

const LatestPost = (props) => {
...
return loading ? (
    props.loading
  ) : (
    <div>
      <h2>Latest Blog Post</h2>
      <h3>{posts[0]?.title.rendered}</h3>
      <a href={posts[0]?.guid.rendered}>Read more</a>
    </div>
  );
...

This approach almost achieves what I want, displaying the loading state and rendering the content. However, handling the Tile component remains an issue. I don't want to maintain two versions of the Tile component, one for Astro and another for React.

Component now showing the loading and rendering the content

The Compromise

My compromise was to recreate a React JSX component. To balance reusability and maintainability, I created a separate TypeScript file containing common attributes for the components. This way, any adjustments can be made in the TypeScript file, reducing duplication.

While this isn't an ideal solution, it's a step towards improving the handling of loading states in Astro.js. I'll continue to explore better solutions and update this page if I find one.