Skip to content

Commit 42eab57

Browse files
committed
chore: restore original onRenderBody structure and apply precise FOUC fixes with comments
Signed-off-by: Ankit Rewar <AnkitRewar11@users.noreply.github.com>
1 parent 9ad62cb commit 42eab57

File tree

3 files changed

+82
-67
lines changed

3 files changed

+82
-67
lines changed

onRenderBody.js

Lines changed: 72 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,81 @@
11
import React from "react";
2-
import { DarkThemeKey, ThemeSetting } from "./src/theme/app/ThemeManager.js";
2+
import { DarkThemeKey, ThemeSetting } from "./src/theme/app/ThemeManager";
33
import lighttheme, { darktheme } from "./src/theme/app/themeStyles";
44

5-
const themes = { light: lighttheme, dark: darktheme };
5+
const themes = {
6+
light: lighttheme,
7+
dark: darktheme,
8+
};
9+
10+
const MagicScriptTag = ({ theme }) => {
11+
const themeJSON = JSON.stringify(theme);
612

7-
const MagicScriptTag = (props) => {
8-
// Injects CSS variables before first paint. The React hydration mismatch
9-
// is resolved downstream in ThemeManager.js using window.__theme.
13+
// Injects CSS variables and theme state before the first paint to prevent FOUC.
1014
const codeToRunOnClient = `
11-
(function() {
12-
// 1. Keeps SYSTEM as the priority preference
13-
const themeFromLocalStorage = localStorage.getItem('${DarkThemeKey}') || '${ThemeSetting.SYSTEM}';
14-
15-
// 2. We change the check to look for LIGHT mode explicitly
16-
const systemLightModeSetting = () => window.matchMedia ? window.matchMedia('(prefers-color-scheme: light)') : null;
17-
18-
const isLightModeActive = () => {
19-
return !!systemLightModeSetting()?.matches;
20-
};
21-
22-
let colorMode;
23-
switch (themeFromLocalStorage) {
24-
case '${ThemeSetting.SYSTEM}':
25-
// LOGIC CHANGE: If Light is active -> Light. Otherwise (Dark, No Preference, or Error) -> Dark.
26-
colorMode = isLightModeActive() ? '${ThemeSetting.LIGHT}' : '${ThemeSetting.DARK}'
27-
break
28-
case '${ThemeSetting.DARK}':
29-
case '${ThemeSetting.LIGHT}':
30-
colorMode = themeFromLocalStorage
31-
break
32-
default:
33-
// 3. Fallback to DARK in case of error
34-
colorMode = '${ThemeSetting.DARK}'
35-
}
36-
37-
const root = document.documentElement;
38-
const iterate = (obj) => {
39-
if (!obj) return;
40-
Object.keys(obj).forEach(key => {
41-
if (typeof obj[key] === 'object') {
42-
iterate(obj[key])
43-
} else {
44-
root.style.setProperty("--" + key, obj[key])
45-
}
46-
})
47-
}
48-
const parsedTheme = JSON.parse('${JSON.stringify(props.theme)}')
49-
const theme = parsedTheme[colorMode]
50-
iterate(theme)
51-
root.style.setProperty('--initial-color-mode', colorMode);
52-
window.__theme = colorMode;
53-
})()
54-
`;
15+
(function() {
16+
// 1. Keeps SYSTEM as the priority preference
17+
try {
18+
var themeFromLocalStorage = localStorage.getItem('${DarkThemeKey}') || '${ThemeSetting.SYSTEM}';
19+
20+
// 2. We change the check to look for LIGHT mode explicitly
21+
var systemLightModeSetting = function() {
22+
return window.matchMedia ? window.matchMedia('(prefers-color-scheme: light)') : null;
23+
};
24+
var isLightModeActive = function() {
25+
var mql = systemLightModeSetting();
26+
return mql ? mql.matches : false;
27+
};
28+
29+
var colorMode;
30+
switch (themeFromLocalStorage) {
31+
case '${ThemeSetting.SYSTEM}':
32+
// LOGIC CHANGE: If Light is active -> Light. Otherwise (Dark, No Preference, or Error) -> Dark.
33+
colorMode = isLightModeActive() ? '${ThemeSetting.LIGHT}' : '${ThemeSetting.DARK}';
34+
break;
35+
case '${ThemeSetting.DARK}':
36+
case '${ThemeSetting.LIGHT}':
37+
colorMode = themeFromLocalStorage;
38+
break;
39+
default:
40+
// 3. Fallback to DARK in case of error
41+
colorMode = '${ThemeSetting.DARK}';
42+
}
43+
44+
var root = document.documentElement;
45+
var parsedTheme = ${themeJSON};
46+
var selectedTheme = parsedTheme[colorMode];
47+
48+
var iterate = function(obj) {
49+
if (!obj) return;
50+
Object.keys(obj).forEach(function(key) {
51+
if (typeof obj[key] === 'object' && obj[key] !== null) {
52+
iterate(obj[key]);
53+
} else {
54+
root.style.setProperty('--' + key, obj[key]);
55+
}
56+
});
57+
};
58+
59+
iterate(selectedTheme);
60+
root.style.setProperty('--initial-color-mode', colorMode);
61+
62+
// FIX: Setting data-theme is required for global CSS styles to apply correctly before React hydration.
63+
root.setAttribute('data-theme', colorMode);
64+
65+
// Sync the calculated theme globally so ThemeManager can pick it up immediately during hydration.
66+
window.__theme = colorMode;
67+
68+
} catch (e) {}
69+
})();
70+
`;
71+
5572
return <script dangerouslySetInnerHTML={{ __html: codeToRunOnClient }} />;
5673
};
5774

58-
export const onRenderBody = ( { setPreBodyComponents }) => {
59-
setPreBodyComponents(<MagicScriptTag key="theme-injection" theme={themes} />);
75+
// FIX: Using setHeadComponents instead of setPreBodyComponents ensures the script runs
76+
// in the <head>, blocking the first paint until the theme is applied and completely eliminating FOUC.
77+
export const onRenderBody = ({ setHeadComponents }) => {
78+
setHeadComponents([
79+
<MagicScriptTag key="theme-initializer" theme={themes} />,
80+
]);
6081
};

src/theme/app/StyledThemeProvider.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ export const StyledThemeProvider = (props) => {
1212
const { children, darkTheme, lightTheme } = props;
1313
const { isDark, didLoad } = useContext(ThemeManagerContext);
1414

15+
// For SSR, we need to provide a consistent theme initially
16+
// This ensures the server and client render the same thing initially
1517
const currentTheme = isDark ? darkTheme : lightTheme;
16-
17-
// Fallback to SSR-injected CSS variables during hydration to prevent FOUC.
1818
const theme = {
1919
...(didLoad ? currentTheme : transformTheme(currentTheme)),
2020
};
@@ -26,7 +26,6 @@ export const StyledThemeProvider = (props) => {
2626
);
2727
};
2828

29-
// Maps JS theme object to raw CSS variables (--key) initialized via onRenderBody.
3029
const transformTheme = (theme) => {
3130
const newTheme = {};
3231
Object.keys(theme).forEach((key) => {

src/theme/app/ThemeManager.js

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
// majority of code from https://www.joshwcomeau.com/react/dark-mode/ and https://github.com/gperl27/gatsby-styled-components-dark-mode
2-
// context provider for app to make accessible theme setting, toggle function, etc.
1+
//majority of code from https://www.joshwcomeau.com/react/dark-mode/ and https://github.com/gperl27/gatsby-styled-components-dark-mode
2+
3+
//context provider for app to make accessible theme setting, toggle function, etc.
34

45
import React, { createContext, useState, useEffect, useCallback } from "react";
56

@@ -36,16 +37,13 @@ const applyThemeToDOM = (theme) => {
3637
const root = window.document.documentElement;
3738
root.style.setProperty("--initial-color-mode", theme);
3839
root.setAttribute("data-theme", theme);
39-
40-
// Sync with SSR injected state
4140
window.__theme = theme;
4241
};
4342

4443
export const ThemeManagerProvider = (props) => {
4544
const [themeSetting, setThemeSetting] = useState(ThemeSetting.SYSTEM);
4645
const [didLoad, setDidLoad] = useState(false);
4746

48-
// Initialize state from SSR script to prevent hydration mismatch
4947
const [isDark, setIsDark] = useState(() => {
5048
if (isBrowser) {
5149
if (window.__theme === ThemeSetting.DARK) return true;
@@ -59,11 +57,8 @@ export const ThemeManagerProvider = (props) => {
5957

6058
const root = window.document.documentElement;
6159
const initialColorValue = (root.style.getPropertyValue("--initial-color-mode") || "").trim();
62-
63-
// Prioritize SSR-injected theme
6460
const actualTheme = window.__theme || initialColorValue || ThemeSetting.LIGHT;
6561

66-
// Get stored theme from localStorage
6762
const storedTheme = localStorage.getItem(DarkThemeKey);
6863

6964
if (storedTheme && storedTheme !== ThemeSetting.SYSTEM) {
@@ -85,7 +80,7 @@ export const ThemeManagerProvider = (props) => {
8580
setDidLoad(true);
8681
}, []);
8782

88-
// Listen to system color scheme changes only when on SYSTEM mode
83+
// Listen to system color scheme changes only when on SYSTEM mode
8984
useEffect(() => {
9085
if (!isBrowser || themeSetting !== ThemeSetting.SYSTEM) return;
9186

@@ -107,11 +102,11 @@ export const ThemeManagerProvider = (props) => {
107102
const newIsDark = !isDark;
108103
const newTheme = newIsDark ? ThemeSetting.DARK : ThemeSetting.LIGHT;
109104

110-
// Update state
105+
// Update state
111106
setIsDark(newIsDark);
112107
setThemeSetting(newTheme);
113108

114-
// Apply to DOM immediately
109+
// Apply to DOM immediately
115110
applyThemeToDOM(newTheme);
116111

117112
// Persist to localStorage
@@ -143,14 +138,14 @@ export const ThemeManagerProvider = (props) => {
143138
return;
144139
}
145140

146-
// Update state
141+
// Update state
147142
setIsDark(newIsDark);
148143
setThemeSetting(setting);
149144

150145
// Apply to DOM immediately
151146
applyThemeToDOM(themeToApply);
152147

153-
// Persist to localStorage
148+
// Persist to localStorage
154149
localStorage.setItem(DarkThemeKey, setting);
155150
},
156151
[isDark]

0 commit comments

Comments
 (0)