簡體   English   中英

如何在反應應用程序中設置系統偏好暗模式,但還允許用戶來回切換當前主題

[英]How do I set system preference dark mode in a react app but also allow users to toggle back and forth the current theme

我有一個反應 web 應用程序,導航上有一個主題切換。 我有一個ThemeProvider Context ,它具有自動檢測用戶的系統主題偏好並設置它的邏輯。 但是,我覺得用戶應該能夠在網站上來回切換主題,盡管他們有系統偏好。 這是ThemeContext.js文件,其中包含所有主題邏輯,包括toggle方法。

import React, { useState, useLayoutEffect } from 'react';

const ThemeContext = React.createContext({
    dark: false,
    toggle: () => {},
});

export default ThemeContext;

export function ThemeProvider({ children }) {
    // keeps state of the current theme
    const [dark, setDark] = useState(false);

    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)')
        .matches;
    const prefersLight = window.matchMedia('(prefers-color-scheme: light)')
        .matches;
    const prefersNotSet = window.matchMedia(
        '(prefers-color-scheme: no-preference)'
    ).matches;

    // paints the app before it renders elements
    useLayoutEffect(() => {
        // Media Hook to check what theme user prefers
        if (prefersDark) {
            setDark(true);
        }

        if (prefersLight) {
            setDark(false);
        }

        if (prefersNotSet) {
            setDark(true);
        }

        applyTheme();

        // if state changes, repaints the app
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [dark]);

    // rewrites set of css variablels/colors
    const applyTheme = () => {
        let theme;
        if (dark) {
            theme = darkTheme;
        }
        if (!dark) {
            theme = lightTheme;
        }

        const root = document.getElementsByTagName('html')[0];
        root.style.cssText = theme.join(';');
    };

    const toggle = () => {
        console.log('Toggle Method Called');

        // A smooth transition on theme switch
        const body = document.getElementsByTagName('body')[0];
        body.style.cssText = 'transition: background .5s ease';

        setDark(!dark);
    };

    return (
        <ThemeContext.Provider
            value={{
                dark,
                toggle,
            }}>
            {children}
        </ThemeContext.Provider>
    );
}

// styles
const lightTheme = [
    '--bg-color: var(--color-white)',
    '--text-color-primary: var(--color-black)',
    '--text-color-secondary: var(--color-prussianBlue)',
    '--text-color-tertiary:var(--color-azureRadiance)',
    '--fill-switch: var(--color-prussianBlue)',
    '--fill-primary:var(--color-prussianBlue)',
];

const darkTheme = [
    '--bg-color: var(--color-mirage)',
    '--text-color-primary: var(--color-white)',
    '--text-color-secondary: var(--color-iron)',
    '--text-color-tertiary: var(--color-white)',
    '--fill-switch: var(--color-gold)',
    '--fill-primary:var(--color-white)',
];

因此,當頁面加載時,顯示用戶的系統首選它們,但也允許用戶通過單擊觸發toggle function 的切換按鈕來切換主題。 在我當前的代碼中,當調用toggle時,似乎 state 更改發生了兩次,因此主題保持不變。 如何確保toggle方法正常工作?

這是有問題的web 應用程序

盡管 Barry 的解決方案有效,但請注意,您可以通過略讀獲得相同的結果,而不是添加更多代碼:

關鍵是將用戶的偏好設置為初始 state 並在效果中停止檢查:

export function ThemeProvider({ children }) {
    /* Because you are setting the initial theme to non-dark, 
    you can assume that your initial state should be dark only 
    when the user's preference is set to dark. */
    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)')
        .matches;

    // True if preference is set to dark, false otherwise.
    const [dark, setDark] = useState(prefersDark);
    /* Note: Initial state is set upon mounting, hence is better 
    to put the <ThemeProvider> up in your tree, close to the root <App> 
    to avoid unmounting it with the result of reverting to the default user 
    preference when and if re-mounting (unless you want that behaviour) */

    useLayoutEffect(() => {
        /* You end up here only when the user takes action 
        to change the theme, hence you can just apply the new theme. */
        applyTheme();
}, [dark]);
...

代碼沙盒示例

問題是useLayoutEffect的整個塊在每次dark值更改時都會運行。 因此,當用戶切換dark時,prefers prefers... if 語句運行並將setDark返回到系統首選項。

為了解決這個問題,您需要跟蹤用戶手動切換主題,然后阻止prefers... if 語句運行。

在您的ThemeProvider中執行以下操作:

  • 添加 state 以監控用戶是否使用了切換
const [userPicked, setUserPicked] = useState(false);
  • 更新您的toggle function:
const toggle = () => {
  console.log('Toggle Method Called');

  const body = document.getElementsByTagName('body')[0];
  body.style.cssText = 'transition: background .5s ease';

  setUserPick(true) // Add this line
  setDark(!dark);
};
  • 最后,將useLayout更新為如下所示:
useLayoutEffect(() => {
  if (!userPicked) { // This will stop the system preferences from taking place if the user manually toggles the them
    if (prefersDark) {
      setDark(true);
    }

    if (prefersLight) {
      setDark(false);
    }

    if (prefersNotSet) {
      setDark(true);
    }
  }

  applyTheme();
}, [dark]);

您的切換組件不必更改。

更新:

薩爾的回答是一個很好的選擇。 我指出了現有代碼中的缺陷以及如何添加它。 這指出了如何更有效地執行您的代碼。

export function ThemeProvider({ children }) {
  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;

  const [dark, setDark] = useState(prefersDark);

  useLayoutEffect(() => {
    applyTheme();
  }, [dark]);

  ...

}

為什么不簡單地使用useEffect

useEffect(() => {
  const prefersDark = window.matchMedia(
    "(prefers-color-scheme: dark)"
  ).matches;

  if (prefersDark) {
    setIsDark(true);
  }
}, []);

useEffect訪問window的原因: Window 未在 Next.js React app 中定義

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM