Fix project detail crash: replace dynamic hooks with single query
Build and Push Docker Image / build (push) Failing after 8s Details

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 <noreply@anthropic.com>
This commit is contained in:
Matt 2026-02-16 15:30:44 +01:00
parent 65a22e6f19
commit f12c29103c
2 changed files with 20 additions and 7 deletions

View File

@ -105,14 +105,13 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
// Extract all rounds from the competition // Extract all rounds from the competition
const competitionRounds = competition?.rounds || [] const competitionRounds = competition?.rounds || []
// Fetch requirements for each round // Fetch requirements for all rounds in a single query (avoids dynamic hook violation)
const requirementQueries = competitionRounds.map((round: { id: string; name: string }) => const roundIds = competitionRounds.map((r: { id: string }) => r.id)
trpc.file.listRequirements.useQuery({ roundId: round.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() const utils = trpc.useUtils()
if (isLoading) { if (isLoading) {
@ -592,7 +591,7 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
</p> </p>
)} )}
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-0.5"> <div className="flex items-center gap-2 text-xs text-muted-foreground mt-0.5">
{req.acceptedMimeTypes.length > 0 && ( {req.acceptedMimeTypes?.length > 0 && (
<span> <span>
{req.acceptedMimeTypes.map((mime: string) => { {req.acceptedMimeTypes.map((mime: string) => {
if (mime === 'application/pdf') return 'PDF' if (mime === 'application/pdf') return 'PDF'

View File

@ -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) * Create a file requirement for a stage (admin only)
*/ */