diff --git a/apps/web/core/lib/wrappers/store-wrapper.tsx b/apps/web/core/lib/wrappers/store-wrapper.tsx index 69cc819a5ca..5dad12826c4 100644 --- a/apps/web/core/lib/wrappers/store-wrapper.tsx +++ b/apps/web/core/lib/wrappers/store-wrapper.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from "react"; -import { useEffect } from "react"; +import { useEffect, useRef } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import { useTheme } from "next-themes"; @@ -28,6 +28,13 @@ function StoreWrapper(props: TStoreWrapper) { const { data: userProfile } = useUserProfile(); const { changeLanguage } = useTranslation(); + // Track if we've initialized theme from server (one-time only) + const hasInitializedThemeRef = useRef(false); + // Track current user to reset on logout/login + const currentUserIdRef = useRef(undefined); + // Track previous theme to detect transitions from custom theme + const previousThemeRef = useRef(undefined); + /** * Sidebar collapsed fetching from local storage */ @@ -38,25 +45,61 @@ function StoreWrapper(props: TStoreWrapper) { }, [sidebarCollapsed, setTheme, toggleSidebar]); /** - * Setting up the theme of the user by fetching it from profile + * Effect 1: Initial theme sync from server (one-time only) + * + * This effect runs ONCE per user session to load theme from server. + * After initial load, all theme changes are localStorage-driven (next-themes). + * This prevents a feedback loop where server updates trigger UI updates in a cycle. + */ + useEffect(() => { + const userId = userProfile?.id; + + // Reset initialization flag when user changes (logout/login) + // This handles both logout (userId becomes undefined) and login (userId changes) + if (userId !== currentUserIdRef.current) { + hasInitializedThemeRef.current = false; + previousThemeRef.current = undefined; + currentUserIdRef.current = userId; + } + + // Only initialize theme from server on FIRST load for this user + if (!userProfile?.theme?.theme || hasInitializedThemeRef.current) { + return; // Skip if already initialized or no profile data + } + + // Apply theme from server profile (one-time only) + setTheme(userProfile?.theme?.theme || "system"); + + // Mark as initialized - prevents future syncs from server + hasInitializedThemeRef.current = true; + }, [userProfile?.theme?.theme, setTheme]); + + /** + * Effect 2: Custom theme CSS application (runs on every change) + * + * This effect applies or clears custom theme CSS variables whenever + * the theme changes. It runs independently of the initial sync effect. */ useEffect(() => { if (!userProfile?.theme?.theme) return; - const currentTheme = userProfile?.theme?.theme || "system"; - const theme = userProfile?.theme; - - if (currentTheme) { - setTheme(currentTheme); - if (currentTheme === "custom") { - // New 2-color palette system - if (theme.primary && theme.background && theme.darkPalette !== undefined) { - applyCustomTheme(theme.primary, theme.background, theme.darkPalette ? "dark" : "light"); - } - } else { - clearCustomTheme(); - } + + const currentTheme = userProfile?.theme?.theme; + const previousTheme = previousThemeRef.current; + const themeData = userProfile?.theme; + + // Apply custom theme if current theme is custom + if (currentTheme === "custom" && themeData.primary && themeData.background && themeData.darkPalette !== undefined) { + applyCustomTheme(themeData.primary, themeData.background, themeData.darkPalette ? "dark" : "light"); + } + // Clear custom theme CSS when switching away from custom + else if (previousTheme === "custom" && currentTheme !== "custom") { + clearCustomTheme(); + // No reload needed - let CSS cascade handle it naturally } - }, [userProfile?.theme, setTheme]); + + // Update previous theme for next comparison + previousThemeRef.current = currentTheme; + }, [userProfile?.theme]); useEffect(() => { if (!userProfile?.language) return;