Efficient way to render components based on API call stage

Introduction

A typical approach to loading a page is to render the entire page with a loader component as the fallback of the Suspense and then populate the page data on success.

The problem with the above method is that when a call to the API failed for some reason, there would be a toast or something similar to indicate that some or all the API calls failed. This leaves the component populated by the API call distorted.

An attempt to fix the problem above would be to reload the entire page thereby making a call to all endpoints (for that page) including the successful calls.

In this article I would discuss and demonstrate a more simple and user-friendly approach to rendering data from API, making them independent by separating concerns.

More like the idea of microservices. when a service or two is down, it doesn't stop the app from being functional, just the broken parts become inaccessible.

Objectives

At the end of this article, you should be able to:

  • Separate components based on concern.
  • handle fallback for each component based on the API call stage.

Prerequisites

You should be familiar with the following:

  • JavaScript
  • React
  • Axios
  • Tailwindcss

You should also be familiar with making API calls using Axios because I would call three different endpoints from the jsonplaceholder API.

If you aren't familiar with making API calls using Axios, you could read up here.

Let's get started

Before I proceed, I will like to give a brief description about the term separation of concern. In simple terms, it simply means making aspects or sections of code (components in our case), secluded and independent of each other.

I am going to have 3 different files. Basically User.js, Post.js, and TodoList.js. All 3 files will be imported in an index.js file which serves as their parent.

The content of the 3 files is an Axios call to an endpoint and a switch statement within a function that checks the stage of the call which is then rendered.

Add the following code snippet in the User.js file:

import { useState, useEffect } from "react";
import axios from "axios";

const User = () => {
  const [user, setUser] = useState({});
  const [stage, setStage] = useState("");

  const getUser = async () => {
    try {
      setStage("loading");
      const { data } = await axios.get(
        "https://jsonplaceholder.typicode.com/users/2"
      );

      setUser(data);
      setStage("done");
    } catch (error) {
      setStage("error");
      console.log({ error });
    }
  };

  useEffect(() => {
    getUser();
  }, []);

  const renderBasedOnCallStage = () => {
    switch (stage) {
      case "loading":
        return (
          <div className="h-44 font-bold flex justify-center items-center">
            loading...
          </div>
        );
      case "done":
        return (
          <>
            <div className="flex items-center gap-2 py-2">
              <span className="text-sm font-medium">Name: </span>
              <p className="">{user?.name}</p>
            </div>
            <div className="flex items-center gap-2 py-2">
              <span className="text-sm font-medium">Email: </span>
              <p className="">{user?.email}</p>
            </div>
            <div className="flex items-center gap-2 py-2">
              <span className="text-sm font-medium">User Name: </span>
              <p className="">{user?.username}</p>
            </div>
            <div className="flex items-center gap-2 py-2">
              <span className="text-sm font-medium">City: </span>
              <p className="">{user?.address?.city}</p>
            </div>
            <div className="flex items-center gap-2 py-2">
              <span className="text-sm font-medium">Website: </span>
              <a className="" href={user?.website}>
                {user?.website}
              </a>
            </div>
          </>
        );
      case "error":
        return (
          <div className="h-44 font-bold flex justify-center items-center text-red-500">
            An error occurred.{" "}
            <span className="cursor-pointer" onClick={() => getUser()}>
              Try again
            </span>
          </div>
        );
      default:
        return "";
    }
  };

  return (
    <div>
      <h2 className="font-bold mb-1">User</h2>
      <div className="border p-3">{renderBasedOnCallStage()}</div>
    </div>
  );
};

export default User;

Add the following code snippet in the Post.js file:

import { useState, useEffect } from "react";
import axios from "axios";

const Post = () => {
  const [post, setPost] = useState({});
  const [stage, setStage] = useState("");

  const getPost = async () => {
    try {
      setStage("loading");
      const { data } = await axios.get(
        "https://jsonplaceholder.typicode.com/posts/11"
      );

      setPost(data);
      setStage("done");
    } catch (error) {
      setStage("error");
      console.log({ error });
    }
  };

  useEffect(() => {
    getPost();
  }, []);

  const renderBasedOnCallStage = () => {
    switch (stage) {
      case "loading":
        return (
          <div className="h-44 font-bold flex justify-center items-center">
            loading...
          </div>
        );
      case "done":
        return (
          <div>
            <h4 className="text-lg font-semibold border-b-4 w-9/12">
              {post?.title}
            </h4>
            <article className="text-md">{post?.body}</article>
          </div>
        );
      case "error":
        return (
          <div className="h-44 font-bold flex justify-center items-center text-red-500">
            An error occurred.{" "}
            <span className="cursor-pointer" onClick={() => getPost()}>
              Try again
            </span>
          </div>
        );
      default:
        return "";
    }
  };

  return (
    <div>
      <h2 className="font-bold mb-1">Post</h2>
      <div className="border p-3">{renderBasedOnCallStage()}</div>
    </div>
  );
};

export default Post;

Add the following code snippet in the TodoList.js file:

import { useState, useEffect } from "react";
import axios from "axios";

const TodoList = () => {
  const [todoList, setTodoList] = useState([]);
  const [stage, setStage] = useState("");

  const getTodoList = async () => {
    try {
      setStage("loading");
      const { data } = await axios.get(
        "https://jsonplaceholder.typicode.com/todos?userId=2"
      );

      setTodoList(data);
      setStage("done");
    } catch (error) {
      setStage("error");
      console.log({ error });
    }
  };

  useEffect(() => {
    getTodoList();
  }, []);

  const renderBasedOnCallStage = () => {
    switch (stage) {
      case "loading":
        return (
          <div className="h-screen font-bold flex justify-center items-center">
            loading...
          </div>
        );
      case "done":
        return (
          <div className="flex flex-wrap gap-2">
            {todoList?.map((todo, i) => (
              <div
                className="flex align-center justify-between border-2 rounded gap-2 p-2"
                key={i}
              >
                <p>{todo?.title}</p>
                <span
                  className={`${
                    todo?.completed ? `text-green-400` : `text-yellow-400`
                  }`}
                >
                  {todo?.completed ? "Completed" : "Pending"}
                </span>
              </div>
            ))}
          </div>
        );
      case "error":
        return (
          <div className="h-screen font-bold flex justify-center items-center text-red-500">
            An error occurred.{" "}
            <span className="cursor-pointer" onClick={() => getTodoList()}>
              Try again
            </span>
          </div>
        );
      default:
        return "";
    }
  };

  return (
    <div>
      <h2 className="font-bold mb-1">TodoList</h2>
      <div className="h- border p-3">{renderBasedOnCallStage()}</div>
    </div>
  );
};

export default TodoList;

Note: The headers (h2) of each component are not wrapped inside the renderBasedOnStage function. That's because it's a simple text that is readily available on page load.

You might ask, aren't the labels for the user profile also available on page load?. Yes, they are, but remember, we don't want our page to appear distorted in case of an error.

As stated earlier, the purpose of the index.js is to bring in the 3 other components, serving as their parent.

Add the following code snippet in the index.js file:

import User from "./User";
import Post from "./Post";
import TodoList from "./TodoList";

const Page = () => {
  return (
    <div className="flex gap-3 flex-col md:grid md:grid-cols-2 m-3">
      <div className="flex flex-col gap-3">
        <User />
        <Post />
      </div>
      <TodoList />
    </div>
  );
};

export default Page;

Looking at the UI in the browser, the loading stage appears like this: loading state.png

With the data still in the loading stage, the user has an overview of what the page is about. Some information about a user, a post, and a to-do list.

At this point, the call to the user profile is still loading, the to-do list fetched successfully, and the post endpoint wasn't successful, as seen below:

3 possible stage.png

With the currently rendered component, the user can interact with the to-do list, that could be their purpose for coming to the page.

Waiting for the user profile to be successful, I could easily click on the Try again text for Post to refetch the data. This action is going to set the Post stage back to loading and attempts to fetch the data. The user profile and to-do list are not affected by this and I wouldn't need to reload the entire page.

At this point, all data are available to the user:

All.png

Conclusion

In this article, we were able to:

  • demonstrate a simpler and user-friendly approach to rendering data on API calls.
  • Separate components based on concern.
  • handle fallback for each component based on the API call stage.

Sure there are better ways to instantiate Axios and seprarate components based on concerns, but the article aims to show how to render based on the API call stage.

I hope you learned from this. Let me know what you think in the comments.