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;