Skip to content

Commit 589157b

Browse files
committed
Refactor PaginatedDependencies to use loader-resolved data
- Remove Promise.all, <Await>, Suspense, and useMemo from the component - Simplify Props to accept resolved objects instead of promises - Use URL query param as single source of truth for page - Remove local/debounced state, refs, and navigation effects - Ensure stable keys for list items - Component now renders cleanly with correct pagination Previously, mixing unresolved promises, local state, and debouncing caused stale/mis-synced data and infinite re-render loops. Now the component is simpler, predictable, and fully driven by loader data and URL. Refs. TS-2750
1 parent 41d8862 commit 589157b

File tree

4 files changed

+61
-182
lines changed

4 files changed

+61
-182
lines changed
Lines changed: 49 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -1,175 +1,69 @@
1-
import { memo, Suspense, useEffect, useMemo, useRef, useState } from "react";
21
import "./PaginatedDependencies.css";
3-
import { Heading, NewPagination, SkeletonBox } from "@thunderstore/cyberstorm";
2+
import { Heading, NewPagination } from "@thunderstore/cyberstorm";
43
import { ListingDependency } from "../ListingDependency/ListingDependency";
5-
import { Await, useNavigationType, useSearchParams } from "react-router";
6-
import { useDebounce } from "use-debounce";
7-
import type { getPackageVersionDetails } from "@thunderstore/dapper-ts/src/methods/packageVersion";
8-
import type { getPackageVersionDependencies } from "@thunderstore/dapper-ts/src/methods/package";
9-
import { setParamsBlobValue } from "cyberstorm/utils/searchParamsUtils";
4+
import { useSearchParams } from "react-router";
5+
import { type PackageVersionDependency } from "@thunderstore/thunderstore-api";
6+
7+
interface DependecyResponse {
8+
results: PackageVersionDependency[];
9+
count: number;
10+
}
1011

1112
interface Props {
12-
version:
13-
| Awaited<ReturnType<typeof getPackageVersionDetails>>
14-
| ReturnType<typeof getPackageVersionDetails>;
15-
dependencies:
16-
| Awaited<ReturnType<typeof getPackageVersionDependencies>>
17-
| ReturnType<typeof getPackageVersionDependencies>;
13+
dependencies: DependecyResponse;
1814
pageSize?: number;
1915
siblingCount?: number;
2016
}
2117

22-
export const PaginatedDependencies = memo(function PaginatedDependencies(
23-
props: Props
24-
) {
25-
const navigationType = useNavigationType();
26-
18+
export function PaginatedDependencies({
19+
dependencies,
20+
pageSize = 20, // Default page size from backend
21+
siblingCount = 4,
22+
}: Props) {
2723
const [searchParams, setSearchParams] = useSearchParams();
24+
const page = Number(searchParams.get("page") ?? 1);
2825

29-
const initialParams = searchParamsToBlob(searchParams);
30-
31-
const [searchParamsBlob, setSearchParamsBlob] =
32-
useState<SearchParamsType>(initialParams);
33-
34-
const [currentPage, setCurrentPage] = useState(
35-
searchParams.get("page") ? Number(searchParams.get("page")) : 1
36-
);
37-
38-
const [debouncedSearchParamsBlob] = useDebounce(searchParamsBlob, 300, {
39-
maxWait: 300,
40-
});
41-
42-
const searchParamsBlobRef = useRef(debouncedSearchParamsBlob);
26+
const handlePageChange = (nextPage: number) => {
27+
const next = new URLSearchParams(searchParams);
4328

44-
const searchParamsRef = useRef(searchParams);
45-
useEffect(() => {
46-
if (navigationType === "POP") {
47-
if (searchParamsRef.current !== searchParams) {
48-
const spb = searchParamsToBlob(searchParams);
49-
setSearchParamsBlob(spb);
50-
setCurrentPage(spb.page);
51-
searchParamsRef.current = searchParams;
52-
}
53-
searchParamsBlobRef.current = searchParamsToBlob(searchParams);
29+
if (nextPage === 1) {
30+
next.delete("page");
31+
} else {
32+
next.set("page", String(nextPage));
5433
}
55-
}, [searchParams]);
5634

57-
useEffect(() => {
58-
if (
59-
navigationType !== "POP" ||
60-
(navigationType === "POP" &&
61-
searchParamsBlobRef.current !== debouncedSearchParamsBlob)
62-
) {
63-
if (searchParamsBlobRef.current !== debouncedSearchParamsBlob) {
64-
const oldPage = searchParams.get("page")
65-
? Number(searchParams.get("page"))
66-
: 1;
67-
// Page number
68-
if (oldPage !== debouncedSearchParamsBlob.page) {
69-
if (debouncedSearchParamsBlob.page === 1) {
70-
searchParams.delete("page");
71-
setCurrentPage(1);
72-
} else {
73-
searchParams.set("page", String(debouncedSearchParamsBlob.page));
74-
setCurrentPage(debouncedSearchParamsBlob.page);
75-
}
76-
}
77-
const uncommittedSearchParams = searchParamsToBlob(searchParams);
78-
79-
if (
80-
navigationType !== "POP" ||
81-
(navigationType === "POP" &&
82-
!compareSearchParamBlobs(
83-
uncommittedSearchParams,
84-
searchParamsBlobRef.current
85-
) &&
86-
compareSearchParamBlobs(
87-
uncommittedSearchParams,
88-
debouncedSearchParamsBlob
89-
))
90-
) {
91-
setSearchParams(searchParams, { preventScrollReset: true });
92-
}
93-
searchParamsBlobRef.current = debouncedSearchParamsBlob;
94-
}
95-
}
96-
}, [debouncedSearchParamsBlob]);
97-
98-
const versionAndDependencies = useMemo(
99-
() => Promise.all([props.version, props.dependencies]),
100-
[currentPage]
101-
);
35+
setSearchParams(next, { preventScrollReset: true });
36+
};
10237

10338
return (
10439
<div className="paginated-dependencies">
105-
<Suspense
106-
fallback={<SkeletonBox className="paginated-dependencies__skeleton" />}
107-
>
108-
<Await
109-
resolve={versionAndDependencies}
110-
errorElement={
111-
<div>Error occurred while loading required dependencies</div>
112-
}
113-
>
114-
{(resolvedValue) => {
115-
return (
116-
<>
117-
<div className="paginated-dependencies__title">
118-
<Heading csLevel="3" csSize="3">
119-
Required mods ({resolvedValue[0].dependency_count})
120-
</Heading>
121-
<span className="paginated-dependencies__description">
122-
This package requires the following packages to work.
123-
</span>
124-
</div>
125-
<div className="paginated-dependencies__body">
126-
{resolvedValue[1].results.map((dep, key) => {
127-
return <ListingDependency key={key} dependency={dep} />;
128-
})}
129-
</div>
130-
<NewPagination
131-
currentPage={currentPage}
132-
pageSize={props.pageSize ?? 20}
133-
totalCount={resolvedValue[0].dependency_count}
134-
onPageChange={setParamsBlobValue(
135-
setSearchParamsBlob,
136-
searchParamsBlob,
137-
"page"
138-
)}
139-
siblingCount={props.siblingCount ?? 4}
140-
/>
141-
</>
142-
);
143-
}}
144-
</Await>
145-
</Suspense>
40+
<div className="paginated-dependencies__title">
41+
<Heading csLevel="3" csSize="3">
42+
Required mods ({dependencies.count})
43+
</Heading>
44+
<span className="paginated-dependencies__description">
45+
This package requires the following packages to work.
46+
</span>
47+
</div>
48+
49+
<div className="paginated-dependencies__body">
50+
{dependencies.results.map((dep, idx: number) => (
51+
<ListingDependency
52+
key={`${dep.name}-${dep.version_number}-${idx}`}
53+
dependency={dep}
54+
/>
55+
))}
56+
</div>
57+
58+
<NewPagination
59+
currentPage={page}
60+
pageSize={pageSize}
61+
totalCount={dependencies.count}
62+
onPageChange={handlePageChange}
63+
siblingCount={siblingCount}
64+
/>
14665
</div>
14766
);
148-
});
67+
}
14968

15069
PaginatedDependencies.displayName = "PaginatedDependencies";
151-
152-
export type SearchParamsType = {
153-
page: number;
154-
};
155-
156-
export const compareSearchParamBlobs = (
157-
b1: SearchParamsType,
158-
b2: SearchParamsType
159-
) => {
160-
if (b1.page !== b2.page) return false;
161-
return true;
162-
};
163-
164-
export const searchParamsToBlob = (searchParams: URLSearchParams) => {
165-
const initialPage = searchParams.get("page");
166-
167-
return {
168-
page:
169-
initialPage &&
170-
!Number.isNaN(Number.parseInt(initialPage)) &&
171-
Number.isSafeInteger(Number.parseInt(initialPage))
172-
? Number.parseInt(initialPage)
173-
: 1,
174-
};
175-
};

apps/cyberstorm-remix/app/p/tabs/Required/PackageVersionRequired.tsx

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,12 @@ export async function clientLoader({ params, request }: LoaderFunctionArgs) {
4949
const page = searchParams.get("page");
5050

5151
return {
52-
version: dapper.getPackageVersionDetails(
52+
version: await dapper.getPackageVersionDetails(
5353
params.namespaceId,
5454
params.packageId,
5555
params.packageVersion
5656
),
57-
dependencies: dapper.getPackageVersionDependencies(
57+
dependencies: await dapper.getPackageVersionDependencies(
5858
params.namespaceId,
5959
params.packageId,
6060
params.packageVersion,
@@ -66,11 +66,6 @@ export async function clientLoader({ params, request }: LoaderFunctionArgs) {
6666
}
6767

6868
export default function PackageVersionRequired() {
69-
const { version, dependencies } = useLoaderData<
70-
typeof loader | typeof clientLoader
71-
>();
72-
73-
return (
74-
<PaginatedDependencies version={version} dependencies={dependencies} />
75-
);
69+
const { dependencies } = useLoaderData<typeof loader | typeof clientLoader>();
70+
return <PaginatedDependencies dependencies={dependencies} />;
7671
}

apps/cyberstorm-remix/app/p/tabs/Required/PackageVersionWithoutCommunityRequired.tsx

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,12 @@ export async function clientLoader({ params, request }: LoaderFunctionArgs) {
4949
const page = searchParams.get("page");
5050

5151
return {
52-
version: dapper.getPackageVersionDetails(
52+
version: await dapper.getPackageVersionDetails(
5353
params.namespaceId,
5454
params.packageId,
5555
params.packageVersion
5656
),
57-
dependencies: dapper.getPackageVersionDependencies(
57+
dependencies: await dapper.getPackageVersionDependencies(
5858
params.namespaceId,
5959
params.packageId,
6060
params.packageVersion,
@@ -66,11 +66,6 @@ export async function clientLoader({ params, request }: LoaderFunctionArgs) {
6666
}
6767

6868
export default function PackageVersionWithoutCommunityRequired() {
69-
const { version, dependencies } = useLoaderData<
70-
typeof loader | typeof clientLoader
71-
>();
72-
73-
return (
74-
<PaginatedDependencies version={version} dependencies={dependencies} />
75-
);
69+
const { dependencies } = useLoaderData<typeof loader | typeof clientLoader>();
70+
return <PaginatedDependencies dependencies={dependencies} />;
7671
}

apps/cyberstorm-remix/app/p/tabs/Required/Required.tsx

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,12 @@ export async function clientLoader({ params, request }: LoaderFunctionArgs) {
5959
);
6060

6161
return {
62-
version: dapper.getPackageVersionDetails(
62+
version: await dapper.getPackageVersionDetails(
6363
params.namespaceId,
6464
params.packageId,
6565
listing.latest_version_number
6666
),
67-
dependencies: dapper.getPackageVersionDependencies(
67+
dependencies: await dapper.getPackageVersionDependencies(
6868
params.namespaceId,
6969
params.packageId,
7070
listing.latest_version_number,
@@ -76,11 +76,6 @@ export async function clientLoader({ params, request }: LoaderFunctionArgs) {
7676
}
7777

7878
export default function PackageVersionRequired() {
79-
const { version, dependencies } = useLoaderData<
80-
typeof loader | typeof clientLoader
81-
>();
82-
83-
return (
84-
<PaginatedDependencies version={version} dependencies={dependencies} />
85-
);
79+
const { dependencies } = useLoaderData<typeof loader | typeof clientLoader>();
80+
return <PaginatedDependencies dependencies={dependencies} />;
8681
}

0 commit comments

Comments
 (0)