【问题标题】:Material ui dark mode reset when page refreshing?页面刷新时Material ui暗模式重置?
【发布时间】:2022-01-10 19:17:11
【问题描述】:

我在我的nextjs 应用程序中使用mui5(Material Ui)。我正在尝试实现dark mode。一切顺利。我想要一个功能,如果任何用户切换暗模式,那么它将保存在本地存储中。然后当我刷新页面时,它会根据本地存储的值自动从本地存储和活动暗或亮模式获取值。如果用户第一次来到该站点,那么它应该自动激活系统偏好模式。我的意思是如果本地存储中没有值,那么它应该自动激活系统偏好模式。我该怎么做。

这是我的代码-

_app.js

export default function MyApp(props) {
  const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;
  const [mode, setMode] = React.useState("light");
  const colorMode = React.useMemo(
    () => ({
      // The dark mode switch would invoke this method
      toggleColorMode: () => {
        setMode((prevMode) =>
          prevMode === 'light' ? 'dark' : 'light',
        );
      },
    }),
    [],
  );

  // Update the theme only if the mode changes
  const muiTheme = React.useMemo(() => createTheme(theme(mode)), [mode]);
  return (
    <ColorModeContext.Provider value={colorMode}>
      <CacheProvider value={emotionCache}>
        <Head>
          <meta name="viewport" content="initial-scale=1, width=device-width" />
        </Head>

        <ThemeProvider theme={muiTheme}>
          <CssBaseline />
          <Component {...pageProps} />
        </ThemeProvider>
      </CacheProvider>
    </ColorModeContext.Provider>
  );
}

MyApp.propTypes = {
  Component: PropTypes.elementType.isRequired,
  emotionCache: PropTypes.object,
  pageProps: PropTypes.object.isRequired,
};

工具按钮-

const theme = useTheme();
const colorMode = useContext(ColorModeContext);

<FormControlLabel
   control={<MaterialUISwitch sx={{ m: 1 }} checked={theme.palette.mode === 'dark' ? false : true} />}
   label=""
   sx={{ mx: "0px" }}
   onClick={colorMode.toggleColorMode}
/>

【问题讨论】:

标签: reactjs material-ui next.js


【解决方案1】:

您可以在这里查看我使用 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

【讨论】:

  • 很好的例子,但它有闪烁。
  • 闪烁是什么意思?对我来说,工作时根本没有闪烁。我已经用了很长时间了,并且有几个项目没有任何问题。
【解决方案2】:
  toggleColorMode: () => {
        //use this line for save in localStorage
        localStorage.setItem("mode",mode=== 'light' ? 'dark' : 'light' )
        setMode((prevMode) =>
          prevMode === 'light' ? 'dark' : 'light',
        );
      },

然后写一个useEffect根据localStorage设置模式

useEffect(()=>{
   if( localStorage.getItem("mode")){
    setMode(localStorage.getItem("mode"))
     }
},[])

【讨论】:

    猜你喜欢
    • 2022-06-17
    • 1970-01-01
    • 1970-01-01
    • 2021-03-05
    • 2013-07-12
    • 2012-08-20
    • 1970-01-01
    • 1970-01-01
    • 2019-10-07
    相关资源
    最近更新 更多