Skip to content

Commit 1ec7936

Browse files
Update useTheme.mjs
Co-Authored-By: Aviv Keller <me@aviv.sh>
1 parent bc6e7e7 commit 1ec7936

1 file changed

Lines changed: 31 additions & 87 deletions

File tree

Lines changed: 31 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,56 @@
11
import { useState, useEffect, useCallback } from 'react';
22

3-
const THEME_STORAGE_KEY = 'theme';
4-
const THEME_PREFERENCES = new Set(['system', 'light', 'dark']);
5-
6-
/**
7-
* Sets up theme toggle button and system preference listener
8-
*/
3+
/** @returns {'dark'|'light'} The current OS-level color scheme. */
94
const getSystemTheme = () =>
105
matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
116

127
/**
13-
* Retrieves the stored theme preference from local storage.
14-
*
15-
* @returns {'system'|'light'|'dark'|null} The stored theme preference or null if not found.
8+
* Applies a theme to the document root.
9+
* Resolves 'system' to the actual OS preference before applying.
10+
* @param {'system'|'light'|'dark'} pref - The theme preference.
1611
*/
17-
const getStoredThemePreference = () => {
18-
try {
19-
const storedTheme = localStorage.getItem(THEME_STORAGE_KEY);
20-
return THEME_PREFERENCES.has(storedTheme) ? storedTheme : null;
21-
} catch {
22-
return null;
23-
}
12+
const applyTheme = pref => {
13+
const theme = pref === 'system' ? getSystemTheme() : pref;
14+
document.documentElement.setAttribute('data-theme', theme);
15+
document.documentElement.style.colorScheme = theme;
2416
};
2517

2618
/**
27-
* Stores the theme preference in local storage.
28-
* If storage is unavailable, it fails silently, allowing the application to continue functioning with an in-memory preference.
29-
*
30-
* @param {'system'|'light'|'dark'} themePreference - The theme preference to store.
19+
* Applies the system theme to the document root.
3120
*/
32-
const setStoredThemePreference = themePreference => {
33-
try {
34-
localStorage.setItem(THEME_STORAGE_KEY, themePreference);
35-
} catch {
36-
// Ignore storage failures and keep non-persistent in-memory preference.
37-
}
38-
};
39-
40-
/**
41-
* Applies a theme preference to the document.
42-
*
43-
* The persisted preference can be 'system', but the applied document theme is
44-
* always resolved to either 'light' or 'dark'.
45-
*
46-
* @param {'system'|'light'|'dark'} themePreference - Theme preference.
47-
*/
48-
const applyThemePreference = themePreference => {
49-
const resolvedTheme =
50-
themePreference === 'system' ? getSystemTheme() : themePreference;
51-
52-
document.documentElement.setAttribute('data-theme', resolvedTheme);
53-
document.documentElement.style.colorScheme = resolvedTheme;
54-
};
21+
const applySystemTheme = () => applyTheme('system');
5522

5623
/**
57-
* A React hook for managing the application's theme preference.
24+
* React hook for managing theme preference.
25+
* Persists the choice to localStorage and listens for OS theme changes
26+
* when set to 'system'.
27+
* @returns {['system'|'light'|'dark', (next: 'system'|'light'|'dark') => void]}
5828
*/
5929
export const useTheme = () => {
60-
const [themePreference, setThemePreferenceState] = useState('system');
30+
// Read stored preference once on mount; default to 'system'.
31+
const [pref, setPref] = useState(() => {
32+
return localStorage.getItem('theme') || 'system';
33+
});
6134

35+
// Apply theme on every preference change, and if 'system',
36+
// also listen for OS-level color scheme changes.
6237
useEffect(() => {
63-
// Use persisted preference if available, otherwise default to system.
64-
const initialPreference = getStoredThemePreference() || 'system';
38+
applyTheme(pref);
6539

66-
applyThemePreference(initialPreference);
67-
setThemePreferenceState(initialPreference);
68-
}, []);
69-
70-
/**
71-
* Keep the resolved document theme in sync with system changes
72-
* whenever the preference is set to 'system'.
73-
*/
74-
useEffect(() => {
75-
if (themePreference !== 'system') {
40+
if (pref !== 'system') {
7641
return;
7742
}
7843

79-
const mediaQueryList = matchMedia('(prefers-color-scheme: dark)');
80-
/**
81-
*
82-
*/
83-
const handleSystemThemeChange = () => applyThemePreference('system');
84-
85-
if ('addEventListener' in mediaQueryList) {
86-
mediaQueryList.addEventListener('change', handleSystemThemeChange);
87-
return () => {
88-
mediaQueryList.removeEventListener('change', handleSystemThemeChange);
89-
};
90-
}
91-
92-
mediaQueryList.addListener(handleSystemThemeChange);
93-
return () => {
94-
mediaQueryList.removeListener(handleSystemThemeChange);
95-
};
96-
}, [themePreference]);
97-
98-
/**
99-
* Updates the theme preference and applies it immediately.
100-
*/
101-
const setThemePreference = useCallback(nextPreference => {
102-
if (!THEME_PREFERENCES.has(nextPreference)) {
103-
return;
104-
}
44+
const mql = matchMedia('(prefers-color-scheme: dark)');
45+
mql.addEventListener('change', applySystemTheme);
46+
return () => mql.removeEventListener('change', applySystemTheme);
47+
}, [pref]);
10548

106-
setThemePreferenceState(nextPreference);
107-
setStoredThemePreference(nextPreference);
108-
applyThemePreference(nextPreference);
49+
/** Updates the preference in both React state and localStorage. */
50+
const setTheme = useCallback(next => {
51+
setPref(next);
52+
localStorage.setItem('theme', next);
10953
}, []);
11054

111-
return [themePreference, setThemePreference];
55+
return [pref, setTheme];
11256
};

0 commit comments

Comments
 (0)