From 0a55a7f5857eae2d6ca9aa40180980a5eab8cb44 Mon Sep 17 00:00:00 2001 From: Daniel Vainio Date: Tue, 4 Nov 2025 10:28:14 +0200 Subject: [PATCH 1/3] Refactor PaginatedDependencies - Remove Promise.all, 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 - Pass only the resolved dependencies to PaginatedDependencies - 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 --- .../PaginatedDependencies.tsx | 204 +++++------------- 1 file changed, 49 insertions(+), 155 deletions(-) diff --git a/apps/cyberstorm-remix/app/commonComponents/PaginatedDependencies/PaginatedDependencies.tsx b/apps/cyberstorm-remix/app/commonComponents/PaginatedDependencies/PaginatedDependencies.tsx index bf2c2008e..71620a9ba 100644 --- a/apps/cyberstorm-remix/app/commonComponents/PaginatedDependencies/PaginatedDependencies.tsx +++ b/apps/cyberstorm-remix/app/commonComponents/PaginatedDependencies/PaginatedDependencies.tsx @@ -1,175 +1,69 @@ -import { memo, Suspense, useEffect, useMemo, useRef, useState } from "react"; import "./PaginatedDependencies.css"; -import { Heading, NewPagination, SkeletonBox } from "@thunderstore/cyberstorm"; +import { Heading, NewPagination } from "@thunderstore/cyberstorm"; import { ListingDependency } from "../ListingDependency/ListingDependency"; -import { Await, useNavigationType, useSearchParams } from "react-router"; -import { useDebounce } from "use-debounce"; -import type { getPackageVersionDetails } from "@thunderstore/dapper-ts/src/methods/packageVersion"; -import type { getPackageVersionDependencies } from "@thunderstore/dapper-ts/src/methods/package"; -import { setParamsBlobValue } from "cyberstorm/utils/searchParamsUtils"; +import { useSearchParams } from "react-router"; +import { type PackageVersionDependency } from "@thunderstore/thunderstore-api"; + +interface DependecyResponse { + results: PackageVersionDependency[]; + count: number; +} interface Props { - version: - | Awaited> - | ReturnType; - dependencies: - | Awaited> - | ReturnType; + dependencies: DependecyResponse; pageSize?: number; siblingCount?: number; } -export const PaginatedDependencies = memo(function PaginatedDependencies( - props: Props -) { - const navigationType = useNavigationType(); - +export function PaginatedDependencies({ + dependencies, + pageSize = 20, // Default page size from backend + siblingCount = 4, +}: Props) { const [searchParams, setSearchParams] = useSearchParams(); + const page = Number(searchParams.get("page") ?? 1); - const initialParams = searchParamsToBlob(searchParams); - - const [searchParamsBlob, setSearchParamsBlob] = - useState(initialParams); - - const [currentPage, setCurrentPage] = useState( - searchParams.get("page") ? Number(searchParams.get("page")) : 1 - ); - - const [debouncedSearchParamsBlob] = useDebounce(searchParamsBlob, 300, { - maxWait: 300, - }); - - const searchParamsBlobRef = useRef(debouncedSearchParamsBlob); + const handlePageChange = (nextPage: number) => { + const next = new URLSearchParams(searchParams); - const searchParamsRef = useRef(searchParams); - useEffect(() => { - if (navigationType === "POP") { - if (searchParamsRef.current !== searchParams) { - const spb = searchParamsToBlob(searchParams); - setSearchParamsBlob(spb); - setCurrentPage(spb.page); - searchParamsRef.current = searchParams; - } - searchParamsBlobRef.current = searchParamsToBlob(searchParams); + if (nextPage === 1) { + next.delete("page"); + } else { + next.set("page", String(nextPage)); } - }, [searchParams]); - useEffect(() => { - if ( - navigationType !== "POP" || - (navigationType === "POP" && - searchParamsBlobRef.current !== debouncedSearchParamsBlob) - ) { - if (searchParamsBlobRef.current !== debouncedSearchParamsBlob) { - const oldPage = searchParams.get("page") - ? Number(searchParams.get("page")) - : 1; - // Page number - if (oldPage !== debouncedSearchParamsBlob.page) { - if (debouncedSearchParamsBlob.page === 1) { - searchParams.delete("page"); - setCurrentPage(1); - } else { - searchParams.set("page", String(debouncedSearchParamsBlob.page)); - setCurrentPage(debouncedSearchParamsBlob.page); - } - } - const uncommittedSearchParams = searchParamsToBlob(searchParams); - - if ( - navigationType !== "POP" || - (navigationType === "POP" && - !compareSearchParamBlobs( - uncommittedSearchParams, - searchParamsBlobRef.current - ) && - compareSearchParamBlobs( - uncommittedSearchParams, - debouncedSearchParamsBlob - )) - ) { - setSearchParams(searchParams, { preventScrollReset: true }); - } - searchParamsBlobRef.current = debouncedSearchParamsBlob; - } - } - }, [debouncedSearchParamsBlob]); - - const versionAndDependencies = useMemo( - () => Promise.all([props.version, props.dependencies]), - [currentPage] - ); + setSearchParams(next, { preventScrollReset: true }); + }; return (
- } - > - Error occurred while loading required dependencies
- } - > - {(resolvedValue) => { - return ( - <> -
- - Required mods ({resolvedValue[0].dependency_count}) - - - This package requires the following packages to work. - -
-
- {resolvedValue[1].results.map((dep, key) => { - return ; - })} -
- - - ); - }} - - +
+ + Required mods ({dependencies.count}) + + + This package requires the following packages to work. + +
+ +
+ {dependencies.results.map((dep, idx: number) => ( + + ))} +
+ + ); -}); +} PaginatedDependencies.displayName = "PaginatedDependencies"; - -export type SearchParamsType = { - page: number; -}; - -export const compareSearchParamBlobs = ( - b1: SearchParamsType, - b2: SearchParamsType -) => { - if (b1.page !== b2.page) return false; - return true; -}; - -export const searchParamsToBlob = (searchParams: URLSearchParams) => { - const initialPage = searchParams.get("page"); - - return { - page: - initialPage && - !Number.isNaN(Number.parseInt(initialPage)) && - Number.isSafeInteger(Number.parseInt(initialPage)) - ? Number.parseInt(initialPage) - : 1, - }; -}; From 5300285e26cabc3bb0cf8b4033b0bdbe74e6708b Mon Sep 17 00:00:00 2001 From: Daniel Vainio Date: Tue, 4 Nov 2025 10:31:32 +0200 Subject: [PATCH 2/3] Update PaginatedDependencies usage - loader fetches and resolves dependencies - clientLoader returns promises - Resolve promises with Await and render skeletonbox - Pass only resolved dependencies to PaginatedDependencies Refs. TS-2750 --- .../tabs/Required/PackageVersionRequired.tsx | 25 ++++++++++++++----- ...PackageVersionWithoutCommunityRequired.tsx | 25 ++++++++++++++----- .../app/p/tabs/Required/Required.tsx | 25 ++++++++++++++----- 3 files changed, 57 insertions(+), 18 deletions(-) diff --git a/apps/cyberstorm-remix/app/p/tabs/Required/PackageVersionRequired.tsx b/apps/cyberstorm-remix/app/p/tabs/Required/PackageVersionRequired.tsx index 8cf870a3a..bd8d99c21 100644 --- a/apps/cyberstorm-remix/app/p/tabs/Required/PackageVersionRequired.tsx +++ b/apps/cyberstorm-remix/app/p/tabs/Required/PackageVersionRequired.tsx @@ -1,11 +1,13 @@ +import { Suspense } from "react"; import { type LoaderFunctionArgs } from "react-router"; -import { useLoaderData } from "react-router"; +import { useLoaderData, Await } from "react-router"; import { DapperTs } from "@thunderstore/dapper-ts"; +import { SkeletonBox } from "@thunderstore/cyberstorm"; +import { PaginatedDependencies } from "~/commonComponents/PaginatedDependencies/PaginatedDependencies"; import { getPublicEnvVariables, getSessionTools, } from "cyberstorm/security/publicEnvVariables"; -import { PaginatedDependencies } from "~/commonComponents/PaginatedDependencies/PaginatedDependencies"; export async function loader({ params, request }: LoaderFunctionArgs) { if (params.namespaceId && params.packageId && params.packageVersion) { @@ -66,11 +68,22 @@ export async function clientLoader({ params, request }: LoaderFunctionArgs) { } export default function PackageVersionRequired() { - const { version, dependencies } = useLoaderData< - typeof loader | typeof clientLoader - >(); + const { dependencies } = useLoaderData(); return ( - + } + > + Error occurred while loading required dependencies + } + > + {(resolvedDependencies) => ( + + )} + + ); } diff --git a/apps/cyberstorm-remix/app/p/tabs/Required/PackageVersionWithoutCommunityRequired.tsx b/apps/cyberstorm-remix/app/p/tabs/Required/PackageVersionWithoutCommunityRequired.tsx index 592621ac2..af6cdd0ac 100644 --- a/apps/cyberstorm-remix/app/p/tabs/Required/PackageVersionWithoutCommunityRequired.tsx +++ b/apps/cyberstorm-remix/app/p/tabs/Required/PackageVersionWithoutCommunityRequired.tsx @@ -1,11 +1,13 @@ +import { Suspense } from "react"; import { type LoaderFunctionArgs } from "react-router"; -import { useLoaderData } from "react-router"; +import { useLoaderData, Await } from "react-router"; import { DapperTs } from "@thunderstore/dapper-ts"; +import { SkeletonBox } from "@thunderstore/cyberstorm"; +import { PaginatedDependencies } from "~/commonComponents/PaginatedDependencies/PaginatedDependencies"; import { getPublicEnvVariables, getSessionTools, } from "cyberstorm/security/publicEnvVariables"; -import { PaginatedDependencies } from "~/commonComponents/PaginatedDependencies/PaginatedDependencies"; export async function loader({ params, request }: LoaderFunctionArgs) { if (params.namespaceId && params.packageId && params.packageVersion) { @@ -66,11 +68,22 @@ export async function clientLoader({ params, request }: LoaderFunctionArgs) { } export default function PackageVersionWithoutCommunityRequired() { - const { version, dependencies } = useLoaderData< - typeof loader | typeof clientLoader - >(); + const { dependencies } = useLoaderData(); return ( - + } + > + Error occurred while loading required dependencies + } + > + {(resolvedDependencies) => ( + + )} + + ); } diff --git a/apps/cyberstorm-remix/app/p/tabs/Required/Required.tsx b/apps/cyberstorm-remix/app/p/tabs/Required/Required.tsx index be62cd526..64d76108c 100644 --- a/apps/cyberstorm-remix/app/p/tabs/Required/Required.tsx +++ b/apps/cyberstorm-remix/app/p/tabs/Required/Required.tsx @@ -1,11 +1,13 @@ +import { Suspense } from "react"; import { type LoaderFunctionArgs } from "react-router"; -import { useLoaderData } from "react-router"; +import { useLoaderData, Await } from "react-router"; import { DapperTs } from "@thunderstore/dapper-ts"; +import { SkeletonBox } from "@thunderstore/cyberstorm"; +import { PaginatedDependencies } from "~/commonComponents/PaginatedDependencies/PaginatedDependencies"; import { getPublicEnvVariables, getSessionTools, } from "cyberstorm/security/publicEnvVariables"; -import { PaginatedDependencies } from "~/commonComponents/PaginatedDependencies/PaginatedDependencies"; export async function loader({ params, request }: LoaderFunctionArgs) { if (params.communityId && params.namespaceId && params.packageId) { @@ -76,11 +78,22 @@ export async function clientLoader({ params, request }: LoaderFunctionArgs) { } export default function PackageVersionRequired() { - const { version, dependencies } = useLoaderData< - typeof loader | typeof clientLoader - >(); + const { dependencies } = useLoaderData(); return ( - + } + > + Error occurred while loading required dependencies + } + > + {(resolvedDependencies) => ( + + )} + + ); } From e914a3cd23b1f375ee27a53f31d379d775e7dd7e Mon Sep 17 00:00:00 2001 From: Daniel Vainio Date: Tue, 4 Nov 2025 10:44:09 +0200 Subject: [PATCH 3/3] Fix typo in interface Refs. TS-2750 --- .../PaginatedDependencies/PaginatedDependencies.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/cyberstorm-remix/app/commonComponents/PaginatedDependencies/PaginatedDependencies.tsx b/apps/cyberstorm-remix/app/commonComponents/PaginatedDependencies/PaginatedDependencies.tsx index 71620a9ba..9b9d409cb 100644 --- a/apps/cyberstorm-remix/app/commonComponents/PaginatedDependencies/PaginatedDependencies.tsx +++ b/apps/cyberstorm-remix/app/commonComponents/PaginatedDependencies/PaginatedDependencies.tsx @@ -4,13 +4,13 @@ import { ListingDependency } from "../ListingDependency/ListingDependency"; import { useSearchParams } from "react-router"; import { type PackageVersionDependency } from "@thunderstore/thunderstore-api"; -interface DependecyResponse { +interface DependencyResponse { results: PackageVersionDependency[]; count: number; } interface Props { - dependencies: DependecyResponse; + dependencies: DependencyResponse; pageSize?: number; siblingCount?: number; }