Iterating a React Design with Styled Components

Avatar of Cliff Hall
Cliff Hall am

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

In einer perfekten Welt hätten unsere Projekte unbegrenzte Ressourcen und Zeit. Unsere Teams würden mit durchdachten und hoch verfeinerten UX-Designs mit dem Coden beginnen. Es gäbe einen Konsens unter den Entwicklern über die beste Vorgehensweise beim Styling. Es gäbe einen oder mehrere CSS-Gurus im Team, die sicherstellen könnten, dass Funktionalität und Stil gleichzeitig ausgerollt werden, ohne dass es zu einem Desaster wird.

Ich habe das tatsächlich schon in großen Unternehmensumgebungen erlebt. Es ist eine schöne Sache. Dieser Artikel ist nicht für diese Leute.

Auf der anderen Seite des Spektrums steht das winzige Startup, das keine Finanzierung hat, ein oder zwei Frontend-Entwickler und einen sehr kurzen Zeitplan, um einige Funktionalitäten zu demonstrieren. Es muss nicht perfekt aussehen, aber es sollte zumindest auf Desktops, Tablets und Mobilgeräten vernünftig gerendert werden. Dies bringt sie zu einem Punkt, an dem es Beratern und frühen Nutzern gezeigt werden kann; vielleicht sogar potenziellen Investoren, die Interesse an dem Konzept bekundet haben. Sobald sie etwas Cashflow aus Verkäufen und/oder Investitionen erhalten, können sie einen dedizierten UX-Designer engagieren und die Benutzeroberfläche polieren.

Das Folgende ist für diese letztere Gruppe.

Projekt-Kickoff-Meeting

Erfinden wir ein Unternehmen, um ins Rollen zu kommen.

Solar Excursions ist ein kleines Reisebüro, das sich zum Ziel gesetzt hat, die aufstrebende Raumtourismusbranche der nahen Zukunft zu bedienen.

Unser kleines Entwicklungsteam hat vereinbart, dass React für die UI verwendet wird. Einer unserer Frontend-Entwickler ist ein großer Fan von Sass und der andere ist vom CSS in JavaScript begeistert. Aber sie werden unter Druck stehen, ihre anfänglichen Sprintziele zu erreichen; es gibt sicherlich keine Zeit, über den bestmöglichen Styling-Ansatz zu streiten. Beide Coder sind sich einig, dass die Wahl auf lange Sicht nicht viel ausmacht, solange sie konsequent ausgeführt wird. Sie sind sicher, dass die Implementierung des Stylings von Grund auf unter Zeitdruck technische Schulden aufbaut, die später bereinigt werden müssen.

Nach einiger Diskussion entscheidet sich das Team, eine oder mehrere "Styling-Refactoring"-Sprints zu planen. Vorerst konzentrieren wir uns darauf, etwas auf den Bildschirm zu bringen, indem wir React-Bootstrap verwenden. Auf diese Weise können wir schnell funktionierende Desktop- und Mobil-Layouts ohne viel Aufwand erstellen.

Je weniger Zeit für Frontend-Styling aufgewendet wird, desto besser, denn wir werden auch die UI an die Dienste anbinden müssen, die unser Backend-Entwickler ausarbeiten wird. Und da sich unsere Anwendungsarchitektur zu formen beginnt, sind sich beide Frontend-Entwickler einig, dass es wichtig ist, dass sie Unit-Tests durchführt. Sie haben viel auf dem Teller.

Basierend auf meinen Diskussionen mit den Entscheidungsträgern habe ich mich als engagierter Projektmanager mindestens zehn Minuten lang mit Balsamiq abgemüht, um dem Team Mockups für die Buchungsseite auf Desktop und Mobilgeräten zur Verfügung zu stellen. Ich gehe davon aus, dass sie Tablets dazwischen einbinden und vernünftig aussehen werden.

A desktop and mobile mockup of the proposed layout for the page, side by side with the desktop mockup on the left. Both mockups use rough black and white layouts of the various components for the screen.
Erste Mockups für die Solar Excursions Trip Booking Page auf Desktop (links) und Mobil (rechts).

Sprint Zero: Review Meeting

Pizza für alle! Das Team hat wirklich hart gearbeitet, um seine Ziele zu erreichen, und wir haben jetzt eine Buchungsseite mit einem Layout, das die Mockups annähert. Die Infrastruktur für die Dienste kommt zustande, aber es ist noch ein langer Weg, bevor wir die UI damit verbinden können. In der Zwischenzeit verwenden die Frontend-Entwickler eine hartkodierte Mock-Datenstruktur.

Screenshots of the page after the first round of development on desktop (left) and mobile (right). Both are approximately the same as the earlier mockups with most components looking exactly like the default user interface provided by the Bootstrap framework.
Die erste Iteration der Seite im Code mit react-bootstrap.

Hier ist ein Blick auf unseren UI-Code bisher

Das ist alles geradliniges React. Wir verwenden einige dieser Hooks-Heißheiten, aber es ist wahrscheinlich für die meisten von Ihnen bereits passé.

Der wichtigste Punkt, den man hier bemerken sollte, ist, wie vier unserer fünf Anwendungskomponenten Komponenten von react-bootstrap importieren und verwenden. Nur die Haupt-App-Komponente ist unbeeinflusst. Das liegt daran, dass sie nur die oberste Ansicht mit unseren benutzerdefinierten Komponenten zusammensetzt.

// App.js imports
import React, { useState } from "react";
import Navigation from "./Navigation";
import Page from "./Page";

// Navigation.js imports
import React from "react";
import { Navbar, Dropdown, Nav } from "react-bootstrap";

// Page.js imports
import React from "react";
import PosterCarousel from "./PosterCarousel";
import DestinationLayout from "./DestinationLayout";
import { Container, Row, Col } from "react-bootstrap";

// PosterCarousel.js imports
import React from "react";
import { Alert, Carousel, Image } from "react-bootstrap";

// DestinationLayout.js imports
import React, { useState, useEffect } from "react";
import {
  Button,
  Card,
  Col,
  Container,
  Dropdown,
  Jumbotron,
  ListGroup,
  Row,
  ToggleButtonGroup,
  ToggleButton
} from "react-bootstrap";

Die Entscheidung, mit Bootstrap schnell voranzugehen, hat uns erlaubt, unsere Sprintziele zu erreichen, aber wir sammeln bereits technische Schulden an. Das sind nur vier betroffene Komponenten, aber da die Anwendung wächst, ist es klar, dass die "Styling-Refactoring"-Sprints, die wir geplant haben, exponentiell schwieriger werden. Und wir haben die Komponenten noch nicht einmal viel angepasst. Sobald wir Dutzende von Komponenten haben, die alle Bootstrap mit viel Inline-Styling verwenden, um sie aufzuhübschen, wird das Refactoring, um React-Bootstrap-Abhängigkeiten zu entfernen, eine wirklich beängstigende Aufgabe sein.

Anstatt weitere Seiten der Buchungs-Pipeline zu erstellen, entscheidet sich das Team, den nächsten Sprint damit zu verbringen, die Verwendung von React-Bootstrap in einem benutzerdefinierten Komponentensatz zu isolieren, da unsere Dienste noch im Aufbau sind. Anwendungskomponenten verwenden nur Komponenten aus diesem Kit. Auf diese Weise wird der Prozess, wenn es darum geht, uns von React-Bootstrap zu entwöhnen, viel einfacher sein. Wir müssen nicht dreißig Verwendungen der React-Bootstrap-Button-Komponente in der gesamten App refaktorieren, sondern schreiben nur die internen Elemente unserer KitButton-Komponente neu.

Sprint One: Review Meeting

Nun, das war einfach. High-Fives. Keine Änderung am visuellen Erscheinungsbild der UI, aber wir haben jetzt einen "Kit"-Ordner, der neben "Components" in unserer React-Quelle liegt. Er enthält eine Reihe von Dateien wie KitButton.js, die im Grunde umbenannte React-Bootstrap-Komponenten exportieren.

Ein Beispiel für eine Komponente aus unserem Kit sieht so aus

// KitButton.js
import { Button, ToggleButton, ToggleButtonGroup } from "react-bootstrap";
export const KitButton = Button;
export const KitToggleButton = ToggleButton;
export const KitToggleButtonGroup = ToggleButtonGroup;

Wir bündeln all diese Kit-Komponenten in einem Modul wie diesem

// kit/index.js
import { KitCard } from "./KitCard";
import { KitHero } from "./KitHero";
import { KitList } from "./KitList";
import { KitImage } from "./KitImage";
import { KitCarousel } from "./KitCarousel";
import { KitDropdown } from "./KitDropdown";
import { KitAttribution } from "./KitAttribution";
import { KitNavbar, KitNav } from "./KitNavbar";
import { KitContainer, KitRow, KitCol } from "./KitContainer";
import { KitButton, KitToggleButton, KitToggleButtonGroup } from "./KitButton";
export {
  KitCard,
  KitHero,
  KitList,
  KitImage,
  KitCarousel,
  KitDropdown,
  KitAttribution,
  KitButton,
  KitToggleButton,
  KitToggleButtonGroup,
  KitContainer,
  KitRow,
  KitCol,
  KitNavbar,
  KitNav
};

Und jetzt sind unsere Anwendungskomponenten vollständig von React-Bootstrap befreit. Hier sind die Importe für die betroffenen Komponenten

// Navigation.js imports
import React from "react";
import { KitNavbar, KitNav, KitDropdown } from "../kit";


// Page.js imports 
import React from "react";
import PosterCarousel from "./PosterCarousel";
import DestinationLayout from "./DestinationLayout";
import { KitContainer, KitRow, KitCol } from "../kit";


// PosterCarousel.js imports
import React from "react";
import { KitAttribution, KitImage, KitCarousel } from "../kit";


// DestinationLayout.js imports
import React, { useState, useEffect } from "react";
import {
  KitCard,
  KitHero,
  KitList,
  KitButton,
  KitToggleButton,
  KitToggleButtonGroup,
  KitDropdown,
  KitContainer,
  KitRow,
  KitCol
} from "../kit";

Hier ist der Frontend-Codebestand jetzt

Obwohl wir alle React-Imports in unseren Kit-Komponenten gesammelt haben, sind unsere Anwendungskomponenten immer noch ein wenig auf die React-Bootstrap-Implementierung angewiesen, da die Attribute, die wir auf unseren Kit-Komponenten-Instanzen platzieren, dieselben sind wie die von React-Bootstrap. Das schränkt uns bei der Neuimplementierung der Kit-Komponenten ein, da wir uns an die gleiche API halten müssen. Zum Beispiel

// From Navigation.js
<KitNavbar bg="dark" variant="dark" fixed="top">

Idealerweise müssten wir diese React-Bootstrap-spezifischen Attribute nicht hinzufügen, wenn wir unsere KitNavbar instanziieren.

Die Frontend-Entwickler versprechen, diese im Laufe der Zeit auszufacatorieren, jetzt, da wir sie als problematisch identifiziert haben. Und alle neuen Referenzen auf React-Bootstrap-Komponenten werden in unser Kit und nicht direkt in die Anwendungskomponenten übernommen.

In der Zwischenzeit haben wir unsere Mock-Daten mit dem Server-Ingenieur geteilt, der hart daran arbeitet, separate Server-Umgebungen aufzubauen, das Datenbankschema zu implementieren und uns einige Dienste zur Verfügung zu stellen.

Das gibt uns Zeit, unserer UI im nächsten Sprint etwas Glanz zu verleihen – was gut ist, denn die Entscheidungsträger möchten separate Themen für jedes Reiseziel sehen. Während der Benutzer durch die Reiseziele blättert, müssen wir das Farbschema der UI ändern, um dem angezeigten Reiseplakat zu entsprechen. Außerdem wollen wir versuchen, diese Komponenten etwas aufzuhübschen, um unseren eigenen Look and Feel zu entwickeln. Sobald wir etwas Geld verdienen, werden wir einen Designer für eine komplette Überholung engagieren, aber hoffentlich können wir für unsere frühen Nutzer einen glücklichen Mittelweg finden.

Sprint Two: Review Meeting

Wow! Das Team hat sich in diesem Sprint wirklich ins Zeug gelegt. Wir haben themenbezogene Themen, angepasste Komponenten und viele der verbleibenden React-Bootstrap-API-Implementierungen aus den Anwendungskomponenten entfernt.

Hier ist, wie der Desktop jetzt aussieht

A more complete rendering of the landing page, this time using Mars as an example to show off a bright orange and red color theme. The interface components no longer look like they came directly from Bootstrap, but are still minimal in style, like dropdowns, buttons and text.
Schauen Sie sich das solarisierte Thema für den roten Planeten an!

Um dies zu ermöglichen, haben die Frontend-Entwickler die Styled Components Bibliothek eingeführt. Dies machte das Styling der einzelnen Kit-Komponenten zum Kinderspiel und ermöglichte die Unterstützung mehrerer Themen.

Werfen wir einen Blick auf einige Highlights ihrer Änderungen für diesen Sprint.

Erstens gibt es für globale Dinge wie das Einbinden von Schriftarten und das Festlegen der Seitenkörperstile eine neue Kit-Komponente namens KitGlobal.

// KitGlobal.js
import { createGlobalStyle } from "styled-components";
export const KitGlobal = createGlobalStyle`
  body {
    @import url('https://fonts.googleapis.com/css?family=Orbitron:500|Nunito:600|Alegreya+Sans+SC:700');
    background-color: ${props => props.theme.foreground};
    overflow-x: hidden;
  }
`;

Sie verwendet den createGlobalStyle-Helfer, um die CSS für das Body-Element zu definieren. Dies importiert unsere gewünschten Webfonts von Google, setzt die Hintergrundfarbe auf den aktuellen "Foreground"-Wert des Themas und schaltet den Überlauf in x-Richtung aus, um eine lästige horizontale Scrollbar zu eliminieren. Wir verwenden diese KitGlobal-Komponente in der Render-Methode unserer App-Komponente.

Ebenfalls in der App-Komponente importieren wir ThemeProvider von styled-components und etwas namens "themes" aus ../theme. Wir verwenden Reacts useState, um das anfängliche Thema auf themes.luna zu setzen, und Reacts useEffect, um setTheme aufzurufen, wann immer sich die "destination" ändert. Die zurückgegebene Komponente ist nun in ThemeProvider verschachtelt, dem "theme" als Prop übergeben wird. Hier ist die App-Komponente in ihrer Gesamtheit.

// App.js
import React, { useState, useEffect } from "react";
import { ThemeProvider } from "styled-components";
import themes from "../theme/";
import { KitGlobal } from "../kit";
import Navigation from "./Navigation";
import Page from "./Page";
export default function App(props) {
  const [destinationIndex, setDestinationIndex] = useState(0);
  const [theme, setTheme] = useState(themes.luna);
  const destination = props.destinations[destinationIndex];
  useEffect(() => {
    setTheme(themes[destination.theme]);
  }, [destination]);

  return (
    <ThemeProvider theme={theme}>
      <React.Fragment>
        <KitGlobal />
        <Navigation
          {...props}
          destinationIndex={destinationIndex}
          setDestinationIndex={setDestinationIndex}
        />
        <Page
          {...props}
          destinationIndex={destinationIndex}
          setDestinationIndex={setDestinationIndex}
        />
      </React.Fragment>
    </ThemeProvider>
  );
}

KitGlobal wird wie jede andere Komponente gerendert. Nichts Besonderes, nur dass das Body-Tag betroffen ist. ThemeProvider verwendet die React Context API, um theme an alle Komponenten weiterzugeben, die es benötigen (also alle). Um das vollständig zu verstehen, müssen wir uns auch ansehen, was ein Thema eigentlich ist.

Um ein Thema zu erstellen, hat einer unserer Frontend-Entwickler alle Reiseplakate genommen und für jedes Paletten erstellt, indem er die prominenten Farben extrahiert hat. Das war ziemlich einfach.

A screenshot of the Tiny Eye website showing the red color palette used for the Mars page theme. There are two images on the left from the page that Tiny Eye used to create a color palette in the center containing various shades of red and the hex values for them on the right.
Wir haben TinyEye dafür verwendet.

Offensichtlich würden wir nicht alle Farben verwenden. Der Ansatz bestand hauptsächlich darin, die zwei am häufigsten verwendeten Farben foreground und background zu nennen. Dann nahmen wir drei weitere Farben, die normalerweise von der hellsten zur dunkelsten geordnet sind: accent1, accent2 und accent3. Schließlich wählten wir zwei kontrastierende Farben, die wir text1 und text2 nannten. Für das obige Reiseziel sah das so aus

// theme/index.js (partial list)
const themes = {
  ...
  mars: {
    background: "#a53237",
    foreground: "#f66f40",
    accent1: "#f8986d",
    accent2: "#9c4952",
    accent3: "#f66f40",
    text1: "#f5e5e1",
    text2: "#354f55"
  },
  ...
};
export default themes;

Sobald wir ein Thema für jedes Reiseziel haben und es an alle Komponenten weitergegeben wird (einschließlich der Kit-Komponenten, aus denen unsere Anwendungskomponenten jetzt aufgebaut sind), müssen wir styled-components verwenden, um diese Themenfarben sowie unsere benutzerdefinierten visuellen Stile wie die Panel-Ecken und den "Border Glow" anzuwenden.

Dies ist ein einfaches Beispiel, bei dem wir unsere KitHero-Komponente dazu gebracht haben, das Thema und benutzerdefinierte Stile auf das Bootstrap Jumbotron anzuwenden

// KitHero.js
import styled from "styled-components";
import { Jumbotron } from "react-bootstrap";

export const KitHero = styled(Jumbotron)`
  background-color: ${props => props.theme.accent1};
  color: ${props => props.theme.text2};
  border-radius: 7px 25px;
  border-color: ${props => props.theme.accent3};
  border-style: solid;
  border-width: 1px;
  box-shadow: 0 0 1px 2px #fdb813, 0 0 3px 4px #f8986d;
  font-family: "Nunito", sans-serif;
  margin-bottom: 20px;
`;

In diesem Fall sind wir mit dem zufrieden, was styled-components zurückgibt, also nennen wir es einfach KitHero und exportieren es.

Wenn wir es in der Anwendung verwenden, sieht es so aus

// DestinationLayout.js (partial code)
const renderHero = () => {
  return (
    <KitHero>
      <h2>{destination.header}</h2>
      <p>{destination.blurb}</p>
      <KitButton>Book Your Trip Now!</KitButton>
    </KitHero>
  );
};

Dann gibt es komplexere Fälle, in denen wir einige Attribute auf der React-Bootstrap-Komponente voreinstellen möchten. Zum Beispiel die KitNavbar-Komponente, die wir zuvor als mit vielen React-Bootstrap-Attributen identifiziert hatten, die wir lieber nicht von der Deklaration der Komponente in der Anwendung übergeben würden.

Nun zum Blick darauf, wie das gehandhabt wurde

// KitNavbar.js (partial code)
import React, { Component } from "react";
import styled from "styled-components";
import { Navbar } from "react-bootstrap";

const StyledBootstrapNavbar = styled(Navbar)`
  background-color: ${props => props.theme.background};
  box-shadow: 0 0 1px 2px #fdb813, 0 0 3px 4px #f8986d;
  display: flex;
  flex-direction: horizontal;
  justify-content: space-between;
  font-family: "Nunito", sans-serif;
`;

export class KitNavbar extends Component {
  render() {
    const { ...props } = this.props;
    return <StyledBootstrapNavbar fixed="top" {...props} />;
  }
}

Zuerst erstellen wir eine Komponente namens StyledBootstrapNavbar mit styled-components. Wir konnten einige Attribute mit dem CSS handhaben, das wir an styled-components übergeben haben. Aber um (vorerst) die zuverlässige Fixierung der Komponente am oberen Bildschirmrand beim Scrollen anderer Elemente weiter zu nutzen, haben sich unsere Frontend-Entwickler entschieden, weiterhin das fixed-Attribut von React-Bootstrap zu verwenden. Um dies zu tun, mussten wir eine KitNavbar-Komponente erstellen, die eine Instanz von StyledBootstrapNavbar mit dem Attribut fixed=top rendert. Wir haben auch alle Props durchgereicht, einschließlich der Kinder.

Wir müssen nur eine separate Klasse erstellen, die die Arbeit von styled-components rendert und Props weitergibt, wenn wir explizit einige Attribute in unserer Kit-Komponente als Standard festlegen wollen. In den meisten Fällen können wir einfach die Ausgabe von styled-components benennen und zurückgeben und sie wie mit KitHero oben verwenden.

Wenn wir nun die KitNavbar in der Navigation-Komponente unserer Anwendung rendern, sieht das so aus

// Navigation.js (partial code)
return (
  <KitNavbar>
    <KitNavbarBrand>
      <KitLogo />
      Solar Excursions
    </KitNavbarBrand>
    {renderDestinationMenu()}
  </KitNavbar>
);

Schließlich haben wir unsere ersten Schritte unternommen, um unsere Kit-Komponenten von React-Bootstrap zu refaktorieren. Die KitAttribution-Komponente ist ein Bootstrap Alert, das für unsere Zwecke kaum mehr als ein gewöhnliches div ist. Wir konnten es leicht refaktorieren, um seine Abhängigkeit von React-Bootstrap zu entfernen.

Das ist die Komponente, wie sie aus dem vorherigen Sprint hervorgegangen ist

// KitAttribution.js (using react-bootstrap)
import { Alert } from "react-bootstrap";
export const KitAttribution = Alert;

Das ist, wie sie jetzt aussieht

// KitAttribution.js
import styled from "styled-components";
export const KitAttribution = styled.div`
  text-align: center;
  background-color: ${props => props.theme.accent1};
  color: ${props => props.theme.text2};
  border-radius: 7px 25px;
  border-color: ${props => props.theme.accent3};
  border-style: solid;
  border-width: 1px;
  box-shadow: 0 0 1px 2px #fdb813, 0 0 3px 4px #f8986d;
  font-family: "Alegreya Sans SC", sans-serif;
  > a {
    color: ${props => props.theme.text2};
    font-family: "Nunito", sans-serif;
  }
  > a:hover {
    color: ${props => props.theme.background};
    text-decoration-color: ${props => props.theme.accent3};
  }
`;

Beachten Sie, wie wir React-Bootstrap nicht mehr importieren und styled.div als Komponentengrundlage verwenden. Sie werden nicht alle so einfach sein, aber es ist ein Prozess.

Hier sind die Ergebnisse der Styling- und Theming-Bemühungen unseres Teams in Sprint Two

Sehen Sie die thematische Seite separat hier.

Fazit

Nach drei Sprints ist unser Team auf dem besten Weg, eine skalierbare Komponentenarchitektur für die UI zu implementieren.

  • Wir bewegen uns dank react-bootstrap schnell, aber stapeln dadurch keine großen technischen Schulden mehr.
  • Dank styled-components konnten wir mehrere Themen implementieren (wie praktisch jede App im Internet heutzutage dunkle und helle Modi bietet). Außerdem sehen wir nicht mehr wie eine Standard-Bootstrap-App aus.
  • Durch die Implementierung eines benutzerdefinierten Komponentensatzes, der alle Referenzen auf React-Bootstrap enthält, können wir uns im Laufe der Zeit davon refaktorieren.

Forken Sie den endgültigen Codebestand auf GitHub.