From 0ca864ff20e9017c352f790cd51c1cdba6e12611 Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 30 Mar 2026 19:18:23 -0400 Subject: [PATCH] fix: clear setTimeout ref on unmount, stable roadmapIdKey dep for votes effect --- components/ChangelogTabs.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/components/ChangelogTabs.tsx b/components/ChangelogTabs.tsx index a92d8f5..87ee996 100644 --- a/components/ChangelogTabs.tsx +++ b/components/ChangelogTabs.tsx @@ -1,6 +1,6 @@ 'use client' -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { PortableTextRenderer } from '@/components/PortableTextRenderer' import { RoadmapItem } from '@/components/RoadmapItem' import type { ChangelogEntry, RoadmapItemData, Tab, VoteState } from '@/types/changelog' @@ -48,15 +48,21 @@ export function ChangelogTabs({ const { user } = useAuth() const [showSignInPrompt, setShowSignInPrompt] = useState(false) + const signInTimerRef = useRef | null>(null) const [votesFetched, setVotesFetched] = useState(false) + // Clear sign-in prompt timer on unmount + useEffect(() => () => { if (signInTimerRef.current) clearTimeout(signInTimerRef.current) }, []) + + // Stable key for roadmapItems identity — avoids unstable array reference in deps + const roadmapIdKey = roadmapItems.map((i) => i._id).join(',') + // Fetch vote state when the What's Next tab is first viewed useEffect(() => { - if (activeTab !== 'whats-next' || votesFetched || roadmapItems.length === 0) return + if (activeTab !== 'whats-next' || votesFetched || !roadmapIdKey) return setVotesFetched(true) - const ids = roadmapItems.map((i) => i._id).join(',') - fetch(`/api/roadmap/votes?ids=${encodeURIComponent(ids)}`, { + fetch(`/api/roadmap/votes?ids=${encodeURIComponent(roadmapIdKey)}`, { credentials: 'include', }) .then((r) => (r.ok ? r.json() : {})) @@ -72,12 +78,13 @@ export function ChangelogTabs({ .catch(() => { // Spring Boot not yet available — silently keep count=0 }) - }, [activeTab, votesFetched, roadmapItems]) + }, [activeTab, votesFetched, roadmapIdKey]) // eslint-disable-line react-hooks/exhaustive-deps async function handleVote(id: string) { if (!user) { + if (signInTimerRef.current) clearTimeout(signInTimerRef.current) setShowSignInPrompt(true) - setTimeout(() => setShowSignInPrompt(false), 4000) + signInTimerRef.current = setTimeout(() => setShowSignInPrompt(false), 4000) return }