Skip to content

Commit e3e021e

Browse files
perf(web): useTheme compiles with the React Compiler
Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent 596e784 commit e3e021e

2 files changed

Lines changed: 136 additions & 139 deletions

File tree

‎apps/web/reactCompiledFiles.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,5 @@ export const REACT_COMPILED_FILES = [
4848
"apps/web/src/hooks/useThreadActions.ts",
4949
"apps/web/src/components/pullRequest/PullRequestCodeTab.tsx",
5050
"apps/web/src/hooks/useLocalStorage.ts",
51+
"apps/web/src/hooks/useTheme.ts",
5152
] as const;

‎apps/web/src/hooks/useTheme.ts‎

Lines changed: 135 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { DesktopBridge } from "@t3tools/contracts";
22
import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors";
33
import * as Schema from "effect/Schema";
4-
import { useCallback, useEffect, useSyncExternalStore } from "react";
4+
import { useEffect, useSyncExternalStore } from "react";
55
import {
66
applyThemePalette,
77
CUSTOM_THEMES_STORAGE_KEY,
@@ -492,157 +492,153 @@ function subscribe(listener: () => void): () => void {
492492
};
493493
}
494494

495-
export function useTheme() {
496-
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
497-
const { theme, resolvedTheme } = snapshot;
498-
499-
const setTheme = useCallback((next: Theme): boolean => {
500-
if (typeof window === "undefined") return false;
501-
try {
502-
// Preserve the current mode before replacing a legacy or inferred theme
503-
// preference. Otherwise a fresh System preference is re-inferred from
504-
// the new theme's base appearance, which can switch a dark UI to light.
505-
writeAppearanceModePreference(readAppearanceModePreference(getStored()));
506-
// Choosing a whole theme replaces any automatic-mode mix. The mix is
507-
// captured first so a failed preference write can put it back instead
508-
// of erasing it or leaving it attached to the new theme.
509-
const previousHalvesRaw = window.localStorage.getItem(THEME_HALVES_STORAGE_KEY);
510-
window.localStorage.removeItem(THEME_HALVES_STORAGE_KEY);
511-
try {
512-
writeThemePreference(next);
513-
} catch (cause) {
514-
if (previousHalvesRaw !== null) {
515-
try {
516-
window.localStorage.setItem(THEME_HALVES_STORAGE_KEY, previousHalvesRaw);
517-
} catch {}
518-
}
519-
throw cause;
520-
}
521-
} catch (cause) {
522-
const error = isThemeStorageError(cause)
523-
? cause
524-
: new ThemeStorageError({
525-
operation: "write",
526-
storageKey: STORAGE_KEY,
527-
theme: next,
528-
cause,
529-
});
530-
console.error(error.message, {
531-
operation: error.operation,
532-
storageKey: error.storageKey,
533-
theme: next,
534-
...safeErrorLogAttributes(error),
535-
});
536-
return false;
537-
}
538-
applyTheme(next, { suppressTransitions: true });
539-
emitChange();
540-
return true;
541-
}, []);
542-
543-
const setAppearanceMode = useCallback((nextAppearanceMode: ThemePreferenceMode): boolean => {
544-
if (typeof window === "undefined") return false;
495+
function setTheme(next: Theme): boolean {
496+
if (typeof window === "undefined") return false;
497+
try {
498+
// Preserve the current mode before replacing a legacy or inferred theme
499+
// preference. Otherwise a fresh System preference is re-inferred from
500+
// the new theme's base appearance, which can switch a dark UI to light.
501+
writeAppearanceModePreference(readAppearanceModePreference(getStored()));
502+
// Choosing a whole theme replaces any automatic-mode mix. The mix is
503+
// captured first so a failed preference write can put it back instead
504+
// of erasing it or leaving it attached to the new theme.
505+
const previousHalvesRaw = window.localStorage.getItem(THEME_HALVES_STORAGE_KEY);
506+
window.localStorage.removeItem(THEME_HALVES_STORAGE_KEY);
545507
try {
546-
writeAppearanceModePreference(nextAppearanceMode);
508+
writeThemePreference(next);
547509
} catch (cause) {
548-
const error = isThemeStorageError(cause)
549-
? cause
550-
: new ThemeStorageError({
551-
operation: "write",
552-
storageKey: THEME_APPEARANCE_MODE_STORAGE_KEY,
553-
cause,
554-
});
555-
console.error(error.message, {
556-
operation: error.operation,
557-
storageKey: error.storageKey,
558-
...safeErrorLogAttributes(error),
559-
});
560-
return false;
510+
if (previousHalvesRaw !== null) {
511+
try {
512+
window.localStorage.setItem(THEME_HALVES_STORAGE_KEY, previousHalvesRaw);
513+
} catch {}
514+
}
515+
throw cause;
561516
}
562-
themeStorageReadFailure = null;
563-
applyTheme(getStored(), { suppressTransitions: true });
564-
emitChange();
565-
return true;
566-
}, []);
567-
568-
const setFollowSystem = useCallback(
569-
(nextFollowSystem: boolean): boolean => {
570-
const currentMode = readAppearanceModePreference(theme);
571-
const nextMode = nextFollowSystem
572-
? "system"
573-
: currentMode === "system"
574-
? (getThemePreferenceMode(theme) ?? "light")
575-
: currentMode;
576-
return setAppearanceMode(nextMode);
577-
},
578-
[setAppearanceMode, theme],
579-
);
580-
581-
const setThemeHalf = useCallback(
582-
(appearance: ThemeAppearance, themeId: string | null): boolean => {
583-
if (typeof window === "undefined") return false;
584-
try {
585-
const current = readStoredThemeHalvesRaw();
586-
const next: { light?: string; dark?: string } = { ...current };
587-
if (themeId === null) delete next[appearance];
588-
else next[appearance] = themeId;
589-
if (next.light === undefined && next.dark === undefined) {
590-
window.localStorage.removeItem(THEME_HALVES_STORAGE_KEY);
591-
} else {
592-
window.localStorage.setItem(THEME_HALVES_STORAGE_KEY, JSON.stringify(next));
593-
}
594-
} catch (cause) {
595-
const error = new ThemeStorageError({
517+
} catch (cause) {
518+
const error = isThemeStorageError(cause)
519+
? cause
520+
: new ThemeStorageError({
596521
operation: "write",
597-
storageKey: THEME_HALVES_STORAGE_KEY,
522+
storageKey: STORAGE_KEY,
523+
theme: next,
598524
cause,
599525
});
600-
console.error(error.message, {
601-
operation: error.operation,
602-
storageKey: error.storageKey,
603-
...safeErrorLogAttributes(error),
526+
console.error(error.message, {
527+
operation: error.operation,
528+
storageKey: error.storageKey,
529+
theme: next,
530+
...safeErrorLogAttributes(error),
531+
});
532+
return false;
533+
}
534+
applyTheme(next, { suppressTransitions: true });
535+
emitChange();
536+
return true;
537+
}
538+
539+
function setAppearanceMode(nextAppearanceMode: ThemePreferenceMode): boolean {
540+
if (typeof window === "undefined") return false;
541+
try {
542+
writeAppearanceModePreference(nextAppearanceMode);
543+
} catch (cause) {
544+
const error = isThemeStorageError(cause)
545+
? cause
546+
: new ThemeStorageError({
547+
operation: "write",
548+
storageKey: THEME_APPEARANCE_MODE_STORAGE_KEY,
549+
cause,
604550
});
605-
return false;
606-
}
607-
applyTheme(getStored(), { suppressTransitions: true });
608-
emitChange();
609-
return true;
610-
},
611-
[],
612-
);
551+
console.error(error.message, {
552+
operation: error.operation,
553+
storageKey: error.storageKey,
554+
...safeErrorLogAttributes(error),
555+
});
556+
return false;
557+
}
558+
themeStorageReadFailure = null;
559+
applyTheme(getStored(), { suppressTransitions: true });
560+
emitChange();
561+
return true;
562+
}
613563

614-
const clearThemeHalves = useCallback((): boolean => {
615-
if (typeof window === "undefined") return false;
616-
try {
564+
function setThemeHalf(appearance: ThemeAppearance, themeId: string | null): boolean {
565+
if (typeof window === "undefined") return false;
566+
try {
567+
const current = readStoredThemeHalvesRaw();
568+
const next: { light?: string; dark?: string } = { ...current };
569+
if (themeId === null) delete next[appearance];
570+
else next[appearance] = themeId;
571+
if (next.light === undefined && next.dark === undefined) {
617572
window.localStorage.removeItem(THEME_HALVES_STORAGE_KEY);
618-
} catch (cause) {
619-
const error = new ThemeStorageError({
620-
operation: "write",
621-
storageKey: THEME_HALVES_STORAGE_KEY,
622-
cause,
623-
});
624-
console.error(error.message, {
625-
operation: error.operation,
626-
storageKey: error.storageKey,
627-
...safeErrorLogAttributes(error),
628-
});
629-
return false;
573+
} else {
574+
window.localStorage.setItem(THEME_HALVES_STORAGE_KEY, JSON.stringify(next));
630575
}
631-
applyTheme(getStored(), { suppressTransitions: true });
632-
emitChange();
633-
return true;
634-
}, []);
576+
} catch (cause) {
577+
const error = new ThemeStorageError({
578+
operation: "write",
579+
storageKey: THEME_HALVES_STORAGE_KEY,
580+
cause,
581+
});
582+
console.error(error.message, {
583+
operation: error.operation,
584+
storageKey: error.storageKey,
585+
...safeErrorLogAttributes(error),
586+
});
587+
return false;
588+
}
589+
applyTheme(getStored(), { suppressTransitions: true });
590+
emitChange();
591+
return true;
592+
}
635593

636-
const refreshTheme = useCallback(({ preservePreview = false } = {}) => {
637-
if (typeof window === "undefined") return;
638-
lastAppliedTheme = null;
639-
applyTheme(getStored(), { suppressTransitions: true, preservePreview });
640-
emitChange();
641-
}, []);
594+
function clearThemeHalves(): boolean {
595+
if (typeof window === "undefined") return false;
596+
try {
597+
window.localStorage.removeItem(THEME_HALVES_STORAGE_KEY);
598+
} catch (cause) {
599+
const error = new ThemeStorageError({
600+
operation: "write",
601+
storageKey: THEME_HALVES_STORAGE_KEY,
602+
cause,
603+
});
604+
console.error(error.message, {
605+
operation: error.operation,
606+
storageKey: error.storageKey,
607+
...safeErrorLogAttributes(error),
608+
});
609+
return false;
610+
}
611+
applyTheme(getStored(), { suppressTransitions: true });
612+
emitChange();
613+
return true;
614+
}
615+
616+
function refreshTheme({ preservePreview = false } = {}): void {
617+
if (typeof window === "undefined") return;
618+
lastAppliedTheme = null;
619+
applyTheme(getStored(), { suppressTransitions: true, preservePreview });
620+
emitChange();
621+
}
622+
623+
export function useTheme() {
624+
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
625+
const { theme, resolvedTheme } = snapshot;
626+
627+
const setFollowSystem = (nextFollowSystem: boolean): boolean => {
628+
const currentMode = readAppearanceModePreference(theme);
629+
const nextMode = nextFollowSystem
630+
? "system"
631+
: currentMode === "system"
632+
? (getThemePreferenceMode(theme) ?? "light")
633+
: currentMode;
634+
return setAppearanceMode(nextMode);
635+
};
642636

637+
// applyTheme reads the appearance mode and halves from storage, so it keys on the
638+
// whole snapshot; getSnapshot keeps one object until one of those changes.
643639
useEffect(() => {
644-
applyTheme(theme);
645-
}, [snapshot.appearanceMode, theme]);
640+
applyTheme(snapshot.theme);
641+
}, [snapshot]);
646642

647643
return {
648644
theme,

0 commit comments

Comments
 (0)