From 0bbd26172def94addf40839d4ca43d8fc23ec17e Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Tue, 23 Dec 2025 14:15:48 +0530 Subject: [PATCH 1/3] fix: theme switch flicker --- apps/web/core/lib/wrappers/store-wrapper.tsx | 73 +++++++++++++++----- 1 file changed, 57 insertions(+), 16 deletions(-) diff --git a/apps/web/core/lib/wrappers/store-wrapper.tsx b/apps/web/core/lib/wrappers/store-wrapper.tsx index 69cc819a5ca..402055731bb 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,59 @@ 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) + if (userId && userId !== currentUserIdRef.current) { + hasInitializedThemeRef.current = false; + 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, userProfile?.id, 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; From 79002b0f3fae3f5e26b5f703583bb520b4df9c79 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Tue, 23 Dec 2025 14:24:49 +0530 Subject: [PATCH 2/3] chore: code refactor --- apps/web/core/lib/wrappers/store-wrapper.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/web/core/lib/wrappers/store-wrapper.tsx b/apps/web/core/lib/wrappers/store-wrapper.tsx index 402055731bb..381b55de8bb 100644 --- a/apps/web/core/lib/wrappers/store-wrapper.tsx +++ b/apps/web/core/lib/wrappers/store-wrapper.tsx @@ -57,6 +57,7 @@ function StoreWrapper(props: TStoreWrapper) { // Reset initialization flag when user changes (logout/login) if (userId && userId !== currentUserIdRef.current) { hasInitializedThemeRef.current = false; + previousThemeRef.current = undefined; currentUserIdRef.current = userId; } @@ -70,7 +71,7 @@ function StoreWrapper(props: TStoreWrapper) { // Mark as initialized - prevents future syncs from server hasInitializedThemeRef.current = true; - }, [userProfile?.theme, userProfile?.id, setTheme]); + }, [userProfile?.theme?.theme, userProfile?.id, setTheme]); /** * Effect 2: Custom theme CSS application (runs on every change) From 511639a7c72950e09eb21c41f0e2c9700c620e4c Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Tue, 23 Dec 2025 14:29:01 +0530 Subject: [PATCH 3/3] chore: code refactor --- apps/web/core/lib/wrappers/store-wrapper.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/web/core/lib/wrappers/store-wrapper.tsx b/apps/web/core/lib/wrappers/store-wrapper.tsx index 381b55de8bb..5dad12826c4 100644 --- a/apps/web/core/lib/wrappers/store-wrapper.tsx +++ b/apps/web/core/lib/wrappers/store-wrapper.tsx @@ -55,7 +55,8 @@ function StoreWrapper(props: TStoreWrapper) { const userId = userProfile?.id; // Reset initialization flag when user changes (logout/login) - if (userId && userId !== currentUserIdRef.current) { + // This handles both logout (userId becomes undefined) and login (userId changes) + if (userId !== currentUserIdRef.current) { hasInitializedThemeRef.current = false; previousThemeRef.current = undefined; currentUserIdRef.current = userId; @@ -71,7 +72,7 @@ function StoreWrapper(props: TStoreWrapper) { // Mark as initialized - prevents future syncs from server hasInitializedThemeRef.current = true; - }, [userProfile?.theme?.theme, userProfile?.id, setTheme]); + }, [userProfile?.theme?.theme, setTheme]); /** * Effect 2: Custom theme CSS application (runs on every change)