Skip to content

Commit 5590bf7

Browse files
[WEB-5772] fix: theme switch flicker (#8428)
1 parent 70eea50 commit 5590bf7

File tree

1 file changed

+59
-16
lines changed

1 file changed

+59
-16
lines changed

apps/web/core/lib/wrappers/store-wrapper.tsx

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ReactNode } from "react";
2-
import { useEffect } from "react";
2+
import { useEffect, useRef } from "react";
33
import { observer } from "mobx-react";
44
import { useParams } from "next/navigation";
55
import { useTheme } from "next-themes";
@@ -28,6 +28,13 @@ function StoreWrapper(props: TStoreWrapper) {
2828
const { data: userProfile } = useUserProfile();
2929
const { changeLanguage } = useTranslation();
3030

31+
// Track if we've initialized theme from server (one-time only)
32+
const hasInitializedThemeRef = useRef(false);
33+
// Track current user to reset on logout/login
34+
const currentUserIdRef = useRef<string | undefined>(undefined);
35+
// Track previous theme to detect transitions from custom theme
36+
const previousThemeRef = useRef<string | undefined>(undefined);
37+
3138
/**
3239
* Sidebar collapsed fetching from local storage
3340
*/
@@ -38,25 +45,61 @@ function StoreWrapper(props: TStoreWrapper) {
3845
}, [sidebarCollapsed, setTheme, toggleSidebar]);
3946

4047
/**
41-
* Setting up the theme of the user by fetching it from profile
48+
* Effect 1: Initial theme sync from server (one-time only)
49+
*
50+
* This effect runs ONCE per user session to load theme from server.
51+
* After initial load, all theme changes are localStorage-driven (next-themes).
52+
* This prevents a feedback loop where server updates trigger UI updates in a cycle.
53+
*/
54+
useEffect(() => {
55+
const userId = userProfile?.id;
56+
57+
// Reset initialization flag when user changes (logout/login)
58+
// This handles both logout (userId becomes undefined) and login (userId changes)
59+
if (userId !== currentUserIdRef.current) {
60+
hasInitializedThemeRef.current = false;
61+
previousThemeRef.current = undefined;
62+
currentUserIdRef.current = userId;
63+
}
64+
65+
// Only initialize theme from server on FIRST load for this user
66+
if (!userProfile?.theme?.theme || hasInitializedThemeRef.current) {
67+
return; // Skip if already initialized or no profile data
68+
}
69+
70+
// Apply theme from server profile (one-time only)
71+
setTheme(userProfile?.theme?.theme || "system");
72+
73+
// Mark as initialized - prevents future syncs from server
74+
hasInitializedThemeRef.current = true;
75+
}, [userProfile?.theme?.theme, setTheme]);
76+
77+
/**
78+
* Effect 2: Custom theme CSS application (runs on every change)
79+
*
80+
* This effect applies or clears custom theme CSS variables whenever
81+
* the theme changes. It runs independently of the initial sync effect.
4282
*/
4383
useEffect(() => {
4484
if (!userProfile?.theme?.theme) return;
45-
const currentTheme = userProfile?.theme?.theme || "system";
46-
const theme = userProfile?.theme;
47-
48-
if (currentTheme) {
49-
setTheme(currentTheme);
50-
if (currentTheme === "custom") {
51-
// New 2-color palette system
52-
if (theme.primary && theme.background && theme.darkPalette !== undefined) {
53-
applyCustomTheme(theme.primary, theme.background, theme.darkPalette ? "dark" : "light");
54-
}
55-
} else {
56-
clearCustomTheme();
57-
}
85+
86+
const currentTheme = userProfile?.theme?.theme;
87+
const previousTheme = previousThemeRef.current;
88+
const themeData = userProfile?.theme;
89+
90+
// Apply custom theme if current theme is custom
91+
if (currentTheme === "custom" && themeData.primary && themeData.background && themeData.darkPalette !== undefined) {
92+
applyCustomTheme(themeData.primary, themeData.background, themeData.darkPalette ? "dark" : "light");
93+
}
94+
// Clear custom theme CSS when switching away from custom
95+
else if (previousTheme === "custom" && currentTheme !== "custom") {
96+
clearCustomTheme();
97+
// No reload needed - let CSS cascade handle it naturally
5898
}
59-
}, [userProfile?.theme, setTheme]);
99+
100+
// Update previous theme for next comparison
101+
previousThemeRef.current = currentTheme;
102+
}, [userProfile?.theme]);
60103

61104
useEffect(() => {
62105
if (!userProfile?.language) return;

0 commit comments

Comments
 (0)