Einfacher Dunkelmodus (und mehrere Farbthemen!) in React

Avatar of Abram Thau
Abram Thau am

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

Ich arbeitete an einer großen React-Anwendung für ein Startup und wollte neben einigen guten Strategien zur Organisation unserer Stile auch dieses „Dark Mode“-Ding ausprobieren. Bei dem riesigen Ökosystem rund um React könnte man denken, dass es eine Standardlösung für Stilthemen gibt, aber eine kurze Websuche zeigt, dass dies wirklich nicht der Fall ist.

Es gibt viele verschiedene Optionen, aber viele davon knüpfen an sehr spezifische CSS-Strategien an, wie z. B. die Verwendung von CSS Modules, einer Form von CSS-in-JS usw. Ich habe auch Tools gefunden, die für bestimmte Frameworks wie Gatsby spezifisch sind, aber nicht für ein generisches React-Projekt. Was ich suchte, war ein grundlegendes System, das einfach einzurichten und zu verwenden ist, ohne viele Hürden überwinden zu müssen; etwas Schnelles, etwas Einfaches, mit dem sich ein ganzes Team von Front-End- und Full-Stack-Entwicklern schnell vertraut machen kann.

Die bestehende Lösung, die mir am besten gefiel, konzentrierte sich auf die Verwendung von CSS-Variablen und Datenattributen, gefunden in dieser StackOverflow-Antwort. Aber das stützte sich auch auf einige useRef-Sachen, die sich etwas umständlich anfühlten. Wie man in jeder Teleshopping-Sendung sagt: es muss einen besseren Weg geben!

Zum Glück gibt es den. Durch die Kombination dieser allgemeinen CSS-Variablenstrategie mit dem schönen useLocalStorage Hook haben wir ein leistungsstarkes, einfach zu bedienendes Theming-System. Ich werde Sie durch die Einrichtung und Ausführung führen, beginnend mit einer brandneuen React-App. Und wenn Sie bis zum Ende dabei bleiben, zeige ich Ihnen auch, wie Sie es mit react-scoped-css integrieren können, was dies zu meiner absolut bevorzugten Methode für die Arbeit mit CSS in React macht.

Projekteinrichtung

Nehmen wir das an ein sehr guter Ausgangspunkt: den Anfang.

Diese Anleitung setzt grundlegende Kenntnisse in CSS, JavaScript und React voraus.

Stellen Sie zuerst sicher, dass Sie eine aktuelle Version von Node und npm installiert haben. Navigieren Sie dann zu dem Ordner, in dem Ihr Projekt leben soll, führen Sie dort git bash aus (oder Ihr bevorzugtes Kommandozeilen-Tool) und führen Sie dann

npx create-react-app easy-react-themes --template typescript

Ersetzen Sie easy-react-themes durch den Namen Ihres Projekts und lassen Sie die Option --template typescript weg, wenn Sie lieber in JavaScript arbeiten möchten. Ich mag TypeScript, aber für diese Anleitung spielt es ehrlich gesagt keine Rolle, abgesehen davon, dass die Dateien mit .ts/.tsx statt mit .js/.jsx enden.

Öffnen wir nun unser brandneues Projekt in einem Code-Editor. Ich verwende VS Code für dieses Beispiel, und wenn Sie dasselbe tun, können Sie diese Befehle ausführen

cd easy-react-themes
code .
Noch nicht viel zu sehen, aber das werden wir ändern!

Wenn Sie als Nächstes npm start ausführen, wird Ihr Entwicklungsserver gestartet und dies in einem neuen Browserfenster angezeigt

Und installieren Sie schließlich noch das Paket use-local-storage mit

npm i use-local-storage

Das war's für die anfängliche Einrichtung des Projekts!

Code-Setup

Öffnen Sie die Datei App.tsx und entfernen Sie den unnötigen Teil.

Wir wollen von diesem hier...
...zu diesem.

Löschen Sie den gesamten Inhalt in App.css

Juhu! Jetzt erstellen wir unsere Themes! Öffnen Sie die Datei index.css und fügen Sie Folgendes hinzu

:root {
  --background: white;
  --text-primary: black;
  --text-secondary: royalblue;
  --accent: purple;
}
[data-theme='dark'] {
  --background: black;
  --text-primary: white;
  --text-secondary: grey;
  --accent: darkred;
}

Hier ist, was wir bisher haben:

Sehen Sie, was wir gerade getan haben? Wenn Sie mit CSS Custom Properties (auch bekannt als CSS-Variablen) nicht vertraut sind, ermöglichen sie uns, einen Wert zu definieren, der anderswo in unseren Stylesheets verwendet wird, mit dem Muster --key: value. In diesem Fall definieren wir nur ein paar Farben und wenden sie auf das :root-Element an, damit sie überall dort verwendet werden können, wo wir sie im gesamten React-Projekt benötigen.

Der zweite Teil, beginnend mit [data-theme='dark'], ist, wo die Dinge interessant werden. HTML (und JSX, das wir verwenden, um HTML in React zu erstellen) erlaubt uns, völlig beliebige Eigenschaften für unsere HTML-Elemente mit dem data-*-Attribut zu setzen. In diesem Fall geben wir dem äußersten <div>-Element unserer Anwendung ein data-theme-Attribut und wechseln seinen Wert zwischen light und dark. Wenn es dark ist, überschreibt der CSS[data-theme='dark']-Abschnitt die von uns in :root definierten Variablen, sodass jede Formatierung, die von diesen Variablen abhängt, ebenfalls umgeschaltet wird.

Lassen Sie uns das in die Praxis umsetzen. Zurück in App.tsx, geben wir React eine Möglichkeit, den Theme-Zustand zu verfolgen. Normalerweise würden wir etwas wie useState für lokalen Zustand oder Redux für globales Zustandsmanagement verwenden, aber wir möchten auch, dass die Theme-Auswahl des Benutzers bestehen bleibt, wenn er unsere App verlässt und später zurückkehrt. Während wir Redux und redux-persist verwenden könnten, ist das für unsere Bedürfnisse viel zu übertrieben.

Stattdessen verwenden wir den useLocalStorage Hook, den wir zuvor installiert haben. Er gibt uns eine Möglichkeit, Dinge im lokalen Speicher zu speichern, wie Sie vielleicht erwarten, aber als React Hook behält er den zustandsbezogenen Kenntnisstand darüber, was er mit localStorage macht, was unser Leben einfacher macht.

Einige von Ihnen denken vielleicht: „Oh nein, was ist, wenn die Seite gerendert wird, bevor mein JavaScript sich mit localStorage meldet und wir den gefürchteten „Flash of wrong theme“ bekommen?“ Aber das müssen Sie hier nicht befürchten, da unsere React-App vollständig clientseitig gerendert wird; die anfängliche HTML-Datei ist im Grunde ein Skelett mit einem einzigen <div>, an das React die App anhängt. Alle endgültigen HTML-Elemente werden von JavaScript generiert, nachdem localStorage überprüft wurde.

Importieren Sie also zuerst den Hook oben in App.tsx mit

import useLocalStorage from 'use-local-storage'

Dann verwenden wir ihn innerhalb unserer App-Komponente mit

const defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const [theme, setTheme] = useLocalStorage('theme', defaultDark ? 'dark' : 'light');

Das tut ein paar Dinge für uns. Erstens prüfen wir, ob der Benutzer eine Theme-Präferenz in seinen Browsereinstellungen festgelegt hat. Dann erstellen wir eine zustandsbezogene theme-Variable, die an localStorage gebunden ist, und die setTheme-Funktion, um theme zu aktualisieren. useLocalStorage fügt ein key:value-Paar zu localStorage hinzu, wenn es noch nicht existiert, das standardmäßig auf theme: "light" gesetzt ist, es sei denn, unsere matchMedia-Prüfung ergibt true, in diesem Fall ist es theme: "dark". Auf diese Weise handhaben wir beide Möglichkeiten elegant: die Theme-Einstellungen für einen wiederkehrenden Benutzer beibehalten oder standardmäßig seine Browsereinstellungen berücksichtigen, wenn wir es mit neuen Benutzern zu tun haben.

Als Nächstes fügen wir der App-Komponente ein paar kleine Inhalte hinzu, damit wir Elemente zum Stylen haben, zusammen mit einem Button und einer Funktion, die uns tatsächlich erlaubt, das Theme umzuschalten.

Die fertige Datei App.tsx

Die Geheimzutat ist in Zeile 14, wo wir data-theme={theme} zu unserem <div> auf oberster Ebene hinzugefügt haben. Indem wir nun den Wert von theme ändern, wählen wir aus, ob wir die CSS-Variablen in :root mit denen im Abschnitt data-theme='dark' der Datei index.css überschreiben möchten oder nicht.

Das Letzte, was wir tun müssen, ist, einige Stile hinzuzufügen, die diese CSS-Variablen verwenden, die wir zuvor erstellt haben, und es wird laufen! Öffnen Sie App.css und fügen Sie diesen CSS-Code dort ein

.App {
  color: var(--text-primary);
  background-color: var(--background);
  font-size: large;
  font-weight: bold;
  padding: 20px;
  height: calc(100vh - 40px);
  transition: all .5s;
}
button {
  color: var(--text-primary);
  background-color: var(--background);
  border: 2px var(--text-primary) solid;
  float: right;
  transition: all .5s;
}

Jetzt hängen der Hintergrund und der Text für das Haupt-<div> sowie der Hintergrund, der Text und die Umrandung des <button> von den CSS-Variablen ab. Das bedeutet, wenn sich das Theme ändert, aktualisiert sich alles, was von diesen Variablen abhängt. Beachten Sie auch, dass wir transition: all .5s sowohl zu App als auch zu <button> für einen reibungslosen Übergang zwischen den Farbthemen hinzugefügt haben.

Nun gehen Sie zurück zum Browser, der die App ausführt, und hier ist, was wir erhalten

Tada! Fügen wir eine weitere Komponente hinzu, nur um zu zeigen, wie das System funktioniert, wenn wir eine echte App bauen. Wir fügen einen /components-Ordner in /src hinzu, einen /square-Ordner in /components und fügen eine Square.tsx und eine square.css hinzu, so

Importieren wir es zurück in App.tsx, so

Hier ist, was wir jetzt als Ergebnis haben

Und da haben wir es! Offensichtlich ist das ein ziemlich einfaches Szenario, bei dem wir nur ein Standard (helles) Theme und ein zweites (dunkles) Theme verwenden. Aber wenn Ihre Anwendung es erfordert, könnte dieses System verwendet werden, um mehrere Theme-Optionen zu implementieren. Persönlich denke ich darüber nach, meinem nächsten Projekt Optionen für hell, dunkel, schokolade und erdbeere zu geben – lassen Sie Ihrer Kreativität freien Lauf!

Bonus: Integration mit React Scoped CSS

Die Verwendung von React Scoped CSS ist meine bevorzugte Methode, um die CSS-Datei jeder Komponente gekapselt zu halten, um Namenskollisionen und unbeabsichtigte Stil-Vererbung zu vermeiden. Mein bisheriger Favorit dafür war CSS Modules, aber das hat den Nachteil, dass das DOM im Browser aussieht, als hätte ein Roboter alle Klassennamen geschrieben… weil genau das der Fall ist. Dieser Mangel an menschlicher Lesbarkeit macht das Debugging viel mühsamer, als es sein müsste. Hier kommt React Scoped CSS ins Spiel. Wir können weiterhin CSS (oder Sass) genau so schreiben, wie wir es getan haben, und die Ausgabe sieht aus, als hätte sie ein Mensch geschrieben.

Da das React Scoped CSS Repository vollständige und detaillierte Installationsanleitungen bietet, fasse ich diese hier nur kurz zusammen.

Installieren und konfigurieren Sie zuerst Create React App Configuration Override (CRACO) gemäß deren Anweisungen. Craco ist ein Tool, das es uns ermöglicht, die Standard-Webpack-Konfiguration, die in Create-React-App (CRA) gebündelt ist, zu überschreiben. Normalerweise müssen Sie ein Projekt „ejecten“, wenn Sie Webpack in einem CRA-Projekt anpassen möchten, was eine unumkehrbare Operation ist und Sie vollständig für alle Abhängigkeiten verantwortlich macht, die normalerweise von anderen gehandhabt werden. Sie möchten das Ejecten normalerweise vermeiden, es sei denn, Sie wissen wirklich, wirklich, was Sie tun, und haben einen guten Grund, diesen Weg zu gehen. Stattdessen erlaubt uns CRACO, einige kleinere Anpassungen an unserer Webpack-Konfiguration vorzunehmen, ohne dass die Dinge unübersichtlich werden.

Sobald das erledigt ist, installieren Sie das React Scoped CSS-Paket

npm i craco-plugin-scoped-css

(Die README-Anweisungen verwenden yarn zur Installation anstelle von npm, aber beides ist in Ordnung.) Jetzt, da es installiert ist, benennen Sie einfach die CSS-Dateien um, indem Sie .scoped vor .css hinzufügen, so

app.css -> app.scoped.css

Und wir müssen sicherstellen, dass wir einen neuen Namen für den Import dieser CSS-Datei in eine Komponente verwenden

import './app.css'; -> import './app.scoped.css';

Jetzt ist das gesamte CSS gekapselt, sodass es nur auf die Komponenten angewendet wird, in die es importiert wird. Es funktioniert durch die Verwendung von data-*-Eigenschaften, ähnlich wie unser Theming-System. Wenn also eine gescoped CSS-Datei in eine Komponente importiert wird, werden alle Elemente dieser Komponente mit einer Eigenschaft wie data-v-46ef2374 gekennzeichnet, und die Stile aus dieser Datei werden so umschlossen, dass sie nur auf Elemente mit dieser exakten Daten-Eigenschaft angewendet werden.

Das ist alles wunderbar, aber der kleine Trick, damit das mit diesem Theming-System funktioniert, ist, dass wir **nicht** wollen, dass die CSS-Variablen gekapselt werden; wir wollen, dass sie auf das gesamte Projekt angewendet werden. Daher ändern wir index.css einfach nicht so, dass es scoped enthält… mit anderen Worten, wir können diese CSS-Datei unverändert lassen. Das ist alles! Jetzt haben wir ein leistungsfähiges Theming-System, das im Einklang mit Scoped CSS funktioniert – wir leben den Traum!

Vielen Dank, dass Sie sich die Zeit genommen haben, diese Anleitung zu lesen. Wenn sie Ihnen geholfen hat, etwas Tolles zu bauen, würde ich mich freuen, davon zu hören!