Skip to content

Commit eaa29cb

Browse files
authored
Preload theme to prevent screen flicker (#183)
* Preload theme to prevent screen flicker * refactor: extract localStorageCache
1 parent 386b78c commit eaa29cb

File tree

9 files changed

+90
-36
lines changed

9 files changed

+90
-36
lines changed

src/Themes/component.tsx

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
import './component.css'
2-
import { FC, useEffect } from 'react'
32
import { allColorSchemes, allThemes } from './model'
4-
import { useSelectedColorScheme, useSelectedTheme } from './repository'
3+
import { useSelectedColorScheme, useSelectedTheme } from './hook'
4+
import { FC } from 'react'
55

66
const ThemesComponent: FC = () => {
7-
const [selectedTheme, setSelectedTheme] = useSelectedTheme('standard')
8-
const [selectedColorScheme, setSelectedColorScheme] = useSelectedColorScheme('auto')
9-
useEffect(() => {
10-
document.documentElement.dataset['theme'] = selectedTheme
11-
document.documentElement.dataset['colorScheme'] = selectedColorScheme
12-
}, [selectedTheme, selectedColorScheme])
13-
7+
const [selectedTheme, setSelectedTheme] = useSelectedTheme()
8+
const [selectedColorScheme, setSelectedColorScheme] = useSelectedColorScheme()
149
return (
1510
<>
1611
<div>

src/Themes/hook.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { ColorScheme, Theme, isColorScheme, isTheme } from './model'
2+
import { Dispatch, useEffect } from 'react'
3+
import { Spec, useChromeStorageWithCache } from '../infrastructure/chromeStorage'
4+
import { getOrInitialValue } from '../infrastructure/localStorageCache'
5+
6+
const selectedThemeSpec: Spec<Theme> = {
7+
areaName: 'sync',
8+
key: 'v3.selectedTheme',
9+
initialValue: 'standard',
10+
isType: isTheme,
11+
}
12+
13+
export const useSelectedTheme = (): [Theme, Dispatch<Theme>] => {
14+
const [theme, setTheme] = useChromeStorageWithCache(selectedThemeSpec)
15+
useEffect(() => {
16+
document.documentElement.dataset['theme'] = theme
17+
}, [theme])
18+
return [theme, setTheme]
19+
}
20+
21+
const selectedColorSchemeSpec: Spec<ColorScheme> = {
22+
areaName: 'sync',
23+
key: 'v3.selectedColorScheme',
24+
initialValue: 'auto',
25+
isType: isColorScheme,
26+
}
27+
28+
export const useSelectedColorScheme = (): [ColorScheme, Dispatch<ColorScheme>] => {
29+
const [colorScheme, setColorScheme] = useChromeStorageWithCache(selectedColorSchemeSpec)
30+
useEffect(() => {
31+
document.documentElement.dataset['colorScheme'] = colorScheme
32+
}, [colorScheme])
33+
return [colorScheme, setColorScheme]
34+
}
35+
36+
export const preloadFromCache = () => {
37+
document.documentElement.dataset['theme'] = getOrInitialValue(selectedThemeSpec)
38+
document.documentElement.dataset['colorScheme'] = getOrInitialValue(selectedColorSchemeSpec)
39+
}

src/Themes/repository.ts

Lines changed: 0 additions & 18 deletions
This file was deleted.

src/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import App from './App/component'
33
import React from 'react'
44
import ReactDOM from 'react-dom/client'
55
import { migratePreferencesFromV2ToV3 } from './migration'
6+
import { preloadFromCache } from './Themes/hook'
67

78
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
89
root.render(
@@ -11,6 +12,9 @@ root.render(
1112
</React.StrictMode>
1213
)
1314

15+
// preload the theme to prevent screen flicker
16+
preloadFromCache()
17+
1418
// the default size of popup is too small, so explicitly set it
1519
// https://developer.chrome.com/docs/extensions/reference/action/#popup
1620
if (document.location.hash === '#popup') {

src/infrastructure/chromeStorage.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
import { useEffect, useState } from 'react'
1+
import { Dispatch, useEffect, useState } from 'react'
2+
import { useLocalStorageCache } from './localStorageCache'
23

3-
type Spec<T> = {
4+
export type Spec<T> = {
45
areaName: chrome.storage.AreaName
56
key: string
67
initialValue: T
78
isType: (value: unknown) => value is T
89
}
910

10-
export const useChromeStorage = <T>(spec: Spec<T>): readonly [T, (newValue: T) => void] => {
11+
export const useChromeStorage = <T>(spec: Spec<T>): readonly [T, Dispatch<T>] => {
1112
const [storedValue, setStoredValue] = useState<T>(spec.initialValue)
1213
useEffect(
1314
() => {
@@ -27,7 +28,7 @@ export const useChromeStorage = <T>(spec: Spec<T>): readonly [T, (newValue: T) =
2728
]
2829
}
2930

30-
const initialLoad = <T>(spec: Spec<T>, setStoredValue: (newValue: T) => void) => {
31+
const initialLoad = <T>(spec: Spec<T>, setStoredValue: Dispatch<T>) => {
3132
chrome.storage[spec.areaName]
3233
.get(spec.key)
3334
.then((items) => {
@@ -48,7 +49,7 @@ const initialLoad = <T>(spec: Spec<T>, setStoredValue: (newValue: T) => void) =>
4849
.catch((e) => console.error(e))
4950
}
5051

51-
const subscribeChange = <T>(spec: Spec<T>, setStoredValue: (newValue: T) => void) => {
52+
const subscribeChange = <T>(spec: Spec<T>, setStoredValue: Dispatch<T>) => {
5253
const area = chrome.storage[spec.areaName]
5354
const listener = (changes: { [key: string]: chrome.storage.StorageChange }) => {
5455
if (!(spec.key in changes)) {
@@ -68,3 +69,15 @@ const subscribeChange = <T>(spec: Spec<T>, setStoredValue: (newValue: T) => void
6869
area.onChanged.addListener(listener)
6970
return () => area.onChanged.removeListener(listener)
7071
}
72+
73+
export const useChromeStorageWithCache = <T extends string>(spec: Spec<T>): [T, Dispatch<T>] => {
74+
const [cache, setCache] = useLocalStorageCache(spec)
75+
const [value, setValue] = useChromeStorage<T>({
76+
...spec,
77+
initialValue: cache,
78+
})
79+
useEffect(() => {
80+
setCache(value)
81+
}, [setCache, value])
82+
return [value, setValue]
83+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Dispatch, useEffect, useState } from 'react'
2+
3+
type Spec<T extends string> = {
4+
key: string
5+
initialValue: T
6+
isType: (value: unknown) => value is T
7+
}
8+
9+
export const useLocalStorageCache = <T extends string>(spec: Spec<T>): [T, Dispatch<T>] => {
10+
const [value, setValue] = useState<T>(getOrInitialValue(spec))
11+
useEffect(() => {
12+
localStorage.setItem(spec.key, value)
13+
}, [spec.key, value])
14+
return [value, setValue]
15+
}
16+
17+
export const getOrInitialValue = <T extends string>(spec: Spec<T>): T => {
18+
const cachedValue = localStorage.getItem(spec.key)
19+
if (spec.isType(cachedValue)) {
20+
return cachedValue
21+
}
22+
return spec.initialValue
23+
}

src/migration/folderItemPreferences.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@ export const migrate = async () => {
2727
}
2828
const shortcutMap = upgrade(folderItemPreferences)
2929
await chrome.storage.sync.set({ [V3_KEY]: shortcutMap.serialize() })
30+
localStorage.removeItem(V2_KEY)
3031
}

src/migration/folderPreferences.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ export const migrate = async () => {
2626
}
2727
const folderCollapse = upgrade(folderPreferences)
2828
await chrome.storage.sync.set({ [V3_KEY]: folderCollapse.serialize() })
29+
localStorage.removeItem(V2_KEY)
2930
}

src/migration/index.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,6 @@ import * as folderPreferences from './folderPreferences'
33

44
// Migrate preferences from v2 (Local Storage) to v3 (Chrome Storage)
55
export const migratePreferencesFromV2ToV3 = async () => {
6-
if (window.localStorage.length === 0) {
7-
return
8-
}
96
await folderPreferences.migrate()
107
await folderItemPreferences.migrate()
11-
window.localStorage.clear()
128
}

0 commit comments

Comments
 (0)