Skip to content

Commit 67cd6fd

Browse files
committed
Merge branch 'main' into move-pydantic,-sqlmodel,-alembic-to-optional-dependencies
2 parents 876eeaa + 9d9ff9e commit 67cd6fd

File tree

15 files changed

+927
-134
lines changed

15 files changed

+927
-134
lines changed

reflex/.templates/web/utils/react-theme.js

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,18 @@ const ThemeContext = createContext({
1818

1919
export function ThemeProvider({ children, defaultTheme = "system" }) {
2020
const [theme, setTheme] = useState(defaultTheme);
21-
const [systemTheme, setSystemTheme] = useState(
22-
defaultTheme !== "system" ? defaultTheme : "light",
23-
);
21+
22+
// Detect system preference synchronously during initialization
23+
const getInitialSystemTheme = () => {
24+
if (defaultTheme !== "system") return defaultTheme;
25+
if (typeof window === "undefined") return "light";
26+
return window.matchMedia("(prefers-color-scheme: dark)").matches
27+
? "dark"
28+
: "light";
29+
};
30+
31+
const [systemTheme, setSystemTheme] = useState(getInitialSystemTheme);
32+
const [isInitialized, setIsInitialized] = useState(false);
2433

2534
const firstRender = useRef(true);
2635

@@ -43,6 +52,7 @@ export function ThemeProvider({ children, defaultTheme = "system" }) {
4352
// Load saved theme from localStorage
4453
const savedTheme = localStorage.getItem("theme") || defaultTheme;
4554
setTheme(savedTheme);
55+
setIsInitialized(true);
4656
});
4757

4858
const resolvedTheme = useMemo(
@@ -68,10 +78,12 @@ export function ThemeProvider({ children, defaultTheme = "system" }) {
6878
};
6979
});
7080

71-
// Save theme to localStorage whenever it changes
81+
// Save theme to localStorage whenever it changes (but not on initial mount)
7282
useEffect(() => {
73-
localStorage.setItem("theme", theme);
74-
}, [theme]);
83+
if (isInitialized) {
84+
localStorage.setItem("theme", theme);
85+
}
86+
}, [theme, isInitialized]);
7587

7688
useEffect(() => {
7789
const root = window.document.documentElement;

reflex/.templates/web/utils/state.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -791,9 +791,9 @@ export const hydrateClientStorage = (client_storage) => {
791791
for (const state_key in client_storage.cookies) {
792792
const cookie_options = client_storage.cookies[state_key];
793793
const cookie_name = cookie_options.name || state_key;
794-
const cookie_value = cookies.get(cookie_name);
794+
const cookie_value = cookies.get(cookie_name, { doNotParse: true });
795795
if (cookie_value !== undefined) {
796-
client_storage_values[state_key] = cookies.get(cookie_name);
796+
client_storage_values[state_key] = cookie_value;
797797
}
798798
}
799799
}

reflex/compiler/utils.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,8 @@ def create_document_root(
359359
Returns:
360360
The document root.
361361
"""
362+
from reflex.utils.misc import preload_color_theme
363+
362364
existing_meta_types = set()
363365

364366
for component in head_components or []:
@@ -385,7 +387,11 @@ def create_document_root(
385387
Meta.create(name="viewport", content="width=device-width, initial-scale=1")
386388
)
387389

390+
# Add theme preload script as the very first component to prevent FOUC
391+
theme_preload_components = [preload_color_theme()]
392+
388393
head_components = [
394+
*theme_preload_components,
389395
*(head_components or []),
390396
*maybe_head_components,
391397
*always_head_components,

reflex/utils/console.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import contextlib
6+
import datetime
67
import inspect
78
import os
89
import shutil
@@ -136,7 +137,7 @@ def print_to_log_file(msg: str, *, dedupe: bool = False, **kwargs):
136137
dedupe: If True, suppress multiple console logs of print message.
137138
kwargs: Keyword arguments to pass to the print function.
138139
"""
139-
log_file_console().print(msg, **kwargs)
140+
log_file_console().print(f"[{datetime.datetime.now()}] {msg}", **kwargs)
140141

141142

142143
def debug(msg: str, *, dedupe: bool = False, **kwargs):

reflex/utils/decorator.py

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import functools
44
from collections.abc import Callable
5-
from typing import ParamSpec, TypeVar
5+
from pathlib import Path
6+
from typing import ParamSpec, TypeVar, cast
67

78
T = TypeVar("T")
89

@@ -70,3 +71,77 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
7071
return result
7172

7273
return wrapper
74+
75+
76+
def _write_cached_procedure_file(payload: str, cache_file: Path, value: object):
77+
import pickle
78+
79+
cache_file.write_bytes(pickle.dumps((payload, value)))
80+
81+
82+
def _read_cached_procedure_file(cache_file: Path) -> tuple[str | None, object]:
83+
import pickle
84+
85+
if cache_file.exists():
86+
with cache_file.open("rb") as f:
87+
return pickle.loads(f.read())
88+
89+
return None, None
90+
91+
92+
P = ParamSpec("P")
93+
Picklable = TypeVar("Picklable")
94+
95+
96+
def cached_procedure(
97+
cache_file_path: Callable[[], Path],
98+
payload_fn: Callable[P, str],
99+
) -> Callable[[Callable[P, Picklable]], Callable[P, Picklable]]:
100+
"""Decorator to cache the result of a function based on its arguments.
101+
102+
Args:
103+
cache_file_path: Function that computes the cache file path.
104+
payload_fn: Function that computes cache payload from function args.
105+
106+
Returns:
107+
The decorated function.
108+
"""
109+
110+
def _inner_decorator(func: Callable[P, Picklable]) -> Callable[P, Picklable]:
111+
def _inner(*args: P.args, **kwargs: P.kwargs) -> Picklable:
112+
_cache_file = cache_file_path()
113+
114+
payload, value = _read_cached_procedure_file(_cache_file)
115+
new_payload = payload_fn(*args, **kwargs)
116+
117+
if payload != new_payload:
118+
new_value = func(*args, **kwargs)
119+
_write_cached_procedure_file(new_payload, _cache_file, new_value)
120+
return new_value
121+
122+
from reflex.utils import console
123+
124+
console.debug(
125+
f"Using cached value for {func.__name__} with payload: {new_payload}"
126+
)
127+
return cast("Picklable", value)
128+
129+
return _inner
130+
131+
return _inner_decorator
132+
133+
134+
def cache_result_in_disk(
135+
cache_file_path: Callable[[], Path],
136+
) -> Callable[[Callable[[], Picklable]], Callable[[], Picklable]]:
137+
"""Decorator to cache the result of a function on disk.
138+
139+
Args:
140+
cache_file_path: Function that computes the cache file path.
141+
142+
Returns:
143+
The decorated function.
144+
"""
145+
return cached_procedure(
146+
cache_file_path=cache_file_path, payload_fn=lambda: "constant"
147+
)

reflex/utils/misc.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,46 @@ def with_cwd_in_syspath():
9090
yield
9191
finally:
9292
sys.path[:] = orig_sys_path
93+
94+
95+
def preload_color_theme():
96+
"""Create a script component that preloads the color theme to prevent FOUC.
97+
98+
This script runs immediately in the document head before React hydration,
99+
reading the saved theme from localStorage and applying the correct CSS classes
100+
to prevent flash of unstyled content.
101+
102+
Returns:
103+
Script: A script component to add to App.head_components
104+
"""
105+
from reflex.components.el.elements.scripts import Script
106+
107+
# Create direct inline script content (like next-themes dangerouslySetInnerHTML)
108+
script_content = """
109+
// Only run in browser environment, not during SSR
110+
if (typeof document !== 'undefined') {
111+
try {
112+
const theme = localStorage.getItem("theme") || "system";
113+
const systemPreference = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
114+
const resolvedTheme = theme === "system" ? systemPreference : theme;
115+
116+
console.log("[PRELOAD] Theme applied:", resolvedTheme, "from theme:", theme, "system:", systemPreference);
117+
118+
// Apply theme immediately - blocks until complete
119+
// Use classList to avoid overwriting other classes
120+
document.documentElement.classList.remove("light", "dark");
121+
document.documentElement.classList.add(resolvedTheme);
122+
document.documentElement.style.colorScheme = resolvedTheme;
123+
124+
} catch (e) {
125+
// Fallback to system preference on any error (resolve "system" to actual theme)
126+
const fallbackTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
127+
console.log("[PRELOAD] Error, falling back to:", fallbackTheme);
128+
document.documentElement.classList.remove("light", "dark");
129+
document.documentElement.classList.add(fallbackTheme);
130+
document.documentElement.style.colorScheme = fallbackTheme;
131+
}
132+
}
133+
"""
134+
135+
return Script.create(script_content)

reflex/utils/prerequisites.py

Lines changed: 4 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import tempfile
2020
import typing
2121
import zipfile
22-
from collections.abc import Callable, Sequence
22+
from collections.abc import Sequence
2323
from datetime import datetime
2424
from pathlib import Path
2525
from types import ModuleType
@@ -37,6 +37,7 @@
3737
from reflex.config import Config, get_config
3838
from reflex.environment import environment
3939
from reflex.utils import console, net, path_ops, processes, redir
40+
from reflex.utils.decorator import cached_procedure
4041
from reflex.utils.exceptions import SystemPackageMissingError
4142
from reflex.utils.misc import get_module_path
4243
from reflex.utils.registry import get_npm_registry
@@ -1249,71 +1250,9 @@ def install_bun():
12491250
)
12501251

12511252

1252-
def _write_cached_procedure_file(payload: str, cache_file: str | Path):
1253-
cache_file = Path(cache_file)
1254-
cache_file.write_text(payload)
1255-
1256-
1257-
def _read_cached_procedure_file(cache_file: str | Path) -> str | None:
1258-
cache_file = Path(cache_file)
1259-
if cache_file.exists():
1260-
return cache_file.read_text()
1261-
return None
1262-
1263-
1264-
def _clear_cached_procedure_file(cache_file: str | Path):
1265-
cache_file = Path(cache_file)
1266-
if cache_file.exists():
1267-
cache_file.unlink()
1268-
1269-
1270-
def cached_procedure(
1271-
cache_file: str | None,
1272-
payload_fn: Callable[..., str],
1273-
cache_file_fn: Callable[[], str] | None = None,
1274-
):
1275-
"""Decorator to cache the runs of a procedure on disk. Procedures should not have
1276-
a return value.
1277-
1278-
Args:
1279-
cache_file: The file to store the cache payload in.
1280-
payload_fn: Function that computes cache payload from function args.
1281-
cache_file_fn: Function that computes the cache file name at runtime.
1282-
1283-
Returns:
1284-
The decorated function.
1285-
1286-
Raises:
1287-
ValueError: If both cache_file and cache_file_fn are provided.
1288-
"""
1289-
if cache_file and cache_file_fn is not None:
1290-
msg = "cache_file and cache_file_fn cannot both be provided."
1291-
raise ValueError(msg)
1292-
1293-
def _inner_decorator(func: Callable):
1294-
def _inner(*args, **kwargs):
1295-
_cache_file = cache_file_fn() if cache_file_fn is not None else cache_file
1296-
if not _cache_file:
1297-
msg = "Unknown cache file, cannot cache result."
1298-
raise ValueError(msg)
1299-
payload = _read_cached_procedure_file(_cache_file)
1300-
new_payload = payload_fn(*args, **kwargs)
1301-
if payload != new_payload:
1302-
_clear_cached_procedure_file(_cache_file)
1303-
func(*args, **kwargs)
1304-
_write_cached_procedure_file(new_payload, _cache_file)
1305-
1306-
return _inner
1307-
1308-
return _inner_decorator
1309-
1310-
13111253
@cached_procedure(
1312-
cache_file_fn=lambda: str(
1313-
get_web_dir() / "reflex.install_frontend_packages.cached"
1314-
),
1315-
payload_fn=lambda p, c: f"{sorted(p)!r},{c.json()}",
1316-
cache_file=None,
1254+
cache_file_path=lambda: get_web_dir() / "reflex.install_frontend_packages.cached",
1255+
payload_fn=lambda packages, config: f"{sorted(packages)!r},{config.json()}",
13171256
)
13181257
def install_frontend_packages(packages: set[str], config: Config):
13191258
"""Installs the base and custom frontend packages.

0 commit comments

Comments
 (0)