Skip to content

Commit 0bbd261

Browse files
fix: theme switch flicker
1 parent dbf84bf commit 0bbd261

File tree

1 file changed

+57
-16
lines changed

1 file changed

+57
-16
lines changed

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

Lines changed: 57 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,59 @@ 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+
if (userId && userId !== currentUserIdRef.current) {
59+
hasInitializedThemeRef.current = false;
60+
currentUserIdRef.current = userId;
61+
}
62+
63+
// Only initialize theme from server on FIRST load for this user
64+
if (!userProfile?.theme?.theme || hasInitializedThemeRef.current) {
65+
return; // Skip if already initialized or no profile data
66+
}
67+
68+
// Apply theme from server profile (one-time only)
69+
setTheme(userProfile?.theme?.theme || "system");
70+
71+
// Mark as initialized - prevents future syncs from server
72+
hasInitializedThemeRef.current = true;
73+
}, [userProfile?.theme, userProfile?.id, setTheme]);
74+
75+
/**
76+
* Effect 2: Custom theme CSS application (runs on every change)
77+
*
78+
* This effect applies or clears custom theme CSS variables whenever
79+
* the theme changes. It runs independently of the initial sync effect.
4280
*/
4381
useEffect(() => {
4482
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-
}
83+
84+
const currentTheme = userProfile?.theme?.theme;
85+
const previousTheme = previousThemeRef.current;
86+
const themeData = userProfile?.theme;
87+
88+
// Apply custom theme if current theme is custom
89+
if (currentTheme === "custom" && themeData.primary && themeData.background && themeData.darkPalette !== undefined) {
90+
applyCustomTheme(themeData.primary, themeData.background, themeData.darkPalette ? "dark" : "light");
91+
}
92+
// Clear custom theme CSS when switching away from custom
93+
else if (previousTheme === "custom" && currentTheme !== "custom") {
94+
clearCustomTheme();
95+
// No reload needed - let CSS cascade handle it naturally
5896
}
59-
}, [userProfile?.theme, setTheme]);
97+
98+
// Update previous theme for next comparison
99+
previousThemeRef.current = currentTheme;
100+
}, [userProfile?.theme]);
60101

61102
useEffect(() => {
62103
if (!userProfile?.language) return;

0 commit comments

Comments
 (0)