Skip to content

Commit 7429182

Browse files
authored
perf: optimize color modes (#185)
1 parent 074f36f commit 7429182

File tree

6 files changed

+172
-149
lines changed

6 files changed

+172
-149
lines changed

packages/core/src/colorModes.test.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
12
/* eslint-disable no-underscore-dangle */
23
/* eslint-disable react/state-in-constructor */
34
/* eslint-env browser */
@@ -80,8 +81,8 @@ describe('#createColorStyles', () => {
8081
},
8182
},
8283
}
83-
expect(createColorStyles(theme)).toBe(
84-
`body{--xstyled-colors-black: #000;--xstyled-colors-white: #fff;--xstyled-colors-red: #ff0000;--xstyled-colors-danger: #ff0000;--xstyled-colors-text: #000;@media (prefers-color-scheme: dark){--xstyled-colors-black: #000;--xstyled-colors-white: #fff;--xstyled-colors-red: #ff4400;--xstyled-colors-danger: #ff4400;--xstyled-colors-text: #fff;}&.xstyled-color-mode-default{--xstyled-colors-black: #000;--xstyled-colors-white: #fff;--xstyled-colors-red: #ff0000;--xstyled-colors-danger: #ff0000;--xstyled-colors-text: #000;}&.xstyled-color-mode-dark{--xstyled-colors-black: #000;--xstyled-colors-white: #fff;--xstyled-colors-red: #ff4400;--xstyled-colors-danger: #ff4400;--xstyled-colors-text: #fff;}}`,
84+
expect(createColorStyles(theme as any)).toBe(
85+
`body{--xstyled-colors-red: #ff0000;--xstyled-colors-text: #000;@media (prefers-color-scheme: dark){--xstyled-colors-red: #ff4400;--xstyled-colors-text: #fff;}&.xstyled-color-mode-default{--xstyled-colors-red: #ff0000;--xstyled-colors-text: #000;}&.xstyled-color-mode-dark{--xstyled-colors-red: #ff4400;--xstyled-colors-text: #fff;}}`,
8586
)
8687
})
8788
})
@@ -272,10 +273,8 @@ describe('#useColorModeTheme', () => {
272273
render(<Dummy />)
273274
expect(colorModeTheme).toEqual({
274275
colors: {
275-
black: 'var(--xstyled-colors-black, #000)',
276-
white: 'var(--xstyled-colors-white, #fff)',
276+
...darkTheme.colors,
277277
red: 'var(--xstyled-colors-red, #ff0000)',
278-
danger: 'var(--xstyled-colors-danger, #ff0000)',
279278
text: 'var(--xstyled-colors-text, #000)',
280279
modes: { dark: darkTheme.colors.modes.dark },
281280
},

packages/core/src/colorModes.tsx

Lines changed: 108 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable @typescript-eslint/no-empty-function */
12
/* eslint-disable react/no-danger */
23
/* eslint-env browser */
34
import * as React from 'react'
@@ -6,28 +7,51 @@ import {
67
toCustomPropertiesReferences,
78
} from './customProperties'
89

9-
type Mode = string | null
10-
type ColorModeState = [Mode, (mode: Mode) => void]
10+
type ColorModeState = [string | null, (mode: string | null) => void]
11+
type Color = string | ((props: Record<string, unknown>) => Color)
12+
type Colors = Record<string, Color>
13+
14+
interface ITheme {
15+
useCustomProperties?: boolean
16+
useColorSchemeMediaQuery?: boolean
17+
initialColorModeName?: string
18+
defaultColorModeName?: string
19+
colors?: Colors & {
20+
modes?: Record<string, Colors>
21+
}
22+
}
23+
24+
interface IColorModeTheme extends ITheme {
25+
colors: Colors & { modes: Record<string, Colors> }
26+
}
1127

1228
const STORAGE_KEY = 'xstyled-color-mode'
1329

14-
const isLocalStorageAvailable =
30+
const isLocalStorageAvailable: boolean =
1531
typeof window !== 'undefined' &&
1632
(() => {
1733
try {
18-
const STORAGE_TEST_KEY = `${STORAGE_KEY}-test`
19-
window.localStorage.setItem(STORAGE_TEST_KEY, STORAGE_TEST_KEY)
20-
window.localStorage.removeItem(STORAGE_TEST_KEY)
34+
const key = 'xstyled-test-key'
35+
window.localStorage.setItem(key, key)
36+
window.localStorage.removeItem(key)
2137
return true
2238
} catch (err) {
2339
return false
2440
}
2541
})()
2642

27-
const storage = isLocalStorageAvailable
43+
interface Storage {
44+
get(): string | null
45+
set(value: string): void
46+
clear(): void
47+
}
48+
49+
const storage: Storage = isLocalStorageAvailable
2850
? {
2951
get: () => window.localStorage.getItem(STORAGE_KEY),
30-
set: (value: string) => window.localStorage.setItem(STORAGE_KEY, value),
52+
set: (value: string) => {
53+
window.localStorage.setItem(STORAGE_KEY, value)
54+
},
3155
clear: () => window.localStorage.removeItem(STORAGE_KEY),
3256
}
3357
: {
@@ -43,82 +67,76 @@ const getColorModeClassName = (mode: string) =>
4367
const XSTYLED_COLORS_PREFIX = 'xstyled-colors'
4468
const SYSTEM_MODES = ['light', 'dark']
4569

46-
interface Theme {
47-
useCustomProperties?: boolean
48-
useColorSchemeMediaQuery?: boolean
49-
initialColorModeName?: string
50-
defaultColorModeName?: string
51-
colors?: {
52-
modes?: {
53-
[key: string]: any
54-
}
55-
}
56-
}
57-
58-
interface ModeTheme extends Theme {
59-
colors: {
60-
modes: {
61-
[key: string]: any
62-
}
63-
}
64-
}
65-
66-
function getModeTheme(theme: ModeTheme, mode: string) {
70+
function getModeTheme(theme: IColorModeTheme, mode: string): IColorModeTheme {
6771
return {
6872
...theme,
6973
colors: { ...theme.colors, ...theme.colors.modes[mode] },
7074
}
7175
}
7276

73-
const getMediaQuery = (query: string) => `@media ${query}`
74-
const getColorModeQuery = (mode: string) => `(prefers-color-scheme: ${mode})`
77+
const getMediaQuery = (query: string): string => `@media ${query}`
78+
const getColorModeQuery = (mode: string): string =>
79+
`(prefers-color-scheme: ${mode})`
7580

76-
function hasColorModes(theme: Theme): theme is ModeTheme {
81+
function checkHasColorModes(theme: ITheme | null): theme is IColorModeTheme {
7782
return Boolean(theme && theme.colors && theme.colors.modes)
7883
}
7984

80-
function hasCustomPropertiesEnabled(theme: Theme) {
81-
return (
85+
function checkHasCustomPropertiesEnabled(theme: ITheme | null): boolean {
86+
return Boolean(
8287
theme &&
83-
(theme.useCustomProperties === undefined || theme.useCustomProperties)
88+
(theme.useCustomProperties === undefined || theme.useCustomProperties),
8489
)
8590
}
8691

87-
function hasMediaQueryEnabled(theme: Theme) {
88-
return (
92+
function checkHasMediaQueryEnabled(theme: ITheme | null): boolean {
93+
return Boolean(
8994
theme &&
90-
(theme.useColorSchemeMediaQuery === undefined ||
91-
theme.useColorSchemeMediaQuery)
95+
(theme.useColorSchemeMediaQuery === undefined ||
96+
theme.useColorSchemeMediaQuery),
9297
)
9398
}
9499

95-
function getInitialColorModeName(theme: Theme) {
100+
function getInitialColorModeName(theme: ITheme): string {
96101
return theme.initialColorModeName || 'default'
97102
}
98103

99-
function getDefaultColorModeName(theme: Theme) {
104+
function getDefaultColorModeName(theme: ITheme): string {
100105
return theme.defaultColorModeName || getInitialColorModeName(theme)
101106
}
102107

108+
function getUsedColorKeys(modes: Record<string, Record<string, Color>>) {
109+
let keys: string[] = []
110+
for (const key in modes) {
111+
keys = [...keys, ...Object.keys(modes[key])]
112+
}
113+
return keys
114+
}
115+
103116
export function createColorStyles(
104-
theme: Theme,
117+
theme: ITheme,
105118
{ targetSelector = 'body' } = {},
106-
) {
107-
if (!hasColorModes(theme)) return null
119+
): string | null {
120+
if (!checkHasColorModes(theme)) return null
121+
108122
const { modes, ...colors } = theme.colors
123+
const colorKeys = getUsedColorKeys(modes)
124+
109125
let styles = toCustomPropertiesDeclarations(
110126
colors,
111-
XSTYLED_COLORS_PREFIX,
112127
theme,
128+
colorKeys,
129+
XSTYLED_COLORS_PREFIX,
113130
)
114131

115132
function getModePropertiesDeclarations(mode: string) {
116-
const modeTheme = getModeTheme(theme as ModeTheme, mode)
133+
const modeTheme = getModeTheme(theme as IColorModeTheme, mode)
117134
const { modes, ...colors } = modeTheme.colors
118135
return toCustomPropertiesDeclarations(
119136
{ ...colors, ...modes[mode] },
120-
XSTYLED_COLORS_PREFIX,
121137
modeTheme,
138+
colorKeys,
139+
XSTYLED_COLORS_PREFIX,
122140
)
123141
}
124142

@@ -148,10 +166,11 @@ function getSystemModeMql(mode: string) {
148166
return window.matchMedia(query)
149167
}
150168

151-
function useSystemMode(theme: ModeTheme) {
169+
function useSystemMode(theme: ITheme) {
152170
const configs: { mode: string; mql: MediaQueryList }[] = React.useMemo(() => {
153-
if (!hasMediaQueryEnabled(theme)) return []
171+
if (!checkHasMediaQueryEnabled(theme)) return []
154172
return SYSTEM_MODES.map((mode) => {
173+
if (!checkHasColorModes(theme)) return null
155174
if (!theme.colors.modes[mode]) return null
156175
const mql = getSystemModeMql(mode)
157176
return mql ? { mode, mql } : null
@@ -189,19 +208,19 @@ const useIsomorphicLayoutEffect =
189208
typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect
190209

191210
export function useColorModeState(
192-
theme: ModeTheme,
211+
theme: ITheme,
193212
{ target }: { target?: Element } = {},
194213
): ColorModeState {
195214
const systemMode = useSystemMode(theme)
196215
const defaultColorMode = getDefaultColorModeName(theme)
197216
const initialColorMode = getInitialColorModeName(theme)
198217
const [mode, setMode] = React.useState(() => {
199-
if (!hasColorModes(theme)) return null
218+
if (!checkHasColorModes(theme)) return null
200219
return defaultColorMode
201220
})
202221

203222
// Add mode className
204-
const customPropertiesEnabled = hasCustomPropertiesEnabled(theme)
223+
const customPropertiesEnabled = checkHasCustomPropertiesEnabled(theme)
205224

206225
const manualSetRef = React.useRef(false)
207226
const manuallySetMode = React.useCallback((value) => {
@@ -211,7 +230,7 @@ export function useColorModeState(
211230

212231
// Set initial color mode in lazy
213232
useIsomorphicLayoutEffect(() => {
214-
if (!hasColorModes(theme)) return
233+
if (!checkHasColorModes(theme)) return
215234
const storedMode = storage.get()
216235
const initialMode = storedMode || systemMode || defaultColorMode
217236
if (mode !== initialMode) {
@@ -257,27 +276,38 @@ export function useColorModeState(
257276
return [mode, manuallySetMode]
258277
}
259278

260-
export function useColorModeTheme(theme: any, mode: Mode) {
279+
export function useColorModeTheme(
280+
theme: ITheme,
281+
mode: string | null,
282+
): ITheme | null {
283+
const [initialMode] = React.useState(mode)
261284
const customPropertiesTheme = React.useMemo(() => {
262-
if (!mode) return null
263-
if (!hasCustomPropertiesEnabled(theme)) return null
264-
if (!hasColorModes(theme)) return theme
265-
285+
if (!initialMode) return null
286+
if (!checkHasCustomPropertiesEnabled(theme)) return null
287+
if (!checkHasColorModes(theme)) return theme
266288
const { modes, ...colors } = theme.colors
289+
const colorKeys = getUsedColorKeys(modes)
290+
267291
return {
268292
...theme,
269293
colors: {
270-
...toCustomPropertiesReferences(colors, XSTYLED_COLORS_PREFIX, theme),
294+
...colors,
295+
...toCustomPropertiesReferences(
296+
colors,
297+
theme,
298+
colorKeys,
299+
XSTYLED_COLORS_PREFIX,
300+
),
271301
modes,
272302
},
273303
__rawColors: theme.colors,
274304
}
275-
}, [theme])
305+
}, [initialMode, theme])
276306

277307
const swapModeTheme = React.useMemo(() => {
278308
if (!mode) return null
279-
if (hasCustomPropertiesEnabled(theme)) return null
280-
if (!hasColorModes(theme)) return theme
309+
if (checkHasCustomPropertiesEnabled(theme)) return null
310+
if (!checkHasColorModes(theme)) return theme
281311

282312
if (mode === getInitialColorModeName(theme)) {
283313
return { ...theme, __colorMode: mode }
@@ -294,12 +324,12 @@ export function useColorModeTheme(theme: any, mode: Mode) {
294324
}
295325
}, [theme, mode])
296326

297-
return customPropertiesTheme || swapModeTheme
327+
return (customPropertiesTheme || swapModeTheme) as ITheme
298328
}
299329

300330
export const ColorModeContext = React.createContext<ColorModeState | null>(null)
301331

302-
export function useColorMode() {
332+
export function useColorMode(): ColorModeState {
303333
const colorModeState = React.useContext(ColorModeContext)
304334

305335
if (!colorModeState) {
@@ -309,6 +339,12 @@ export function useColorMode() {
309339
return colorModeState
310340
}
311341

342+
export interface ColorModeProviderProps {
343+
children: React.ReactNode
344+
target?: Element
345+
targetSelector?: string
346+
}
347+
312348
export function createColorModeProvider({
313349
ThemeContext,
314350
ThemeProvider,
@@ -317,16 +353,12 @@ export function createColorModeProvider({
317353
ThemeContext: React.Context<any>
318354
ThemeProvider: React.ComponentType<any>
319355
ColorModeStyle: React.ComponentType<any>
320-
}) {
356+
}): React.FC<ColorModeProviderProps> {
321357
function ColorModeProvider({
322358
children,
323359
target,
324360
targetSelector,
325-
}: {
326-
children: React.ReactNode
327-
target?: Element
328-
targetSelector?: string
329-
}) {
361+
}: ColorModeProviderProps) {
330362
const theme = React.useContext(ThemeContext)
331363
if (!theme) {
332364
throw new Error(
@@ -362,7 +394,9 @@ function getInitScript({
362394
} catch (e) {} })();`
363395
}
364396

365-
export function getColorModeInitScriptElement(options?: GetInitScriptOptions) {
397+
export function getColorModeInitScriptElement(
398+
options?: GetInitScriptOptions,
399+
): JSX.Element {
366400
return (
367401
<script
368402
key="xstyled-color-mode-init"
@@ -371,6 +405,8 @@ export function getColorModeInitScriptElement(options?: GetInitScriptOptions) {
371405
)
372406
}
373407

374-
export function getColorModeInitScriptTag(options?: GetInitScriptOptions) {
408+
export function getColorModeInitScriptTag(
409+
options?: GetInitScriptOptions,
410+
): string {
375411
return `<script>${getInitScript(options)}</script>`
376412
}

0 commit comments

Comments
 (0)