Skip to content

Commit b39ce94

Browse files
committed
refactor(ui): ThemeProvider - extract themeName/cookie functionality
1 parent d57787b commit b39ce94

File tree

5 files changed

+85
-42
lines changed

5 files changed

+85
-42
lines changed

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
"@bundle-stats/utils": "4.21.7",
7878
"ariakit": "2.0.0-next.44",
7979
"d3": "7.9.0",
80+
"js-cookie": "2.2.1",
8081
"modern-css-reset": "1.4.0",
8182
"react-use": "17.6.0",
8283
"snarkdown": "2.0.0",

packages/ui/src/context/theme.tsx

Lines changed: 20 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,7 @@
11
import type { ReactNode } from 'react';
22
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
3-
import { useCookie } from 'react-use';
4-
import { wait } from '@bundle-stats/utils';
53

6-
// classList required timeout to allow to disable motion during the switch
7-
const CLASSLIST_UPDATE_TIMEOUT = 10;
8-
9-
type ThemeName = 'light' | 'dark';
10-
11-
const getCurrentTheme = (): ThemeName => {
12-
const { matches } = window.matchMedia('(prefers-color-scheme: dark)');
13-
return matches ? 'dark' : 'light';
14-
};
4+
import { getCurrentTheme, switchTheme, useCookieTheme, type ThemeName } from '../utils/theme';
155

166
type ThemeContextProps = {
177
name: ThemeName;
@@ -23,47 +13,35 @@ const ThemeContext = createContext<ThemeContextProps>({
2313
update: () => {},
2414
});
2515

26-
export const ThemeProvider = ({ children }: { children: ReactNode }) => {
27-
const [cookieValue, setCookieValue] = useCookie('theme');
28-
const [name, setName] = useState<ThemeName>((cookieValue as ThemeName) || getCurrentTheme());
29-
30-
const updateClassList = useCallback(async (newTheme: ThemeName) => {
31-
const htmlElm = document.querySelector('html');
32-
33-
htmlElm?.classList.add('no-motion');
34-
35-
await wait(CLASSLIST_UPDATE_TIMEOUT);
36-
37-
if (newTheme === 'dark') {
38-
htmlElm?.classList.remove('light-theme');
39-
htmlElm?.classList.add('dark-theme');
40-
} else {
41-
htmlElm?.classList.remove('dark-theme');
42-
htmlElm?.classList.add('light-theme');
43-
}
16+
export type ThemeProviderProps = {
17+
children: ReactNode;
18+
};
4419

45-
await wait(CLASSLIST_UPDATE_TIMEOUT);
20+
export const ThemeProvider = (props: ThemeProviderProps) => {
21+
const { children } = props;
4622

47-
htmlElm?.classList.remove('no-motion');
48-
}, []);
23+
const [cookieValue, setCookieValue] = useCookieTheme();
24+
const [theme, setTheme] = useState<ThemeName>((cookieValue as ThemeName) || getCurrentTheme());
4925

50-
const updateTheme = useCallback((nextTheme: ThemeName) => {
51-
setName(nextTheme);
52-
updateClassList(nextTheme);
53-
setCookieValue(nextTheme);
54-
}, []);
26+
const updateTheme = useCallback(
27+
(nextTheme: ThemeName) => {
28+
setCookieValue(nextTheme);
29+
switchTheme(nextTheme);
30+
},
31+
[setTheme, setCookieValue],
32+
);
5533

56-
// Sync classList
34+
// Sync classNames on load
5735
useEffect(() => {
58-
updateClassList(name);
59-
}, [name]);
36+
switchTheme(theme);
37+
}, [theme]);
6038

6139
const value = useMemo(
6240
() => ({
63-
name,
41+
name: theme,
6442
update: updateTheme,
6543
}),
66-
[name, updateTheme],
44+
[theme, updateTheme],
6745
);
6846

6947
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;

packages/ui/src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './colors';
2+
export * from './theme';
23
export * from './jobs';
34
export * from './components';

packages/ui/src/utils/theme.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { useCookie } from 'react-use';
2+
import Cookies from 'js-cookie';
3+
import { wait } from '@bundle-stats/utils';
4+
5+
export type ThemeName = 'light' | 'dark';
6+
7+
// classList required timeout to allow to disable motion during the switch
8+
const CLASSLIST_UPDATE_TIMEOUT = 10;
9+
10+
const COOKIE_NAME = 'theme';
11+
12+
export const getCurrentTheme = (): ThemeName => {
13+
const { matches } = window.matchMedia('(prefers-color-scheme: dark)');
14+
return matches ? 'dark' : 'light';
15+
};
16+
17+
export const getCookieTheme = (): ThemeName | null => {
18+
const nextTheme = Cookies.get(COOKIE_NAME);
19+
20+
if (!nextTheme) {
21+
return null;
22+
}
23+
24+
if (['light', 'dark'].includes(nextTheme)) {
25+
return nextTheme as ThemeName;
26+
}
27+
28+
return null;
29+
};
30+
31+
export const switchTheme = async (newTheme: ThemeName) => {
32+
const htmlElm = document.querySelector('html');
33+
34+
htmlElm?.classList.add('no-motion');
35+
36+
await wait(CLASSLIST_UPDATE_TIMEOUT);
37+
38+
if (newTheme === 'dark') {
39+
htmlElm?.classList.remove('light-theme');
40+
htmlElm?.classList.add('dark-theme');
41+
} else {
42+
htmlElm?.classList.remove('dark-theme');
43+
htmlElm?.classList.add('light-theme');
44+
}
45+
46+
await wait(CLASSLIST_UPDATE_TIMEOUT);
47+
48+
htmlElm?.classList.remove('no-motion');
49+
};
50+
51+
export const useCookieTheme = () => {
52+
return useCookie(COOKIE_NAME);
53+
};

0 commit comments

Comments
 (0)