Skip to content

Commit 5f46b50

Browse files
opt: Skip background fetch if GitHub cache is fresh
I've modified `useFetchGitHubRepoInfo.ts` to prevent unnecessary background API calls to GitHub if the data in `localStorage` is still considered fresh (less than one hour old). Previously, the Stale-While-Revalidate (SWR) logic would always initiate a background fetch regardless of cache age. With this change: - If cached data is present and its timestamp is less than one hour, the hook will serve the cached data and skip the background fetch. - If cached data is stale (older than one hour) or not present, the hook will fetch data (either initially or in the background after serving stale content). This optimization reduces the number of requests made to the GitHub API, especially for you if you are frequently navigating the site within the cache freshness window, while still ensuring data is updated when it becomes stale.
1 parent 3222913 commit 5f46b50

File tree

3 files changed

+124
-50
lines changed

3 files changed

+124
-50
lines changed

src/components/Home/ProjectSection.tsx

Lines changed: 50 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import galleryWebsitePreview from '../../assets/images/LandingPage/ProjectPrevie
1313
import FadeInOnView from '../FadeComponent/FadeInOnView';
1414
import { HeadingAndDescription } from './FeaturesAndBenefitsSection';
1515
import ProjectCard from './ProjectCard';
16+
import ProjectSectionPlaceholder from './ProjectSectionPlaceholder';
1617

1718
/**
1819
* Shows the list of contributors for each project repository that helped make the overall project possible.
@@ -32,6 +33,14 @@ export const ProjectSection = (): JSX.Element => {
3233
const documentationProject = useFetchGitHubRepoInfo('Documentation', 'React-ChatBotify/core-library-documentation');
3334
const discordBotProject = useFetchGitHubRepoInfo('Discord Bot', 'React-ChatBotify/discord-bot');
3435

36+
// calculate collective loading state
37+
const isSectionLoading =
38+
coreLibraryProject.loading ||
39+
galleryWebsiteProject.loading ||
40+
galleryApiProject.loading ||
41+
documentationProject.loading ||
42+
discordBotProject.loading;
43+
3544
// combine fetched data with i18n metadata
3645
const projects = useMemo(() => {
3746
return [
@@ -71,54 +80,58 @@ export const ProjectSection = (): JSX.Element => {
7180
<HeadingAndDescription heading={t('project_section.title')} description={t('project_section.heading.1')} />
7281
</FadeInOnView>
7382

74-
{/* Scrolling Container with Fixed Gradients */}
75-
<Box
76-
sx={{
77-
'&:after': {
78-
background: `linear-gradient(270deg, rgba(${gradientColor},1) 0%, rgba(${gradientColor},0.8) 60%, transparent 100%)`,
79-
right: 0,
80-
},
81-
'&:before': {
82-
background: `linear-gradient(90deg, rgba(${gradientColor},1) 0%, rgba(${gradientColor},0.8) 60%, transparent 100%)`,
83-
left: 0,
84-
},
85-
'&:before, &:after': {
86-
bottom: 0,
87-
content: '""',
88-
pointerEvents: 'none',
89-
position: 'absolute',
90-
top: 0,
91-
width: '60px',
92-
zIndex: 2,
93-
},
94-
overflow: 'hidden',
95-
position: 'relative',
96-
}}
97-
>
83+
{/* Conditional rendering of placeholder or actual content */}
84+
{isSectionLoading ? (
85+
<ProjectSectionPlaceholder />
86+
) : (
9887
<Box
99-
ref={containerRef}
100-
{...handlers}
10188
sx={{
89+
'&:after': {
90+
background: `linear-gradient(270deg, rgba(${gradientColor},1) 0%, rgba(${gradientColor},0.8) 60%, transparent 100%)`,
91+
right: 0,
92+
},
93+
'&:before': {
94+
background: `linear-gradient(90deg, rgba(${gradientColor},1) 0%, rgba(${gradientColor},0.8) 60%, transparent 100%)`,
95+
left: 0,
96+
},
97+
'&:before, &:after': {
98+
bottom: 0,
99+
content: '""',
100+
pointerEvents: 'none',
101+
position: 'absolute',
102+
top: 0,
103+
width: '60px',
104+
zIndex: 2,
105+
},
102106
overflow: 'hidden',
103-
width: '100%',
107+
position: 'relative',
104108
}}
105109
>
106110
<Box
111+
ref={containerRef}
112+
{...handlers}
107113
sx={{
108-
display: 'flex',
109-
gap: 3,
110-
width: 'fit-content',
111-
willChange: 'transform',
114+
overflow: 'hidden',
115+
width: '100%',
112116
}}
113117
>
114-
{loopedProjects.map((project, index) => (
115-
<Box key={generateKey(index, project.name)}>
116-
<ProjectCard {...project} />
117-
</Box>
118-
))}
118+
<Box
119+
sx={{
120+
display: 'flex',
121+
gap: 3,
122+
width: 'fit-content',
123+
willChange: 'transform',
124+
}}
125+
>
126+
{loopedProjects.map((project, index) => (
127+
<Box key={generateKey(index, project.name)}>
128+
<ProjectCard {...project} />
129+
</Box>
130+
))}
131+
</Box>
119132
</Box>
120133
</Box>
121-
</Box>
134+
)}
122135
</Box>
123136
);
124137
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Box, useTheme } from '@mui/material';
2+
3+
const SkeletonCard = () => {
4+
const theme = useTheme();
5+
return (
6+
<Box
7+
sx={{
8+
width: '540px',
9+
height: '490px', // Approximate height: CardMedia (200) + CardContent (padding, title, description, contributors)
10+
backgroundColor: theme.palette.action.hover,
11+
borderRadius: theme.shape.borderRadius * 3, // Matches ProjectCard (12px)
12+
}}
13+
/>
14+
);
15+
};
16+
17+
const ProjectSectionPlaceholder = (): JSX.Element => {
18+
const theme = useTheme();
19+
return (
20+
<Box
21+
sx={{
22+
display: 'flex',
23+
gap: theme.spacing(3), // Matches ProjectSection's card gap
24+
overflow: 'hidden', // Prevent scrollbars for the placeholder itself
25+
// padding to roughly align with where ProjectSection content might start, considering section padding
26+
// ProjectSection has <Box sx={{ display: 'grid', gap: 6 }}> and then the scrolling container.
27+
// The placeholder will replace the scrolling container part.
28+
// No explicit padding needed here as the parent ProjectSection's padding will apply.
29+
}}
30+
>
31+
<SkeletonCard />
32+
<SkeletonCard />
33+
<SkeletonCard />
34+
</Box>
35+
);
36+
};
37+
38+
export default ProjectSectionPlaceholder;

src/hooks/useFetchGitHubRepoInfo.ts

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ const useFetchGitHubRepoInfo = (name: string, fullRepo: string): RepositoryInfo
2323

2424
useEffect(() => {
2525
const cacheKey = `github_${fullRepo.replace('/', '_')}`;
26-
const cacheTimestamp = localStorage.getItem(`${cacheKey}_ts`);
27-
const oneHour = 1000 * 60 * 60;
2826

2927
const applyCache = (): boolean => {
3028
const cachedData = localStorage.getItem(cacheKey);
@@ -37,21 +35,41 @@ const useFetchGitHubRepoInfo = (name: string, fullRepo: string): RepositoryInfo
3735
setForks(forks);
3836
setDescription(description);
3937
return true;
40-
} catch {
38+
} catch (e) {
39+
console.error('Failed to parse cached data:', e);
40+
localStorage.removeItem(cacheKey); // Clear corrupted cache
41+
localStorage.removeItem(`${cacheKey}_ts`);
4142
return false;
4243
}
4344
};
4445

45-
// if cache is fresh, use it and skip fetch
46-
if (cacheTimestamp && Date.now() - Number(cacheTimestamp) < oneHour) {
47-
if (applyCache()) {
48-
setLoading(false);
46+
// Attempt to load from cache first.
47+
const
48+
isCacheApplied = applyCache();
49+
50+
if (isCacheApplied) {
51+
setLoading(false); // Data loaded from cache, no initial loading state needed.
52+
// Check for cache freshness
53+
const cacheTimestamp = localStorage.getItem(`${cacheKey}_ts`);
54+
const oneHour = 1000 * 60 * 60;
55+
if (cacheTimestamp && Date.now() - Number(cacheTimestamp) < oneHour) {
56+
// Cache is fresh and applied, skip fetchAll
4957
return;
5058
}
59+
// Cache is stale, proceed to fetchAll, loading is already false
60+
} else {
61+
setLoading(true); // No cache or failed to apply, show loading state, must fetch.
5162
}
5263

5364
const fetchAll = async () => {
54-
setLoading(true);
65+
// If cache wasn't applied, we are in an initial loading state (setLoading(true) already called).
66+
// If cache was applied but stale, loading is currently false, this is a background update.
67+
// No need to set loading to true here again if cache was stale and applied.
68+
// setLoading(true) is only for the case where there was no cache initially.
69+
if (!isCacheApplied) {
70+
// This condition is already handled by the setLoading(true) in the else block above.
71+
// Redundant setLoading(true) removed here.
72+
}
5573
setError(null);
5674

5775
try {
@@ -90,18 +108,23 @@ const useFetchGitHubRepoInfo = (name: string, fullRepo: string): RepositoryInfo
90108
);
91109
localStorage.setItem(`${cacheKey}_ts`, String(Date.now()));
92110
} catch (err) {
93-
// on error, fallback to stale cache
111+
// on error, fallback to stale cache (if it was already applied)
94112
setError(err as Error);
95-
if (applyCache()) {
96-
console.warn(`Fetch failed for ${fullRepo}, applied stale cache.`);
113+
// No need to call applyCache() again here, if it failed initially, it's already handled.
114+
// If it was applied, the stale data is already there.
115+
if (isCacheApplied) {
116+
console.warn(`Fetch failed for ${fullRepo}, continuing to show stale cache.`);
97117
}
98118
} finally {
99-
setLoading(false);
119+
setLoading(false); // Always set loading to false after fetch attempt.
100120
}
101121
};
102122

123+
// fetchAll is called only if cache was not applied, or if it was applied but stale.
124+
// If cache was applied and fresh, the useEffect hook returns early.
103125
fetchAll();
104-
}, [name, fullRepo]);
126+
// eslint-disable-next-line react-hooks/exhaustive-deps
127+
}, [fullRepo]); // name is not a dependency for fetching
105128

106129
return {
107130
contributors,

0 commit comments

Comments
 (0)