1

I have a React app where I need to fetch project details from an API using a list of project IDs. I have a JSON file (projectIds.json) containing approximately 950 project IDs. The app fetches project details for all the project IDs at once, but this approach causes the API to become super slow and the app keeps loading forever.

I would like to optimize the fetching process and improve the performance of my React app. How can I modify the code below to achieve this?

import React, { useEffect, useState } from "react";
import axios from "axios";
import { Card, Container, Col, Button, Spinner } from "react-bootstrap";
import { BASE_URL, TOKEN, extractImagesFromYaml } from "./utils";
import projectIds from "./projectIds.json";
import { Project } from "./type";
import { useApiCache } from "./ApiContext";

const ProjectsPage: React.FC = () => {
  const { updateApiCache, getFromApiCache } = useApiCache();
  const [projects, setProjects] = useState<Project[]>([]);
  const [loading, setLoading] = useState<boolean>(true);

  const [error, setError] = useState<string | null>(null);
  const [visibleImages, setVisibleImages] = useState(2);
  const [searchTerm, setSearchTerm] = useState("");

  const [isSearchMode, setIsSearchMode] = useState<boolean>(false);

  const handleLoadMore = () => {
    setVisibleImages((prevVisibleImages) => prevVisibleImages + 3);
  };

  const handleGitLabButtonClick = (gitLabUrl: string) => {
    window.open(gitLabUrl, "_blank");
  };

  const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value.toLowerCase();
    setSearchTerm(value);
    setIsSearchMode(value !== "");
  };

  const filteredProjects = projects.filter((project) => {
    const searchTermLower = searchTerm.toLowerCase();

    // Check if projectName or any image includes the search term
    const projectNameMatch = project.projectName
      .toLowerCase()
      .includes(searchTermLower);
    const imageMatch = project.images.some((imageObj) =>
      imageObj.image.toLowerCase().includes(searchTermLower)
    );

    // Return true if either projectName or any image matches the search term
    return project.images.length > 0 && (projectNameMatch || imageMatch);
  });

  useEffect(() => {
    const fetchProjects = async () => {
      setLoading(true);
      setError(null);

      try {
        const cachedData = getFromApiCache("projects");
        if (cachedData) {
          setProjects(cachedData);
          setLoading(false);
          return;
        }

        const projectPromises = projectIds.map(async (project) => {
          try {
            const projectResponse = await axios.get(
              `${BASE_URL}/projects/${project.projectIds}`,
              {
                headers: {
                  Authorization: `Bearer ${TOKEN}`,
                },
              }
            );

            const projectData = projectResponse.data;
            const gitlabUrl = projectData.web_url;

            const ciLintResponse = await axios.get(
              `${BASE_URL}/projects/${project.projectIds}/ci/lint`,
              {
                headers: {
                  Authorization: `Bearer ${TOKEN}`,
                },
              }
            );

            const ciLintData = ciLintResponse.data;
            const imageArray = extractImagesFromYaml(ciLintData.merged_yaml);

            const newProject: Project = {
              projectId: project.projectIds,
              projectName: projectData.name,
              gitlabUrl: gitlabUrl,
              images: imageArray,
            };

            return newProject;
          } catch (error) {
            console.error(
              `Error fetching project data for project ID ${project.projectIds}`,
              error
            );
            return null;
          }
        });

        const results = await Promise.all(projectPromises);
        const validResults = results.filter(
          (result): result is Project => result !== null
        );
        setProjects(validResults);
        updateApiCache("projects", validResults);
        setLoading(false);
      } catch (error) {
        setError("An error occurred while fetching projects.");
        setLoading(false);
      }
    };

    fetchProjects();
  }, []);

  if (loading) {
    return (
      <div
        className="d-flex justify-content-center align-items-center"
        style={{ minHeight: "100vh" }}
      >
        <h3>Loading...</h3>
        <Spinner animation="border" variant="primary" />
      </div>
    );
  }

  if (error) {
    return (
      <div
        className="d-flex justify-content-center align-items-center"
        style={{ minHeight: "100vh" }}
      >
        Error: {error}
      </div>
    );
  }

  function highlightSearchTerm(text: string, searchTerm: string) {
    if (!searchTerm) {
      return text;
    }

    const regex = new RegExp(`(${searchTerm})`, "gi");
    const parts = text.split(regex);

    return parts.map((part, index) => {
      if (part.toLowerCase() === searchTerm.toLowerCase()) {
        return (
          <span key={index} style={{ backgroundColor: "red" }}>
            {part}
          </span>
        );
      }
      return part;
    });
  }

  return (
    <Container>
      <div className="d-flex justify-content-between align-items-center mb-2">
        <h1>Projects</h1>
        <div className="search-input">
          <input
            type="text"
            className="form-control"
            placeholder="Search by project name or image"
            onChange={handleSearch}
          />
        </div>
      </div>
      {filteredProjects.length === 0 && isSearchMode ? (
        <div className="d-flex justify-content-center align-items-center">
          <h3>No projects found or project is still loading</h3>
        </div>
      ) : (
        <div>
          {filteredProjects.map((project, index) => (
            <Col key={index} xs="auto" style={{ marginBottom: "1rem" }}>
              <Card
                bg="success"
                text="white"
                style={{ width: "auto" }}
                className="mb-2"
              >
                <Card.Body>
                  <Card.Title>
                    {highlightSearchTerm(project.projectName, searchTerm)}
                  </Card.Title>
                  <Card.Title>{project.projectId}</Card.Title>
                  <Card.Text className="mb-1">Use image:</Card.Text>
                  <ul className="mb-0 pb-0">
                    {project.images
                      .slice(0, visibleImages)
                      .map((image: any, i: any) => (
                        <li key={i}>
                          {highlightSearchTerm(image.image, searchTerm)}
                        </li>
                      ))}
                  </ul>
                  <div style={{ marginTop: "1rem" }}>
                    {visibleImages < project.images.length && (
                      <Button
                        variant="secondary"
                        onClick={handleLoadMore}
                        style={{ marginRight: "0.5rem" }}
                      >
                        Load More
                      </Button>
                    )}
                    <Button
                      variant="primary"
                      onClick={() => handleGitLabButtonClick(project.gitlabUrl)}
                    >
                      Repo
                    </Button>
                  </div>
                </Card.Body>
              </Card>
            </Col>
          ))}
        </div>
      )}
    </Container>
  );
};

export default ProjectsPage;
Krisna
  • 2,854
  • 2
  • 24
  • 66

1 Answers1

0

There are a few high-level approaches that will help.

First, use your profiling tools, if you haven't done so already. Chrome's and Firefox's Dev Tools are very powerful, and there are numerous resources online detailing their various features. You've explained that the app is slow to load but haven't gone into specifics about what you've done to confirm what's causing the slowdown. The Dev Tools' Network tab's timing section can help you visualize the network requests. If they show that the waterfall effect from the 950 * 2 HTTP requests is causing the slowdown (which is what I expect), then you can focus your efforts on making the front-end make fewer or smarter requests. If, however, they show something else (maybe the requests finish quickly and there's some unexpected CPU-intensive render function, or maybe the back-end API is much slower than you expect), then you don't have to waste time optimizing an area that isn't the problem.

Second, consider breaking up the API requests, with help from React libraries. Right now, your code is likely slowing down for the following reasons:

  • You're loading every single project at once, even if those projects can't all be on the screen.
  • Nothing can display until everything is loaded.

Therefore, to make things better:

  • Lazily load projects by loading what's on screen or loading what's on screen first.
  • Allow individual project items to render before everything is loaded.

There are React libraries that can help with this. Consider using the following approach:

  1. React Virtuoso can render a virtual list - meaning only those items that are on screen are rendered, while still rendering a scroll bar to allow the user to scroll through the whole list.
  2. Each list item does the necessary API fetches itself, so that individual items can render even if the whole list hasn't loaded.
  3. API fetches are done with a library such as SWR, TanStack Query, or RTK Query, which will manage the caching for you (as well as providing lots of other nice features!) - this saves you from having to write an API cache yourself (which would otherwise get much trickier after doing 1 and 2).
  4. Search filtering could perhaps be done by passing the filter as a prop to individual items, so they can hide themselves (return null) if they don't match.

Third, consider redesigning your API. Issuing 1900 HTTP requests is always going to have overhead. If you have control over your API back-end, it may well be that it's more resource-efficient to return everything in a single list.

Josh Kelley
  • 56,064
  • 19
  • 146
  • 246