From 4d4062985d5b4b90298bf369d3e25861f14e7db7 Mon Sep 17 00:00:00 2001 From: goddonebianu Date: Wed, 11 Mar 2026 09:51:25 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20React=20Compiler=20?= =?UTF-8?q?=E4=B8=A5=E6=A0=BC=E6=A8=A1=E5=BC=8F=E4=B8=8B=E7=9A=84=20lint?= =?UTF-8?q?=20=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Memorize: 将 loadCards 内联到 useEffect 中避免变量提升问题 - DecksClient: 修复 effect 中异步加载,创建 deck 后使用 actionGetDeckById - LanguageSettings: 使用 effect 设置 cookie 避免 render 期间修改 - theme-provider: 修复 hydration 逻辑避免 render 期间访问 ref --- src/app/(features)/memorize/Memorize.tsx | 42 +++++++++++++--------- src/app/decks/DecksClient.tsx | 34 ++++++++++++------ src/components/layout/LanguageSettings.tsx | 17 ++++++--- src/components/theme-provider.tsx | 27 +++++++------- 4 files changed, 75 insertions(+), 45 deletions(-) diff --git a/src/app/(features)/memorize/Memorize.tsx b/src/app/(features)/memorize/Memorize.tsx index a11b0f7..6ba561f 100644 --- a/src/app/(features)/memorize/Memorize.tsx +++ b/src/app/(features)/memorize/Memorize.tsx @@ -34,26 +34,34 @@ const Memorize: React.FC = ({ deckId, deckName }) => { const [error, setError] = useState(null); useEffect(() => { + let ignore = false; + + const loadCards = async () => { + setIsLoading(true); + setError(null); + startTransition(async () => { + const result = await actionGetCardsForReview({ deckId, limit: 50 }); + if (!ignore) { + if (result.success && result.data) { + setCards(result.data); + setCurrentIndex(0); + setShowAnswer(false); + setLastScheduled(null); + } else { + setError(result.message); + } + setIsLoading(false); + } + }); + }; + loadCards(); + + return () => { + ignore = true; + }; }, [deckId]); - const loadCards = () => { - setIsLoading(true); - setError(null); - startTransition(async () => { - const result = await actionGetCardsForReview({ deckId, limit: 50 }); - if (result.success && result.data) { - setCards(result.data); - setCurrentIndex(0); - setShowAnswer(false); - setLastScheduled(null); - } else { - setError(result.message); - } - setIsLoading(false); - }); - }; - const getCurrentCard = (): ActionOutputCardWithNote | null => { return cards[currentIndex] ?? null; }; diff --git a/src/app/decks/DecksClient.tsx b/src/app/decks/DecksClient.tsx index 9551e29..c6bf01b 100644 --- a/src/app/decks/DecksClient.tsx +++ b/src/app/decks/DecksClient.tsx @@ -22,6 +22,7 @@ import { actionDeleteDeck, actionGetDecksByUserId, actionUpdateDeck, + actionGetDeckById, } from "@/modules/deck/deck-action"; import type { ActionOutputDeck } from "@/modules/deck/deck-action-dto"; @@ -148,17 +149,25 @@ export function DecksClient({ userId }: DecksClientProps) { const [decks, setDecks] = useState([]); const [loading, setLoading] = useState(true); - const loadDecks = async () => { - setLoading(true); - const result = await actionGetDecksByUserId(userId); - if (result.success && result.data) { - setDecks(result.data); - } - setLoading(false); - }; - useEffect(() => { + let ignore = false; + + const loadDecks = async () => { + setLoading(true); + const result = await actionGetDecksByUserId(userId); + if (!ignore) { + if (result.success && result.data) { + setDecks(result.data); + } + setLoading(false); + } + }; + loadDecks(); + + return () => { + ignore = true; + }; }, [userId]); const handleUpdateDeck = (deckId: number, updates: Partial) => { @@ -176,8 +185,11 @@ export function DecksClient({ userId }: DecksClientProps) { if (!deckName?.trim()) return; const result = await actionCreateDeck({ name: deckName.trim() }); - if (result.success) { - loadDecks(); + if (result.success && result.deckId) { + const deckResult = await actionGetDeckById({ deckId: result.deckId }); + if (deckResult.success && deckResult.data) { + setDecks((prev) => [...prev, deckResult.data!]); + } } else { toast.error(result.message); } diff --git a/src/components/layout/LanguageSettings.tsx b/src/components/layout/LanguageSettings.tsx index 4db8227..fd0f9b6 100644 --- a/src/components/layout/LanguageSettings.tsx +++ b/src/components/layout/LanguageSettings.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect, useRef, useCallback } from "react"; import { Languages } from "lucide-react"; import { cn } from "@/utils/cn"; @@ -17,6 +17,7 @@ const languages = [ export function LanguageSettings() { const [isOpen, setIsOpen] = useState(false); + const [pendingLocale, setPendingLocale] = useState(null); const menuRef = useRef(null); useEffect(() => { @@ -46,10 +47,16 @@ export function LanguageSettings() { } }, [isOpen]); - const setLocale = async (locale: string) => { - document.cookie = `locale=${locale}`; - window.location.reload(); - }; + useEffect(() => { + if (pendingLocale) { + document.cookie = `locale=${pendingLocale}; path=/`; + window.location.reload(); + } + }, [pendingLocale]); + + const setLocale = useCallback((locale: string) => { + setPendingLocale(locale); + }, []); return (
diff --git a/src/components/theme-provider.tsx b/src/components/theme-provider.tsx index 84978ce..4a79b58 100644 --- a/src/components/theme-provider.tsx +++ b/src/components/theme-provider.tsx @@ -1,6 +1,6 @@ "use client"; -import { createContext, useContext, useEffect, useState } from "react"; +import { createContext, useContext, useEffect, useState, useMemo } from "react"; import { THEME_PRESETS, DEFAULT_THEME, @@ -20,26 +20,33 @@ const ThemeContext = createContext(null); const STORAGE_KEY = "theme-preset"; +function getInitialTheme(): string { + if (typeof window === "undefined") return DEFAULT_THEME; + const saved = localStorage.getItem(STORAGE_KEY); + return saved && getThemePreset(saved) ? saved : DEFAULT_THEME; +} + export function ThemeProvider({ children }: { children: React.ReactNode }) { const [currentTheme, setCurrentTheme] = useState(DEFAULT_THEME); - const [mounted, setMounted] = useState(false); + const [hydrated, setHydrated] = useState(false); useEffect(() => { - setMounted(true); - const savedTheme = localStorage.getItem(STORAGE_KEY); - if (savedTheme && getThemePreset(savedTheme)) { + const savedTheme = getInitialTheme(); + if (savedTheme !== currentTheme) { setCurrentTheme(savedTheme); } + setHydrated(true); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { - if (!mounted) return; + if (!hydrated) return; const preset = getThemePreset(currentTheme); if (preset) { applyThemeColors(preset); localStorage.setItem(STORAGE_KEY, currentTheme); } - }, [currentTheme, mounted]); + }, [currentTheme, hydrated]); const setTheme = (themeId: string) => { if (getThemePreset(themeId)) { @@ -47,11 +54,7 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) { } }; - const themePreset = getThemePreset(currentTheme) || THEME_PRESETS[0]; - - if (!mounted) { - return null; - } + const themePreset = useMemo(() => getThemePreset(currentTheme) || THEME_PRESETS[0], [currentTheme]); return (