Как установить темный режим предпочтения системы в приложении реакции, но также разрешить пользователям переключаться между текущей темой - PullRequest
2 голосов
/ 09 апреля 2020

У меня есть веб-приложение реагирования с переключением тем на панели навигации. У меня есть ThemeProvider Context, в котором есть logi c для автоматического определения предпочтения системной темы пользователя и его установки. Тем не менее, я считаю, что пользователь должен иметь возможность переключать темы назад и вперед на веб-сайте, несмотря на свои системные предпочтения. Вот файл ThemeContext.js со всеми логиками темы c, включая метод 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. В моем текущем коде, когда вызывается toggle, кажется, что изменения состояния происходят дважды, и поэтому тема остается неизменной. Как убедиться, что метод toggle работает правильно?

Вот веб-приложение в вопросе

Ответы [ 2 ]

1 голос
/ 09 апреля 2020

Несмотря на то, что решение Барри работает, обратите внимание, что вместо добавления большего количества кода вы могли бы добиться того же результата, если его скимминг:

Ключ заключается в том, чтобы установить предпочтение пользователя в качестве исходного состояния и прекратить проверять его в эффект:

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]);
...

Пример CodeSandbox

1 голос
/ 09 апреля 2020

Проблема в том, что весь блок useLayoutEffect выполняется при каждом изменении значения dark. Поэтому, когда пользователь переключает dark, выполняются операторы prefers... if и setDark возвращаются к системным настройкам.

Чтобы решить эту проблему, вам нужно будет отслеживать, как пользователь вручную переключает тему и затем запретить запуск операторов prefers... if.

В вашем ThemeProvider выполните следующее:

  • Добавьте состояние для мониторинга, если пользователь использовал переключение
const [userPicked, setUserPicked] = useState(false);
  • Обновите функцию toggle:
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]);

  ...

}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...