Ein Dark-Mode-Umschalter mit React und ThemeProvider

Avatar of Maks Akymenko
Maks Akymenko am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Ich mag es, wenn Websites eine Dunkelmodus-Option haben. Der Dunkelmodus macht Webseiten für mich leichter lesbar und hilft meinen Augen, sich zu entspannen. Viele Websites, darunter YouTube und Twitter, haben ihn bereits implementiert, und wir sehen ihn auch auf vielen anderen Seiten.

In diesem Tutorial bauen wir einen Umschalter, der es Benutzern ermöglicht, zwischen hellem und dunklem Modus zu wechseln, unter Verwendung eines <ThemeProvider Wrappers aus der styled-components Bibliothek. Wir erstellen einen benutzerdefinierten Hook useDarkMode, der die prefers-color-scheme Media-Query unterstützt, um den Modus entsprechend den Farbschemaeinstellungen des Benutzers im BS festzulegen.

Wenn das schwierig klingt, verspreche ich, das ist es nicht! Lasst uns eintauchen und es umsetzen.

Siehe den Pen
Tag/Nacht-Modus-Umschalter mit React und ThemeProvider
von Maks Akymenko (@maximakymenko)
auf CodePen.

Lasst uns alles einrichten

Wir werden create-react-app verwenden, um ein neues Projekt zu initiieren

npx create-react-app my-app
cd my-app
yarn start

Öffnen Sie als Nächstes ein separates Terminalfenster und installieren Sie styled-components

yarn add styled-components

Als Nächstes erstellen wir zwei Dateien. Die erste ist global.js, die unsere Basisstile enthält, und die zweite ist theme.js, die Variablen für unsere dunklen und hellen Themen enthält

// theme.js
export const lightTheme = {
  body: '#E2E2E2',
  text: '#363537',
  toggleBorder: '#FFF',
  gradient: 'linear-gradient(#39598A, #79D7ED)',
}

export const darkTheme = {
  body: '#363537',
  text: '#FAFAFA',
  toggleBorder: '#6B8096',
  gradient: 'linear-gradient(#091236, #1E215D)',
}

Sie können die Variablen beliebig anpassen, da dieser Code nur zu Demonstrationszwecken verwendet wird.

// global.js
// Source: https://github.com/maximakymenko/react-day-night-toggle-app/blob/master/src/global.js#L23-L41

import { createGlobalStyle } from 'styled-components';

export const GlobalStyles = createGlobalStyle`
  *,
  *::after,
  *::before {
    box-sizing: border-box;
  }

  body {
    align-items: center;
    background: ${({ theme }) => theme.body};
    color: ${({ theme }) => theme.text};
    display: flex;
    flex-direction: column;
    justify-content: center;
    height: 100vh;
    margin: 0;
    padding: 0;
    font-family: BlinkMacSystemFont, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
    transition: all 0.25s linear;
  }

Gehen Sie zur Datei App.js. Wir werden alles darin löschen und das Layout für unsere App hinzufügen. Hier ist, was ich getan habe

import React from 'react';
import { ThemeProvider } from 'styled-components';
import { lightTheme, darkTheme } from './theme';
import { GlobalStyles } from './global';

function App() {
  return (
    <ThemeProvider theme={lightTheme}>
      <>
        <GlobalStyles />
        <button>Toggle theme</button>
        <h1>It's a light theme!</h1>
        <footer>
        </footer>
      </>
    </ThemeProvider>
  );
}

export default App;

Dies importiert unsere hellen und dunklen Themen. Die ThemeProvider Komponente wird ebenfalls importiert und erhält die hellen Theme-Stile (lightTheme) übergeben. Wir importieren auch GlobalStyles, um alles an einem Ort zu bündeln.

Hier ist ungefähr, was wir bisher haben

Nun zur Umschaltfunktionalität

Es gibt noch kein magisches Umschalten zwischen den Themen, also implementieren wir die Umschaltfunktionalität. Wir werden nur ein paar Codezeilen benötigen, um sie zum Laufen zu bringen.

Importieren Sie zuerst den useState Hook von react

// App.js
import React, { useState } from 'react';

Verwenden Sie als Nächstes den Hook, um einen lokalen Zustand zu erstellen, der das aktuelle Thema verfolgt, und fügen Sie eine Funktion hinzu, um beim Klicken zwischen den Themen zu wechseln

// App.js
const [theme, setTheme] = useState('light');

// The function that toggles between themes
const toggleTheme = () => {
  // if the theme is not light, then set it to dark
  if (theme === 'light') {
    setTheme('dark');
  // otherwise, it should be light
  } else {
    setTheme('light');
  }
}

Danach muss nur noch diese Funktion an unser Schaltflächenelement übergeben und das Thema bedingt geändert werden. Schauen Sie mal

// App.js
import React, { useState } from 'react';
import { ThemeProvider } from 'styled-components';
import { lightTheme, darkTheme } from './theme';
import { GlobalStyles } from './global';

// The function that toggles between themes
function App() {
  const [theme, setTheme] = useState('light');
  const toggleTheme = () => {
    if (theme === 'light') {
      setTheme('dark');
    } else {
      setTheme('light');
    }
  }
  
  // Return the layout based on the current theme
  return (
    <ThemeProvider theme={theme === 'light' ? lightTheme : darkTheme}>
      <>
        <GlobalStyles />
        // Pass the toggle functionality to the button
        <button onClick={toggleTheme}>Toggle theme</button>
        <h1>It's a light theme!</h1>
        <footer>
        </footer>
      </>
    </ThemeProvider>
  );
}

export default App;

Wie funktioniert es?

// global.js
background: ${({ theme }) => theme.body};
color: ${({ theme }) => theme.text};
transition: all 0.25s linear;

Zuvor haben wir in unseren GlobalStyles die Eigenschaften background und color auf Werte aus dem theme Objekt gesetzt. Jetzt, jedes Mal, wenn wir den Schalter umlegen, ändern sich die Werte je nach den Objekten darkTheme und lightTheme, die wir an ThemeProvider übergeben. Die Eigenschaft transition ermöglicht es uns, diese Änderung etwas reibungsloser zu gestalten, als mit Keyframe-Animationen zu arbeiten.

Nun brauchen wir die Umschaltkomponente

Wir sind hier im Grunde fertig, denn jetzt wisst ihr, wie man eine Umschaltfunktionalität erstellt. Wir können es aber immer besser machen, also verbessern wir die App, indem wir eine benutzerdefinierte Toggle Komponente erstellen und unsere Umschaltfunktionalität wiederverwendbar machen. Das ist einer der Hauptvorteile, wenn man das in React macht, oder?

Wir behalten alles der Einfachheit halber in einer Datei. Erstellen wir also eine neue Datei namens Toggle.js und fügen Sie Folgendes hinzu

// Toggle.js
import React from 'react'
import { func, string } from 'prop-types';
import styled from 'styled-components';
// Import a couple of SVG files we'll use in the design: https://www.flaticon.com
import { ReactComponent as MoonIcon } from 'icons/moon.svg';
import { ReactComponent as SunIcon } from 'icons/sun.svg';

const Toggle = ({ theme, toggleTheme }) => {
  const isLight = theme === 'light';
  return (
    <button onClick={toggleTheme} >
      <SunIcon />
      <MoonIcon />
    </button>
  );
};

Toggle.propTypes = {
  theme: string.isRequired,
  toggleTheme: func.isRequired,
}

export default Toggle;

Sie können Icons von hier und hier herunterladen. Und wenn wir Icons als Komponenten verwenden wollen, denken Sie daran, sie als React-Komponenten zu importieren.

Wir haben zwei Props übergeben: theme liefert das aktuelle Thema (hell oder dunkel) und die Funktion toggleTheme wird verwendet, um zwischen ihnen umzuschalten. Unten haben wir eine Variable isLight erstellt, die einen booleschen Wert zurückgibt, abhängig von unserem aktuellen Thema. Diesen werden wir später an unsere gestylte Komponente übergeben.

Wir haben auch eine styled Funktion von styled-components importiert, also lassen Sie sie uns verwenden. Sie können dies am Anfang Ihrer Datei nach den Imports hinzufügen oder eine dedizierte Datei dafür erstellen (z. B. Toggle.styled.js), wie ich es unten getan habe. Wiederum, dies ist rein zur Präsentation gedacht, so dass Sie Ihre Komponente so stylen können, wie Sie es für richtig halten.

// Toggle.styled.js
const ToggleContainer = styled.button`
  background: ${({ theme }) => theme.gradient};
  border: 2px solid ${({ theme }) => theme.toggleBorder};
  border-radius: 30px;
  cursor: pointer;
  display: flex;
  font-size: 0.5rem;
  justify-content: space-between;
  margin: 0 auto;
  overflow: hidden;
  padding: 0.5rem;
  position: relative;
  width: 8rem;
  height: 4rem;

  svg {
    height: auto;
    width: 2.5rem;
    transition: all 0.3s linear;
    
    // sun icon
    &:first-child {
      transform: ${({ lightTheme }) => lightTheme ? 'translateY(0)' : 'translateY(100px)'};
    }
    
    // moon icon
    &:nth-child(2) {
      transform: ${({ lightTheme }) => lightTheme ? 'translateY(-100px)' : 'translateY(0)'};
    }
  }
`;

Das Importieren von Icons als Komponenten ermöglicht es uns, die Stile der SVG-Icons direkt zu ändern. Wir prüfen, ob das lightTheme aktiv ist, und wenn ja, bewegen wir das entsprechende Icon aus dem sichtbaren Bereich – so ähnlich wie der Mond verschwindet, wenn es Tag ist, und umgekehrt.

Vergessen Sie nicht, die Schaltfläche in Toggle.js durch die ToggleContainer Komponente zu ersetzen, unabhängig davon, ob Sie in einer separaten Datei oder direkt in Toggle.js stylen. Stellen Sie sicher, dass Sie die Variable isLight an sie übergeben, um das aktuelle Thema anzugeben. Ich habe die Prop lightTheme genannt, damit sie ihren Zweck klar widerspiegelt.

Das Letzte, was zu tun ist, ist, unsere Komponente in App.js zu importieren und ihr die erforderlichen Props zu übergeben. Außerdem habe ich eine Bedingung hinzugefügt, um zwischen „light“ und „dark“ im Titel umzuschalten, wenn sich das Thema ändert, um etwas mehr Interaktivität zu erzielen.

// App.js
<Toggle theme={theme} toggleTheme={toggleTheme} />
<h1>It's a {theme === 'light' ? 'light theme' : 'dark theme'}!</h1>

Vergessen Sie nicht, die Autoren von flaticon.com für die Bereitstellung der Icons zu nennen.

// App.js
<span>Credits:</span>
<small><b>Sun</b> icon made by <a href="https://www.flaticon.com/authors/smalllikeart">smalllikeart</a> from <a href="https://www.flaticon.com">www.flaticon.com</a></small>
<small><b>Moon</b> icon made by <a href="https://www.freepik.com/home">Freepik</a> from <a href="https://www.flaticon.com">www.flaticon.com</a></small>

Das ist schon besser

Der useDarkMode Hook

Beim Erstellen einer Anwendung sollten wir bedenken, dass die Anwendung skalierbar sein muss, das heißt, wiederverwendbar, sodass wir sie an vielen Stellen oder sogar in verschiedenen Projekten verwenden können.

Deshalb wäre es großartig, wenn wir unsere Umschaltfunktionalität an einen separaten Ort verschieben könnten – warum also keinen dedizierten Hook dafür erstellen?

Erstellen wir eine neue Datei namens useDarkMode.js im src-Verzeichnis des Projekts und verschieben wir unsere Logik mit einigen Anpassungen in diese Datei

// useDarkMode.js
import { useEffect, useState } from 'react';

export const useDarkMode = () => {
  const [theme, setTheme] = useState('light');
  const toggleTheme = () => {
    if (theme === 'light') {
      window.localStorage.setItem('theme', 'dark')
      setTheme('dark')
    } else {
      window.localStorage.setItem('theme', 'light')
      setTheme('light')
    }
  };

  useEffect(() => {
    const localTheme = window.localStorage.getItem('theme');
    localTheme && setTheme(localTheme);
  }, []);

  return [theme, toggleTheme]
};

Wir haben hier ein paar Dinge hinzugefügt. Wir möchten, dass unser Thema zwischen den Sitzungen im Browser erhalten bleibt. Wenn also jemand ein dunkles Thema gewählt hat, wird er es auch beim nächsten Besuch der App bekommen. Das ist eine riesige UX-Verbesserung. Aus diesen Gründen verwenden wir localStorage.

Wir haben auch den useEffect Hook implementiert, um beim Mounten der Komponente zu prüfen. Wenn der Benutzer zuvor ein Thema ausgewählt hat, übergeben wir es an unsere setTheme Funktion. Am Ende geben wir unser theme zurück, das das gewählte theme und die Funktion toggleTheme zum Umschalten zwischen den Modi enthält.

Nun implementieren wir den useDarkMode Hook. Gehen Sie zu App.js, importieren Sie den neu erstellten Hook, destrukturieren Sie unsere Eigenschaften theme und toggleTheme aus dem Hook und platzieren Sie sie dort, wo sie hingehören.

// App.js
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { useDarkMode } from './useDarkMode';
import { lightTheme, darkTheme } from './theme';
import { GlobalStyles } from './global';
import Toggle from './components/Toggle';

function App() {
  const [theme, toggleTheme] = useDarkMode();
  const themeMode = theme === 'light' ? lightTheme : darkTheme;

  return (
    <ThemeProvider theme={themeMode}>
      <>
        <GlobalStyles />
        <Toggle theme={theme} toggleTheme={toggleTheme} />
        <h1>It's a {theme === 'light' ? 'light theme' : 'dark theme'}!</h1>
        <footer>
          Credits:
          <small>Sun icon made by smalllikeart from www.flaticon.com</small>
          <small>Moon icon made by Freepik from www.flaticon.com</small>
        </footer>
      </>
    </ThemeProvider>
  );
}

export default App;

Dies funktioniert *fast* perfekt, aber es gibt eine kleine Sache, die wir tun können, um unsere Erfahrung zu verbessern. Wechseln Sie zum dunklen Thema und laden Sie die Seite neu. Sehen Sie, dass das Sonnensymbol für einen kurzen Moment vor dem Mondsymbol geladen wird?

Das passiert, weil unser useState Hook das light Thema initialisiert. Danach wird useEffect ausgeführt, prüft localStorage und setzt dann erst das theme auf dark.

Bisher habe ich zwei Lösungen gefunden. Die erste besteht darin, zu prüfen, ob ein Wert in localStorage in unserem useState vorhanden ist

// useDarkMode.js
const [theme, setTheme] = useState(window.localStorage.getItem('theme') || 'light');

Ich bin mir jedoch nicht sicher, ob es eine gute Praxis ist, solche Prüfungen innerhalb von useState durchzuführen. Lassen Sie mich Ihnen also eine zweite Lösung zeigen, die ich verwende.

Diese wird etwas komplizierter sein. Wir erstellen einen weiteren Zustand und nennen ihn componentMounted. Dann fügen wir innerhalb des useEffect Hooks, wo wir unser localTheme prüfen, eine else Anweisung hinzu. Wenn kein theme in localStorage vorhanden ist, fügen wir es hinzu. Danach setzen wir setComponentMounted auf true. Am Ende fügen wir componentMounted zu unserer Rückgabeanweisung hinzu.

// useDarkMode.js
import { useEffect, useState } from 'react';

export const useDarkMode = () => {
  const [theme, setTheme] = useState('light');
  const [componentMounted, setComponentMounted] = useState(false);
  const toggleTheme = () => {
    if (theme === 'light') {
      window.localStorage.setItem('theme', 'dark');
      setTheme('dark');
    } else {
      window.localStorage.setItem('theme', 'light');
      setTheme('light');
    }
  };

  useEffect(() => {
    const localTheme = window.localStorage.getItem('theme');
    if (localTheme) {
      setTheme(localTheme);
    } else {
      setTheme('light')
      window.localStorage.setItem('theme', 'light')
    }
    setComponentMounted(true);
  }, []);
  
  return [theme, toggleTheme, componentMounted]
};

Sie haben vielleicht bemerkt, dass wir einige Codefragmente wiederholen. Wir versuchen immer, das DRY-Prinzip beim Schreiben von Code zu befolgen, und hier haben wir die Möglichkeit, es zu nutzen. Wir können eine separate Funktion erstellen, die unseren Zustand setzt und theme an localStorage übergibt. Ich glaube, der beste Name dafür wäre setTheme, aber den haben wir schon verwendet, also nennen wir sie setMode.

// useDarkMode.js
const setMode = mode => {
  window.localStorage.setItem('theme', mode)
  setTheme(mode)
};

Mit dieser Funktion können wir unsere useDarkMode.js etwas refaktorieren

// useDarkMode.js
import { useEffect, useState } from 'react';
export const useDarkMode = () => {
  const [theme, setTheme] = useState('light');
  const [componentMounted, setComponentMounted] = useState(false);

  const setMode = mode => {
    window.localStorage.setItem('theme', mode)
    setTheme(mode)
  };

  const toggleTheme = () => {
    if (theme === 'light') {
      setMode('dark');
    } else {
      setMode('light');
    }
  };

  useEffect(() => {
    const localTheme = window.localStorage.getItem('theme');
    if (localTheme) {
      setTheme(localTheme);
    } else {
      setMode('light');
    }
    setComponentMounted(true);
  }, []);

  return [theme, toggleTheme, componentMounted]
};

Wir haben nur ein wenig Code geändert, aber er sieht viel besser aus und ist leichter zu lesen und zu verstehen!

Ist die Komponente gemountet?

Zurück zur Eigenschaft componentMounted. Wir werden sie verwenden, um zu prüfen, ob unsere Komponente gemountet wurde, denn das passiert im useEffect Hook.

Wenn das noch nicht geschehen ist, rendern wir ein leeres div.

// App.js
if (!componentMounted) {
  return <div />
};

Hier ist der vollständige Code für App.js

// App.js
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { useDarkMode } from './useDarkMode';
import { lightTheme, darkTheme } from './theme';
import { GlobalStyles } from './global';
import Toggle from './components/Toggle';

function App() {
  const [theme, toggleTheme, componentMounted] = useDarkMode();

  const themeMode = theme === 'light' ? lightTheme : darkTheme;

  if (!componentMounted) {
    return <div />
  };

  return (
    <ThemeProvider theme={themeMode}>
      <>
        <GlobalStyles />
        <Toggle theme={theme} toggleTheme={toggleTheme} />
        <h1>It's a {theme === 'light' ? 'light theme' : 'dark theme'}!</h1>
        <footer>
          <span>Credits:</span>
          <small><b>Sun</b> icon made by <a href="https://www.flaticon.com/authors/smalllikeart">smalllikeart</a> from <a href="https://www.flaticon.com">www.flaticon.com</a></small>
          <small><b>Moon</b> icon made by <a href="https://www.freepik.com/home">Freepik</a> from <a href="https://www.flaticon.com">www.flaticon.com</a></small>
        </footer>
      </>
    </ThemeProvider>
  );
}

export default App;

Verwendung des bevorzugten Farbschemas des Benutzers

Dieser Teil ist nicht zwingend erforderlich, aber er ermöglicht Ihnen eine noch bessere Benutzererfahrung. Diese Medienfunktion wird verwendet, um zu erkennen, ob der Benutzer gewünscht hat, dass die Seite ein helles oder dunkles Farbschema verwendet, basierend auf den Einstellungen in seinem BS. Wenn beispielsweise das Standard-Farbschema eines Benutzers auf einem Telefon oder Laptop auf dunkel eingestellt ist, passt Ihre Website ihr Farbschema entsprechend an. Es ist erwähnenswert, dass diese Media-Query noch ein Arbeitsprojekt ist und in der Media Queries Level 5 Spezifikation enthalten ist, die sich im Editor's Draft befindet.

Diese Browser-Support-Daten stammen von Caniuse, wo Sie weitere Details finden. Eine Zahl gibt an, dass der Browser die Funktion ab dieser Version unterstützt.

Desktop

ChromeFirefoxIEEdgeSafari
7667Nein7912.1

Mobil / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
12712712713.0-13.1

Die Implementierung ist ziemlich unkompliziert. Da wir mit einer Media-Query arbeiten, müssen wir in dem useEffect Hook prüfen, ob der Browser sie unterstützt, und das entsprechende Thema festlegen. Dazu verwenden wir window.matchMedia, um zu prüfen, ob es existiert und ob der Dunkelmodus unterstützt wird. Wir müssen auch an den localTheme denken, denn wenn er verfügbar ist, möchten wir ihn nicht mit dem Dunkelwert überschreiben, es sei denn, der Wert ist natürlich auf hell eingestellt.

Wenn alle Prüfungen bestanden sind, setzen wir das dunkle Thema.

// useDarkMode.js
useEffect(() => {
if (
  window.matchMedia &&
  window.matchMedia('(prefers-color-scheme: dark)').matches && 
  !localTheme
) {
  setTheme('dark')
  }
})

Wie bereits erwähnt, müssen wir an die Existenz von localTheme denken – deshalb müssen wir unsere bisherige Logik implementieren, in der wir danach gesucht haben.

Hier ist, was wir von früher hatten

// useDarkMode.js
useEffect(() => {
const localTheme = window.localStorage.getItem('theme');
  if (localTheme) {
    setTheme(localTheme);
  } else {
    setMode('light');
  }
})

Mischen wir es auf. Ich habe die if- und else-Anweisungen durch ternäre Operatoren ersetzt, um die Dinge etwas leserlicher zu gestalten.

// useDarkMode.js
useEffect(() => {
const localTheme = window.localStorage.getItem('theme');
window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches && !localTheme ?
  setMode('dark') :
  localTheme ?
    setTheme(localTheme) :
    setMode('light');})
})

Hier ist die Datei userDarkMode.js mit dem vollständigen Code

// useDarkMode.js
import { useEffect, useState } from 'react';

export const useDarkMode = () => {
  const [theme, setTheme] = useState('light');
  const [componentMounted, setComponentMounted] = useState(false);
  const setMode = mode => {
    window.localStorage.setItem('theme', mode)
    setTheme(mode)
  };

  const toggleTheme = () => {
    if (theme === 'light') {
      setMode('dark')
    } else {
      setMode('light')
    }
  };

  useEffect(() => {
    const localTheme = window.localStorage.getItem('theme');
    window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches && !localTheme ?
      setMode('dark') :
      localTheme ?
        setTheme(localTheme) :
        setMode('light');
    setComponentMounted(true);
  }, []);

  return [theme, toggleTheme, componentMounted]
};

Probieren Sie es aus! Es wechselt den Modus, speichert das Thema in localStorage und setzt das Standardthema entsprechend dem BS-Farbschema, falls es verfügbar ist.


Herzlichen Glückwunsch, mein Freund! Gut gemacht! Wenn Sie Fragen zur Implementierung haben, können Sie mir gerne eine Nachricht senden!