Skip to content

Commit 73e4071

Browse files
committed
refactor(store): migrate RecentSongs to Context-based Zustand and fix pagination
- Migrate singleton Zustand store to React Context to allow synchronous SSR hydration and prevent initial render flashes. - Fix pagination desynchronization by incrementing the page state only after a successful network response. - Remove redundant ID-based concurrency checks, relying strictly on AbortController for deterministic network cancellation. - Delete implicit `useRecentSongsCategoriesLoader` side-effect hook in favor of explicit component-level data fetching. Fixes #87
1 parent f9a1b33 commit 73e4071

6 files changed

Lines changed: 215 additions & 196 deletions

File tree

apps/frontend/src/app/(content)/page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Metadata } from 'next';
22

3+
import { BROWSER_SONGS } from '@nbw/config';
34
import type { FeaturedSongsDto, PageDto, SongPreviewDto } from '@nbw/database';
45
import axiosInstance from '@web/lib/axios';
56
import { HomePageProvider } from '@web/modules/browse/components/client/context/HomePage.context';
@@ -9,8 +10,8 @@ async function fetchRecentSongs() {
910
try {
1011
const response = await axiosInstance.get<PageDto<SongPreviewDto>>('/song', {
1112
params: {
12-
page: 1, // TODO: fix constants
13-
limit: 11, // TODO: change 'limit' parameter to 'skip' and load 12 songs initially, then load 8 more songs on each pagination
13+
page: 1,
14+
limit: BROWSER_SONGS.recentFetchCount,
1415
sort: 'recent',
1516
order: 'desc',
1617
},

apps/frontend/src/modules/browse/components/HomePageComponent.tsx

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,19 @@ import { WelcomeBanner } from '../WelcomeBanner';
1919

2020
import { CategoryButtonGroup } from './client/CategoryButton';
2121
import { useFeaturedSongsProvider } from './client/context/FeaturedSongs.context';
22-
import {
23-
useRecentSongsProvider,
24-
useRecentSongsPageLoader,
25-
useRecentSongsCategoriesLoader,
26-
} from './client/context/RecentSongs.context';
22+
import { useRecentSongsStore } from './client/context/RecentSongs.context';
2723
import LoadMoreButton from './client/LoadMoreButton';
2824
import { TimespanButtonGroup } from './client/TimespanButton';
2925
import SongCard from './SongCard';
3026
import SongCardGroup from './SongCardGroup';
3127

3228
export const HomePageComponent = () => {
33-
// Initialize sync hooks for proper effect handling
34-
useRecentSongsPageLoader();
35-
useRecentSongsCategoriesLoader();
36-
3729
const { featuredSongsPage, timespan } = useFeaturedSongsProvider();
38-
const { recentSongs, increasePageRecent, hasMore } = useRecentSongsProvider();
30+
const recentItems = useRecentSongsStore((state) => state.recentItems);
31+
const increasePageRecent = useRecentSongsStore(
32+
(state) => state.increasePageRecent,
33+
);
34+
const hasMore = useRecentSongsStore((state) => state.hasMore);
3935
return (
4036
<>
4137
{/* Welcome banner/Hero */}
@@ -87,14 +83,17 @@ export const HomePageComponent = () => {
8783
</div>
8884
<div className='h-6' />
8985
<SongCardGroup data-test='recent-songs'>
90-
{recentSongs.map((song, i) =>
91-
// TODO: currently null = skeleton, undefined = ad slot. There must be a more robust system to indicate this.
92-
song === undefined ? (
93-
<SongCardAdSlot key={i} />
94-
) : (
95-
<SongCard key={i} song={song} />
96-
),
97-
)}
86+
{recentItems.map((item, i) => {
87+
if (item.type === 'ad') {
88+
return <SongCardAdSlot key={i} />;
89+
}
90+
91+
if (item.type === 'loading') {
92+
return <SongCard key={i} song={null} />;
93+
}
94+
95+
return <SongCard key={i} song={item.data} />;
96+
})}
9897
</SongCardGroup>
9998
<div className='flex flex-col w-full justify-between items-center mt-4'>
10099
{hasMore ? (

apps/frontend/src/modules/browse/components/client/CategoryButton.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
'use client';
2+
import { useEffect } from 'react';
3+
24
import { UPLOAD_CONSTANTS } from '@nbw/config';
35
import type { CategoryType } from '@nbw/database';
46
import {
@@ -9,7 +11,7 @@ import {
911
CarouselPreviousSmall,
1012
} from '@web/modules/shared/components/client/Carousel';
1113

12-
import { useRecentSongsProvider } from './context/RecentSongs.context';
14+
import { useRecentSongsStore } from './context/RecentSongs.context';
1315

1416
type CategoryButtonProps = {
1517
children: React.ReactNode;
@@ -20,8 +22,21 @@ type CategoryButtonProps = {
2022
};
2123

2224
export const CategoryButtonGroup = () => {
23-
const { categories, setSelectedCategory, selectedCategory } =
24-
useRecentSongsProvider();
25+
const categories = useRecentSongsStore((state) => state.categories);
26+
const fetchCategories = useRecentSongsStore((state) => state.fetchCategories);
27+
const setSelectedCategory = useRecentSongsStore(
28+
(state) => state.setSelectedCategory,
29+
);
30+
const selectedCategory = useRecentSongsStore(
31+
(state) => state.selectedCategory,
32+
);
33+
const categoryCount = Object.keys(categories).length;
34+
35+
useEffect(() => {
36+
if (categoryCount === 0) {
37+
void fetchCategories();
38+
}
39+
}, [categoryCount, fetchCategories]);
2540

2641
return (
2742
<Carousel

apps/frontend/src/modules/browse/components/client/context/HomePage.context.tsx

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,8 @@
22

33
import type { FeaturedSongsDtoType, SongPreviewDtoType } from '@nbw/database';
44

5-
import {
6-
FeaturedSongsProvider,
7-
useFeaturedSongsStore,
8-
} from './FeaturedSongs.context';
9-
import {
10-
RecentSongsProvider,
11-
useRecentSongsStore,
12-
} from './RecentSongs.context';
13-
14-
/**
15-
* Composed hook that provides access to both FeaturedSongs and RecentSongs stores
16-
*/
17-
export function useHomePageStore() {
18-
const featuredSongs = useFeaturedSongsStore();
19-
const recentSongs = useRecentSongsStore();
20-
21-
return {
22-
featuredSongs,
23-
recentSongs,
24-
};
25-
}
5+
import { FeaturedSongsProvider } from './FeaturedSongs.context';
6+
import { RecentSongsProvider } from './RecentSongs.context';
267

278
export function HomePageProvider({
289
children,
@@ -41,8 +22,3 @@ export function HomePageProvider({
4122
</RecentSongsProvider>
4223
);
4324
}
44-
45-
// Legacy hook name for backward compatibility
46-
export function useHomePageProvider() {
47-
return useHomePageStore();
48-
}

0 commit comments

Comments
 (0)