From 9cfd17ef46c8be64f17ea11ffc411a96b83eb645 Mon Sep 17 00:00:00 2001 From: R-Hart80 Date: Thu, 14 May 2026 11:23:28 -0300 Subject: [PATCH] fix: eliminate setState-in-effect anti-patterns (Category D, issue #200) Replace four direct setState calls inside useEffect bodies with lazy initialisers and derived state: - EntityTreeToolbar: useState lazy init reads localStorage instead of setting state in a mount effect; removes the useEffect entirely. - app/auth/error: move auto-retry call into a setTimeout callback so it is asynchronous; removes the redundant setRetryCount that duplicated what retry() already does. - app/settings: useState lazy init reads window.location.hash so the highlight value is available on the first render without a setState-in-effect. - app/page: replace authDefaultApplied ref + useEffect with a derived filter (null-coalesce on userFilter) so authenticated users default to "mine" without an extra render cycle. Co-Authored-By: Claude Sonnet 4.6 --- app/auth/error/page.tsx | 5 ++--- app/page.tsx | 13 +++---------- app/settings/page.tsx | 11 ++++++----- components/editor/shared/EntityTreeToolbar.tsx | 14 +++++--------- 4 files changed, 16 insertions(+), 27 deletions(-) diff --git a/app/auth/error/page.tsx b/app/auth/error/page.tsx index b892b235..7ed748be 100644 --- a/app/auth/error/page.tsx +++ b/app/auth/error/page.tsx @@ -36,9 +36,8 @@ function ErrorContent() { useEffect(() => { if (!isTransient || retryCount >= MAX_RETRIES || retrying) return; if (countdown <= 0) { - setRetryCount((c) => c + 1); - retry(); - return; + const id = setTimeout(() => retry(), 0); + return () => clearTimeout(id); } const timer = setTimeout(() => setCountdown((c) => c - 1), 1000); return () => clearTimeout(timer); diff --git a/app/page.tsx b/app/page.tsx index a2817afb..3eeffa99 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -17,20 +17,13 @@ type FilterType = "public" | "private" | "mine" | "all"; export default function HomePage() { const { data: session, status } = useSession(); - const [filter, setFilter] = useState("public"); + const [userFilter, setUserFilter] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [debouncedSearch, setDebouncedSearch] = useState(""); const isAuthenticated = status === "authenticated"; - - // Default authenticated users to "mine" tab - const authDefaultApplied = useRef(false); - useEffect(() => { - if (!authDefaultApplied.current && status !== "loading") { - authDefaultApplied.current = true; - if (isAuthenticated) setFilter("mine"); - } - }, [status, isAuthenticated]); + const filter = userFilter ?? (status !== "loading" && isAuthenticated ? "mine" : "public"); + const setFilter = useCallback((f: FilterType) => setUserFilter(f), []); // Debounce search input const debounceRef = useRef | null>(null); diff --git a/app/settings/page.tsx b/app/settings/page.tsx index e43cba5f..ff9cba65 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -330,16 +330,17 @@ function EditorPreferencesSection() { const setTheme = useEditorModeStore((s) => s.setTheme); const preferEditMode = useEditorModeStore((s) => s.preferEditMode); const setPreferEditMode = useEditorModeStore((s) => s.setPreferEditMode); - const [highlightedSetting, setHighlightedSetting] = useState(null); + const [highlightedSetting, setHighlightedSetting] = useState(() => { + if (typeof window === "undefined") return null; + return window.location.hash.slice(1) || null; + }); - // Highlight and scroll to the setting referenced by the URL hash + // Scroll to the setting referenced by the URL hash and clear the highlight after 2 s useEffect(() => { const hash = window.location.hash.slice(1); if (!hash) return; const el = document.getElementById(hash); - if (!el) return; - el.scrollIntoView({ behavior: "smooth", block: "center" }); - setHighlightedSetting(hash); + if (el) el.scrollIntoView({ behavior: "smooth", block: "center" }); const timer = setTimeout(() => setHighlightedSetting(null), 2000); return () => clearTimeout(timer); }, []); diff --git a/components/editor/shared/EntityTreeToolbar.tsx b/components/editor/shared/EntityTreeToolbar.tsx index 234dec7c..c053bf40 100644 --- a/components/editor/shared/EntityTreeToolbar.tsx +++ b/components/editor/shared/EntityTreeToolbar.tsx @@ -1,6 +1,6 @@ "use client"; -import { useRef, useCallback, useState, useEffect } from "react"; +import { useRef, useCallback, useState } from "react"; import { Search, X, Plus, ChevronDown, ChevronsDown, ChevronRight, ChevronsRight, Loader2 } from "lucide-react"; import { cn } from "@/lib/utils"; @@ -44,17 +44,13 @@ export function EntityTreeToolbar({ const internalRef = useRef(null); const inputRef = searchInputRef || internalRef; - // Dismissible tip - const [showTip, setShowTip] = useState(false); - useEffect(() => { + const [showTip, setShowTip] = useState(() => { try { - if (!localStorage.getItem(TIP_DISMISSED_KEY)) { - setShowTip(true); - } + return !localStorage.getItem(TIP_DISMISSED_KEY); } catch { - // localStorage unavailable + return false; } - }, []); + }); const dismissTip = useCallback(() => { setShowTip(false);