您可以在这里查看我使用 Next.js 和 Material UI (5) 所做的示例:
- 有 2 个主题可用:lightTheme 和 darkTheme。
- 有一个 ThemeSwitcherButton 组件,以便我们可以在两个主题之间切换。
- 创建一个新的 ThemeProvider 和 ThemeContext 来存储选定的主题模式值,提供读取和更改它的权限。
- 使用 useLocalStorage 挂钩将用户的首选项存储在本地存储中。
- 如果没有存储值,则使用 Material UI useMediaQuery 挂钩加载从浏览器首选项读取的主题模式。
我用的是 Typescript,不过你用纯 JavaScript 没关系
创建所需的 2 个主题:
我们将有 2 个文件来独立修改特定属性。
darkTheme.ts
import { createTheme } from '@mui/material/styles'
const darkTheme = createTheme({
palette: {
mode: 'dark',
},
})
export default darkTheme
lightTheme.ts
import { createTheme } from '@mui/material/styles'
const lightTheme = createTheme({
palette: {
mode: 'light',
},
})
export default lightTheme
创建切换按钮:
这里重要的是从上下文中检索的值和功能,按钮样式或图标可以是任何东西。
interface ThemeSwitcherButtonProps extends IconButtonProps { }
const ThemeSwitcherButton = ({ ...rest }: ThemeSwitcherButtonProps) => {
const { themeMode, toggleTheme } = useThemeContext()
return (
<Tooltip
title={themeMode === 'light' ? `Switch to dark mode` : `Switch to light mode`}
>
<IconButton
{...rest}
onClick={toggleTheme}
>
{themeMode === 'light' ? <DarkModeOutlined /> : <LightModeRounded />}
</IconButton>
</Tooltip>
)
}
export default ThemeSwitcherButton
创建 ThemeContext、ThemeProvider 和 useThemeContext:
我们使用 Material ui 库中的useMediaQuery 来检查浏览器的偏好模式。此钩子适用于客户端渲染和 ssr。
我们还使用useLocalStorage 挂钩将状态保存在本地存储中,以便持久化。
我们用这个新的 Provider 包装了原来的 Material UI ThemeProvider(重命名为 MuiThemeProvider),所以在 _app 文件中只需要一个 Provider
ThemeContext.tsx
import { createContext, ReactNode, useContext } from 'react'
import { ThemeProvider as MuiThemeProvider, useMediaQuery } from '@mui/material'
import lightTheme from '@/themes/light'
import darkTheme from '@/themes/dark'
import useLocalStorage from '@/react/hooks/useLocalStorage'
const DARK_SCHEME_QUERY = '(prefers-color-scheme: dark)'
type ThemeMode = 'light' | 'dark'
interface ThemeContextType {
themeMode: ThemeMode
toggleTheme: () => void
}
const ThemeContext = createContext<ThemeContextType>({} as ThemeContextType)
const useThemeContext = () => useContext(ThemeContext)
const ThemeProvider = ({ children }: { children: ReactNode }) => {
const isDarkOS = useMediaQuery(DARK_SCHEME_QUERY)
const [themeMode, setThemeMode] = useLocalStorage<ThemeMode>('themeMode', isDarkOS ? 'light' : 'dark')
const toggleTheme = () => {
switch (themeMode) {
case 'light':
setThemeMode('dark')
break
case 'dark':
setThemeMode('light')
break
default:
}
}
return (
<ThemeContext.Provider value={{ themeMode, toggleTheme }}>
<MuiThemeProvider theme={themeMode === 'light' ? lightTheme : darkTheme}>
{children}
</MuiThemeProvider>
</ThemeContext.Provider>
)
}
export {
useThemeContext,
ThemeProvider
}
_app.tsx
export default function MyApp(props: MyAppProps) {
const { Component, emotionCache = clientSideEmotionCache, pageProps } = props
const getLayout = Component.getLayout ?? ((page) => page)
return (
<CacheProvider value={emotionCache}>
<Head>
<meta name="viewport" content="initial-scale=1, width=device-width" />
</Head>
<ThemeProvider>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
{getLayout(<Component {...pageProps} />)}
</ThemeProvider>
</CacheProvider>
)
}
创建 useLocalStorage 钩子:
我从here 获取了源代码,并对其进行了一些修改,以便由于 ssr 和客户端渲染不匹配而可以与 Next.js 一起正常工作。
useLocalStorage 还使用另一个 useEventListener 挂钩,以在所有其他打开的选项卡之间同步值的更改。
使用LocalStorage.tsx
// edited from source: https://usehooks-ts.com/react-hook/use-local-storage
// to support ssr in Next.js
import { Dispatch, SetStateAction, useEffect, useState } from 'react'
import useEventListener from '@/react/hooks/useEventListener'
type SetValue<T> = Dispatch<SetStateAction<T>>
function useLocalStorage<T>(key: string, initialValue: T): [T, SetValue<T>] {
// Read local storage the parse stored json or return initialValue
const readStorage = (): T => {
if (typeof window === 'undefined') {
return initialValue
}
try {
const item = window.localStorage.getItem(key)
return item ? (parseJSON(item) as T) : initialValue
} catch (error) {
console.warn(`Error reading localStorage key “${key}”:`, error)
return initialValue
}
}
// Persists the new value to localStorage.
const setStorage: SetValue<T> = value => {
if (typeof window == 'undefined') {
console.warn(
`Tried setting localStorage key “${key}” even though environment is not a client`,
)
}
try {
// Allow value to be a function so we have the same API as useState
const newValue = value instanceof Function ? value(state) : value
// Save to local storage
window.localStorage.setItem(key, JSON.stringify(newValue))
// We dispatch a custom event so every useLocalStorage hook are notified
window.dispatchEvent(new Event('local-storage'))
} catch (error) {
console.warn(`Error setting localStorage key “${key}”:`, error)
}
}
// State to store the value
const [state, setState] = useState<T>(initialValue)
// Once the component is mounted, read from localStorage and update state.
useEffect(() => {
setState(readStorage())
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
setStorage(state)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state])
const handleStorageChange = () => {
setState(readStorage())
}
// this only works for other documents, not the current one
useEventListener('storage', handleStorageChange)
// this is a custom event, triggered in writeValueToLocalStorage
// See: useLocalStorage()
useEventListener('local-storage', handleStorageChange)
return [state, setState]
}
export default useLocalStorage
// A wrapper for "JSON.parse()"" to support "undefined" value
function parseJSON<T>(value: string | null): T | undefined {
try {
return value === 'undefined' ? undefined : JSON.parse(value ?? '')
} catch (error) {
console.log('parsing error on', { value })
return undefined
}
}
useEventListener.tsx 与网络中的完全相同。
代码
此示例的所有代码都可以在我的github repository 中找到,它们可以用作带有 Typescript、Nextjs 和 Material UI 的项目的起点。
你也可以试试example here。