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)
*/