From f12c29103c7fef70d1f9aa59754872a102be4693 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 16 Feb 2026 15:30:44 +0100 Subject: [PATCH] Fix project detail crash: replace dynamic hooks with single query The project detail page called useQuery inside .map() to fetch file requirements per round, violating React's rules of hooks. When competitionRounds changed from [] to [round1, round2], the hook count changed, causing React to crash with "Cannot read properties of undefined (reading 'length')". Fix: Add listRequirementsByRounds endpoint that accepts multiple roundIds in one query, replacing the dynamic hook pattern with a single stable useQuery call. Co-Authored-By: Claude Opus 4.6 --- src/app/(admin)/admin/projects/[id]/page.tsx | 13 ++++++------- src/server/routers/file.ts | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/app/(admin)/admin/projects/[id]/page.tsx b/src/app/(admin)/admin/projects/[id]/page.tsx index 12ff478..b3d62f3 100644 --- a/src/app/(admin)/admin/projects/[id]/page.tsx +++ b/src/app/(admin)/admin/projects/[id]/page.tsx @@ -105,14 +105,13 @@ function ProjectDetailContent({ projectId }: { projectId: string }) { // Extract all rounds from the competition const competitionRounds = competition?.rounds || [] - // Fetch requirements for each round - const requirementQueries = competitionRounds.map((round: { id: string; name: string }) => - trpc.file.listRequirements.useQuery({ roundId: round.id }) + // Fetch requirements for all rounds in a single query (avoids dynamic hook violation) + const roundIds = competitionRounds.map((r: { id: string }) => r.id) + const { data: allRequirements = [] } = trpc.file.listRequirementsByRounds.useQuery( + { roundIds }, + { enabled: roundIds.length > 0 } ) - // Combine requirements from all rounds - const allRequirements = requirementQueries.flatMap((q: { data?: unknown[] }) => q.data || []) - const utils = trpc.useUtils() if (isLoading) { @@ -592,7 +591,7 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {

)}
- {req.acceptedMimeTypes.length > 0 && ( + {req.acceptedMimeTypes?.length > 0 && ( {req.acceptedMimeTypes.map((mime: string) => { if (mime === 'application/pdf') return 'PDF' diff --git a/src/server/routers/file.ts b/src/server/routers/file.ts index c52f97f..5f5adf0 100644 --- a/src/server/routers/file.ts +++ b/src/server/routers/file.ts @@ -818,6 +818,20 @@ export const fileRouter = router({ }) }), + /** + * List file requirements for multiple rounds in a single query. + * Avoids dynamic hook violations when fetching requirements per-round. + */ + listRequirementsByRounds: protectedProcedure + .input(z.object({ roundIds: z.array(z.string()).max(50) })) + .query(async ({ ctx, input }) => { + if (input.roundIds.length === 0) return [] + return ctx.prisma.fileRequirement.findMany({ + where: { roundId: { in: input.roundIds } }, + orderBy: { sortOrder: 'asc' }, + }) + }), + /** * Create a file requirement for a stage (admin only) */