Entwickeln Sie mit React oder interessieren Sie sich nur für diese Technologie? Dann willkommen zu meinem neuen Projekt - Total React .
Einführung
Ich arbeite seit 5 Jahren mit React. Wenn es jedoch um die Struktur der Anwendung oder ihr Erscheinungsbild (Design) geht, ist es schwierig, universelle Ansätze zu nennen.
Gleichzeitig gibt es bestimmte Codierungstechniken, mit denen Sie die langfristige Unterstützung und Skalierbarkeit Ihrer Projekte sicherstellen können.
Dieser Artikel ist eine Art Regelwerk für die Entwicklung von React-Anwendungen, die sich für mich und die Teams, mit denen ich zusammengearbeitet habe, als effektiv erwiesen haben.
Diese Regeln umfassen Komponenten, Anwendungsstruktur, Testen, Styling, Statusverwaltung und Datenabruf. Die Beispiele wurden absichtlich vereinfacht, um sich eher auf allgemeine Prinzipien als auf eine spezifische Implementierung zu konzentrieren.
Die vorgeschlagenen Ansätze sind nicht die ultimative Wahrheit. Dies ist nur meine Meinung. Es gibt viele verschiedene Möglichkeiten, um dieselbe Aufgabe zu erfüllen.
Komponenten
Funktionskomponenten
Bevorzugen Sie funktionale Komponenten - sie haben eine einfachere Syntax. Ihnen fehlen Lebenszyklusmethoden, Konstruktoren und Boilerplate-Code. Mit ihnen können Sie dieselbe Logik wie Klassenkomponenten implementieren, jedoch mit weniger Aufwand und aussagekräftiger (der Komponentencode ist leichter zu lesen).
Verwenden Sie Funktionskomponenten, bis Sie Sicherungen benötigen. Das zu berücksichtigende mentale Modell wird viel einfacher sein.
// ""
class Counter extends React.Component {
state = {
counter: 0,
}
constructor(props) {
super(props)
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
this.setState({ counter: this.state.counter + 1 })
}
render() {
return (
<div>
<p> : {this.state.counter}</p>
<button onClick={this.handleClick}></button>
</div>
)
}
}
//
function Counter() {
const [counter, setCounter] = useState(0)
handleClick = () => setCounter(counter + 1)
return (
<div>
<p> : {counter}</p>
<button onClick={handleClick}></button>
</div>
)
}
Konsistente (sequentielle) Komponenten
Halten Sie sich beim Erstellen von Komponenten an denselben Stil. Platzieren Sie Hilfsfunktionen an derselben Stelle, verwenden Sie denselben Export (standardmäßig oder nach Namen) und verwenden Sie dieselbe Namenskonvention für Komponenten.
Jeder Ansatz hat seine eigenen Vor- und Nachteile.
Es spielt keine Rolle, wie Sie die Komponenten ganz unten oder in der Definition exportieren. Halten Sie sich einfach an eine Regel.
Komponentennamen
Benennen Sie immer die Komponenten. Dies hilft beim Parsen des Fehlerstapel-Trace, wenn React-Entwicklertools verwendet werden.
Außerdem können Sie feststellen, welche Komponente Sie gerade entwickeln.
//
export default () => <form>...</form>
//
export default function Form() {
return <form>...</form>
}
Sekundärfunktionen
Funktionen, für die die in der Komponente gespeicherten Daten nicht erforderlich sind, müssen sich außerhalb (außerhalb) der Komponente befinden. Der ideale Ort dafür ist vor der Komponentendefinition, damit der Code von oben nach unten überprüft werden kann.
Dies reduziert das "Rauschen" der Komponente - nur das Wesentliche bleibt darin.
//
function Component({ date }) {
function parseDate(rawDate) {
...
}
return <div> {parseDate(date)}</div>
}
//
function parseDate(date) {
...
}
function Component({ date }) {
return <div> {parseDate(date)}</div>
}
Innerhalb der Komponente sollte eine Mindestanzahl von Zusatzfunktionen vorhanden sein. Platzieren Sie sie außerhalb und übergeben Sie Werte aus dem Status als Argumente.
Durch Befolgen der Regeln zum Erstellen von "sauberen" Funktionen ist es einfacher, Fehler zu verfolgen und die Komponente zu erweitern.
// ""
export default function Component() {
const [value, setValue] = useState('')
function isValid() {
// ...
}
return (
<>
<input
value={value}
onChange={e => setValue(e.target.value)}
onBlur={validateInput}
/>
<button
onClick={() => {
if (isValid) {
// ...
}
}}
>
</button>
</>
)
}
//
function isValid(value) {
// ...
}
export default function Component() {
const [value, setValue] = useState('')
return (
<>
<input
value={value}
onChange={e => setValue(e.target.value)}
onBlur={validateInput}
/>
<button
onClick={() => {
if (isValid(value)) {
// ...
}
}}
>
</button>
</>
)
}
Statisches (hartes) Markup
Erstellen Sie keine statischen Markups für Navigation, Filter oder Listen. Erstellen Sie stattdessen ein Objekt mit Einstellungen und durchlaufen Sie es.
Dies bedeutet, dass Sie das Markup und die Elemente bei Bedarf nur an einer Stelle ändern müssen.
//
function Filters({ onFilterClick }) {
return (
<>
<p> </p>
<ul>
<li>
<div onClick={() => onFilterClick('fiction')}> </div>
</li>
<li>
<div onClick={() => onFilterClick('classics')}>
</div>
</li>
<li>
<div onClick={() => onFilterClick('fantasy')}></div>
</li>
<li>
<div onClick={() => onFilterClick('romance')}></div>
</li>
</ul>
</>
)
}
//
const GENRES = [
{
identifier: 'fiction',
name: ' ',
},
{
identifier: 'classics',
name: '',
},
{
identifier: 'fantasy',
name: '',
},
{
identifier: 'romance',
name: '',
},
]
function Filters({ onFilterClick }) {
return (
<>
<p> </p>
<ul>
{GENRES.map(genre => (
<li>
<div onClick={() => onFilterClick(genre.identifier)}>
{genre.name}
</div>
</li>
))}
</ul>
</>
)
}
Komponentenabmessungen
Eine Komponente ist nur eine Funktion, die Requisiten nimmt und Markups zurückgibt. Sie folgen den gleichen Gestaltungsprinzipien.
Wenn eine Funktion zu viele Aufgaben ausführt, verschieben Sie einen Teil der Logik auf eine andere Funktion. Gleiches gilt für Komponenten. Wenn eine Komponente zu komplexe Funktionen enthält, teilen Sie sie in mehrere Komponenten auf.
Wenn ein Teil des Markups komplex ist, Schleifen oder Bedingungen enthält, extrahieren Sie es in eine separate Komponente.
Verlassen Sie sich bei der Interaktion und beim Abrufen von Daten auf Requisiten und Rückrufe. Die Anzahl der Codezeilen ist nicht immer ein objektives Kriterium für die Qualität. Denken Sie immer daran, reaktionsschnell und abstrahiert zu sein.
Kommentare in JSX
Wenn Sie eine Erklärung benötigen, erstellen Sie einen Kommentarblock und fügen Sie dort die erforderlichen Informationen hinzu. Markup ist Teil der Logik. Wenn Sie also das Gefühl haben, einen Teil kommentieren zu müssen, tun Sie dies.
function Component(props) {
return (
<>
{/* , */}
{user.subscribed ? null : <SubscriptionPlans />}
</>
)
}
Leistungsschalter
Ein Fehler in der Komponente sollte die Benutzeroberfläche nicht beschädigen. Es gibt seltene Fälle, in denen ein kritischer Fehler zu einem Absturz oder einer Umleitung der Anwendung führen soll. In den meisten Fällen reicht es aus, ein bestimmtes Element vom Bildschirm zu entfernen.
In einer Funktion, die Daten anfordert, können beliebig viele Try / Catch-Blöcke vorhanden sein. Verwenden Sie Sicherungen nicht nur auf der obersten Ebene Ihrer Anwendung, sondern umschließen Sie alle Komponenten, die möglicherweise eine Ausnahme auslösen könnten, um eine Fehlerkaskade zu vermeiden.
function Component() {
return (
<Layout>
<ErrorBoundary>
<CardWidget />
</ErrorBoundary>
<ErrorBoundary>
<FiltersWidget />
</ErrorBoundary>
<div>
<ErrorBoundary>
<ProductList />
</ErrorBoundary>
</div>
</Layout>
)
}
Requisiten zerstören
Die meisten Komponenten sind Funktionen, die Requisiten aufnehmen und Markups zurückgeben. In einer normalen Funktion verwenden wir Argumente, die direkt an sie übergeben werden. Bei Komponenten ist es daher sinnvoll, einen ähnlichen Ansatz zu verfolgen. Es ist nicht nötig, "Requisiten" überall zu wiederholen.
Der Grund dafür, dass keine Destrukturierung verwendet wird, könnte der Unterschied zwischen dem externen und dem internen Zustand sein. Bei einer normalen Funktion gibt es jedoch keinen Unterschied zwischen Argumenten und Variablen. Sie müssen die Dinge nicht komplizieren.
// "props"
function Input(props) {
return <input value={props.value} onChange={props.onChange} />
}
//
function Component({ value, onChange }) {
const [state, setState] = useState('')
return <div>...</div>
}
Anzahl der Requisiten
Die Antwort auf die Frage nach der Anzahl der Requisiten ist sehr subjektiv. Die Anzahl der an eine Komponente übergebenen Requisiten korreliert mit der Anzahl der von der Komponente verwendeten Variablen. Je mehr Requisiten an die Komponente übergeben werden, desto höher ist ihre Verantwortung (dh die Anzahl der von der Komponente gelösten Aufgaben).
Eine große Anzahl von Requisiten kann darauf hinweisen, dass die Komponente zu viel tut.
Wenn mehr als 5 Requisiten an eine Komponente übergeben werden, denke ich über die Notwendigkeit nach, diese zu teilen. In einigen Fällen benötigt die Komponente nur viele Daten. Beispielsweise benötigt ein Texteingabefeld möglicherweise viele Requisiten. Andererseits ist dies ein sicheres Zeichen dafür, dass ein Teil der Logik in eine separate Komponente extrahiert werden muss.
Bitte beachten Sie: Je mehr Requisiten eine Komponente erhält, desto häufiger wird sie neu gezeichnet.
Übergeben eines Objekts anstelle von Grundelementen
Eine Möglichkeit, die Anzahl der übergebenen Requisiten zu verringern, besteht darin, ein Objekt anstelle von Grundelementen zu übergeben. Anstatt zum Beispiel den Namen des Benutzers, seine E-Mail-Adresse usw. zu übertragen. Sie können sie einzeln gruppieren. Dies erleichtert auch das Hinzufügen neuer Daten.
//
<UserProfile
bio={user.bio}
name={user.name}
email={user.email}
subscription={user.subscription}
/>
// ,
<UserProfile user={user} />
Bedingtes Rendern
In einigen Fällen kann die Verwendung kurzer Berechnungen (der logische UND-Operator &&) für das bedingte Rendern dazu führen, dass 0 in der Benutzeroberfläche angezeigt wird. Um dies zu vermeiden, verwenden Sie den ternären Operator. Der einzige Nachteil dieses Ansatzes ist etwas mehr Code.
Der Operator && reduziert die Codemenge, was großartig ist. Ternarnik ist "wortreicher", funktioniert aber immer richtig. Darüber hinaus wird es weniger zeitaufwändig, bei Bedarf eine Alternative hinzuzufügen.
//
function Component() {
const count = 0
return <div>{count && <h1>: {count}</h1>}</div>
}
// ,
function Component() {
const count = 0
return <div>{count ? <h1>: {count}</h1> : null}</div>
}
Verschachtelte ternäre Operatoren
Ternäre Operatoren sind nach der ersten Verschachtelungsebene schwer zu lesen. Obwohl Ternäre Platz sparen, ist es am besten, ihre Absichten explizit und offensichtlich auszudrücken.
//
isSubscribed ? (
<ArticleRecommendations />
) : isRegistered ? (
<SubscribeCallToAction />
) : (
<RegisterCallToAction />
)
//
function CallToActionWidget({ subscribed, registered }) {
if (subscribed) {
return <ArticleRecommendations />
}
if (registered) {
return <SubscribeCallToAction />
}
return <RegisterCallToAction />
}
function Component() {
return (
<CallToActionWidget
subscribed={subscribed}
registered={registered}
/>
)
}
Listen
Das Durchlaufen der Elemente einer Liste ist eine häufige Aufgabe, die normalerweise mit der Methode "map ()" ausgeführt wird. In einer Komponente, die viel Markup enthält, verbessern der zusätzliche Einzug und die Syntax "map ()" jedoch nicht die Lesbarkeit.
Wenn Sie die Elemente durchlaufen müssen, extrahieren Sie sie in eine separate Komponente, auch wenn das Markup klein ist. Die übergeordnete Komponente benötigt keine Details, sondern muss nur "wissen", dass die Liste an einer bestimmten Stelle gerendert wird.
Die Iteration kann in einer Komponente belassen werden, deren einziger Zweck darin besteht, die Liste anzuzeigen. Wenn das Listen-Markup komplex und lang ist, extrahieren Sie es am besten in eine separate Komponente.
//
function Component({ topic, page, articles, onNextPage }) {
return (
<div>
<h1>{topic}</h1>
{articles.map(article => (
<div>
<h3>{article.title}</h3>
<p>{article.teaser}</p>
<img src={article.image} />
</div>
))}
<div> {page}</div>
<button onClick={onNextPage}></button>
</div>
)
}
//
function Component({ topic, page, articles, onNextPage }) {
return (
<div>
<h1>{topic}</h1>
<ArticlesList articles={articles} />
<div> {page}</div>
<button onClick={onNextPage}></button>
</div>
)
}
Standard Requisiten
Eine Möglichkeit, Standard-Requisiten zu definieren, besteht darin, der Komponente ein Attribut "defaultProps" hinzuzufügen. Bei diesem Ansatz befinden sich die Komponentenfunktion und die Werte für ihre Argumente jedoch an verschiedenen Stellen.
Daher ist es vorzuziehen, "Standard" -Werte zuzuweisen, wenn Requisiten zerstört werden. Dies erleichtert das Lesen des Codes von oben nach unten und hält Definitionen und Werte an einem Ort.
//
function Component({ title, tags, subscribed }) {
return <div>...</div>
}
Component.defaultProps = {
title: '',
tags: [],
subscribed: false,
}
//
function Component({ title = '', tags = [], subscribed = false }) {
return <div>...</div>
}
Verschachtelte Renderfunktionen
Wenn Sie Logik oder Markup aus einer Komponente extrahieren müssen, fügen Sie sie nicht in eine Funktion derselben Komponente ein. Eine Komponente ist eine Funktion. Dies bedeutet, dass der extrahierte Teil des Codes als verschachtelte Funktion dargestellt wird.
Dies bedeutet, dass die verschachtelte Funktion Zugriff auf den Status und die Daten der äußeren Funktion hat. Dies macht den Code weniger lesbar - was macht diese Funktion (wofür ist sie verantwortlich)?
Verschieben Sie die verschachtelte Funktion in eine separate Komponente, geben Sie ihr einen Namen und verlassen Sie sich auf Requisiten anstelle von Verschlüssen.
//
function Component() {
function renderHeader() {
return <header>...</header>
}
return <div>{renderHeader()}</div>
}
//
import Header from '@modules/common/components/Header'
function Component() {
return (
<div>
<Header />
</div>
)
}
Staatsverwaltung
Getriebe
Manchmal brauchen wir eine leistungsfähigere Methode zum Definieren und Verwalten des Status als "useState ()". Versuchen Sie es mit "useReducer ()", bevor Sie Bibliotheken von Drittanbietern verwenden. Es ist ein großartiges Tool zum Verwalten komplexer Zustände, ohne Abhängigkeiten zu erfordern.
In Kombination mit Kontext und TypeScript kann useReducer () sehr leistungsfähig sein. Leider wird es nicht sehr oft verwendet. Die Leute bevorzugen spezielle Bibliotheken.
Wenn Sie mehrere Statuselemente benötigen, verschieben Sie diese in den Reduzierer:
//
const TYPES = {
SMALL: 'small',
MEDIUM: 'medium',
LARGE: 'large'
}
function Component() {
const [isOpen, setIsOpen] = useState(false)
const [type, setType] = useState(TYPES.LARGE)
const [phone, setPhone] = useState('')
const [email, setEmail] = useState('')
const [error, setError] = useSatte(null)
return (
// ...
)
}
//
const TYPES = {
SMALL: 'small',
MEDIUM: 'medium',
LARGE: 'large'
}
const initialState = {
isOpen: false,
type: TYPES.LARGE,
phone: '',
email: '',
error: null
}
const reducer = (state, action) => {
switch (action.type) {
...
default:
return state
}
}
function Component() {
const [state, dispatch] = useReducer(reducer, initialState)
return (
// ...
)
}
Hooks versus HOCs und Render Requisiten
In einigen Fällen müssen wir eine Komponente "härten" oder ihr Zugriff auf externe Daten gewähren. Hierfür gibt es drei Möglichkeiten: Komponenten höherer Ordnung (HOCs), die über Requisiten und Hooks gerendert werden.
Am effektivsten ist es, Haken zu verwenden. Sie entsprechen voll und ganz der Philosophie, dass eine Komponente eine Funktion ist, die andere Funktionen verwendet. Mit Hooks können Sie auf mehrere Quellen zugreifen, die externe Funktionen enthalten, ohne dass ein Konflikt zwischen diesen Quellen droht. Die Anzahl der Haken spielt keine Rolle, wir wissen immer, woher wir den Wert haben.
HOCs erhalten Werte als Requisiten. Es ist nicht immer offensichtlich, woher die Werte stammen, von der übergeordneten Komponente oder von ihrem Wrapper. Darüber hinaus ist das Verketten mehrerer Requisiten eine bekannte Fehlerquelle.
Die Verwendung von Render-Requisiten führt zu einer tiefen Verschachtelung und einer schlechten Lesbarkeit. Das Platzieren mehrerer Komponenten mit Render-Requisiten im selben Baum verschärft die Situation weiter. Außerdem verwenden sie nur Werte im Markup, sodass die Logik zum Abrufen der Werte hier geschrieben oder von außen empfangen werden muss.
Bei Hooks arbeiten wir mit einfachen Werten, die leicht zu verfolgen sind und sich nicht mit JSX mischen lassen.
// -
function Component() {
return (
<>
<Header />
<Form>
{({ values, setValue }) => (
<input
value={values.name}
onChange={e => setValue('name', e.target.value)}
/>
<input
value={values.password}
onChange={e => setValue('password', e.target.value)}
/>
)}
</Form>
<Footer />
</>
)
}
//
function Component() {
const [values, setValue] = useForm()
return (
<>
<Header />
<input
value={values.name}
onChange={e => setValue('name', e.target.value)}
/>
<input
value={values.password}
onChange={e => setValue('password', e.target.value)}
/>
<Footer />
</>
)
}
Bibliotheken zum Abrufen von Daten
Sehr oft "kommen" die Daten für den Status von der API. Wir müssen sie im Speicher speichern, aktualisieren und an mehreren Stellen empfangen.
Moderne Bibliotheken wie React Query bieten eine ganze Reihe von Tools zur Bearbeitung externer Daten. Wir können Daten zwischenspeichern, löschen und neue anfordern. Diese Tools können auch zum Senden von Daten, Auslösen einer Aktualisierung eines anderen Datenelements usw. verwendet werden.
Die Arbeit mit externen Daten ist noch einfacher, wenn Sie einen GraphQL-Client wie Apollo verwenden . Es implementiert das Konzept des Client-Status sofort.
Staatsverwaltungsbibliotheken
In den allermeisten Fällen benötigen wir keine Bibliotheken, um den Anwendungsstatus zu verwalten. Sie werden nur in sehr großen Anwendungen mit sehr komplexem Zustand benötigt. In solchen Situationen verwende ich eine von zwei Lösungen - Recoil oder Redux .
Komponentenmentale Modelle
Container und Vertreter
Normalerweise ist es üblich, Komponenten in zwei Gruppen zu unterteilen - Repräsentanten und Container oder "intelligent" und "dumm".
Unter dem Strich enthalten einige Komponenten keinen Status und keine Funktionalität. Sie werden nur von der übergeordneten Komponente mit einigen Requisiten aufgerufen. Die Containerkomponente enthält wiederum eine Geschäftslogik, sendet Anforderungen zum Empfangen von Daten und verwaltet den Status.
Dieses mentale Modell beschreibt tatsächlich das MVC-Entwurfsmuster für serverseitige Anwendungen. Sie arbeitet dort großartig.
In modernen Clientanwendungen rechtfertigt sich dieser Ansatz jedoch nicht. Das Einfügen der gesamten Logik in mehrere Komponenten führt zu einer Überblähung. Dies führt dazu, dass eine Komponente zu viele Probleme löst. Der Code für eine solche Komponente ist schwer zu pflegen. Wenn die Anwendung wächst, wird es fast unmöglich, den Code in einem ordnungsgemäßen Zustand zu halten.
Zustands- und zustandslose Komponenten
Teilen Sie Komponenten in zustandsbehaftete und zustandslose Komponenten. Das oben erwähnte mentale Modell legt nahe, dass eine kleine Anzahl von Komponenten die Logik der gesamten Anwendung steuern muss. Dieses Modell geht von der Aufteilung der Logik in die maximal mögliche Anzahl von Komponenten aus.
Die Daten sollten so nah wie möglich an der Komponente sein, in der sie verwendet werden. Bei Verwendung des GrapQL-Clients erhalten wir Daten in einer Komponente, die diese Daten anzeigt. Auch wenn es sich nicht um eine Top-Level-Komponente handelt. Denken Sie nicht an Container, sondern an die Verantwortung der Komponenten. Bestimmen Sie die am besten geeignete Komponente, um einen Teil des Status zu speichern
Beispielsweise muss eine <Form /> -Komponente Formulardaten enthalten. Die <Input /> -Komponente muss Werte empfangen und Rückrufe aufrufen. Die <Button /> -Komponente muss das Formular über den Wunsch des Benutzers informieren, Daten zur Verarbeitung usw. zu senden.
Wer ist für die Validierung des Formulars verantwortlich? Eingabefeld? Dies bedeutet, dass diese Komponente für die Geschäftslogik der Anwendung verantwortlich ist. Wie wird das Formular über einen Fehler informiert? Wie wird der Fehlerstatus aktualisiert? Wird das Formular von einem solchen Update "wissen"? Wenn ein Fehler auftritt, können die Daten dann zur Verarbeitung gesendet werden?
Wenn solche Fragen auftauchen, wird deutlich, dass die Verantwortlichkeiten verwirrt sind. In diesem Fall ist die "Eingabe" besser, um eine zustandslose Komponente zu bleiben und Fehlermeldungen vom Formular zu erhalten.
Anwendungsstruktur
Gruppierung nach Route / Modul
Die Gruppierung nach Containern und Komponenten erschwert das Erlernen der Anwendung. Das Bestimmen, zu welchem Teil einer Anwendung eine bestimmte Komponente gehört, setzt eine "enge" Vertrautheit mit der gesamten Codebasis voraus.
Nicht alle Komponenten sind gleich - einige werden global verwendet, andere sind auf bestimmte Anforderungen zugeschnitten. Diese Struktur eignet sich für kleine Projekte. Für mittlere bis große Projekte ist eine solche Struktur jedoch nicht akzeptabel.
//
├── containers
| ├── Dashboard.jsx
| ├── Details.jsx
├── components
| ├── Table.jsx
| ├── Form.jsx
| ├── Button.jsx
| ├── Input.jsx
| ├── Sidebar.jsx
| ├── ItemCard.jsx
// /
├── modules
| ├── common
| | ├── components
| | | ├── Button.jsx
| | | ├── Input.jsx
| ├── dashboard
| | ├── components
| | | ├── Table.jsx
| | | ├── Sidebar.jsx
| ├── details
| | ├── components
| | | ├── Form.jsx
| | | ├── ItemCard.jsx
Gruppieren Sie die Komponenten von Anfang an nach Route / Modul. Diese Struktur ermöglicht eine langfristige Unterstützung und Erweiterung. Dadurch wird verhindert, dass die Anwendung aus ihrer Architektur herauswächst. Wenn Sie sich auf die "Container-Komponenten-Architektur" verlassen, geschieht dies sehr schnell.
Die modulbasierte Architektur ist hoch skalierbar. Sie fügen einfach neue Module hinzu, ohne die Komplexität des Systems zu erhöhen.
Die "Containerarchitektur" ist nicht falsch, aber nicht sehr allgemein (abstrakt). Es wird niemandem sagen, der es anders lernt, als dass es React verwendet, um die Anwendung zu entwickeln.
Gemeinsame Module
Komponenten wie Schaltflächen, Eingabefelder und Karten sind allgegenwärtig. Auch wenn Sie kein komponentenbasiertes Framework verwenden, extrahieren Sie diese in gemeinsam genutzte Komponenten.
Auf diese Weise können Sie sehen, welche allgemeinen Komponenten in Ihrer Anwendung verwendet werden, auch ohne die Hilfe eines Storybooks . Dies vermeidet die Vervielfältigung von Code. Sie möchten nicht, dass jedes Mitglied Ihres Teams eine eigene Version der Schaltfläche entwirft, oder? Leider ist dies häufig auf eine schlechte Anwendungsarchitektur zurückzuführen.
Absolute Wege
Die einzelnen Teile der Anwendung sollten so einfach wie möglich geändert werden. Dies gilt nicht nur für den Komponentencode, sondern auch für dessen Position. Absolute Pfade bedeuten, dass Sie nichts ändern müssen, wenn Sie die importierte Komponente an einen anderen Speicherort verschieben. Dies erleichtert auch das Auffinden der Komponente.
//
import Input from '../../../modules/common/components/Input'
//
import Input from '@modules/common/components/Input'
Ich verwende das Präfix "@" als Indikator für das innere Modul, habe aber auch Beispiele für die Verwendung des Zeichens "~" gesehen.
Externe Komponenten einwickeln
Versuchen Sie, nicht zu viele Komponenten von Drittanbietern direkt zu importieren. Durch Erstellen eines Adapters für solche Komponenten können wir deren API bei Bedarf ändern. Wir können auch die an einem Ort verwendeten Bibliotheken ändern.
Dies gilt sowohl für Komponentenbibliotheken wie die semantische Benutzeroberfläche als auch für Dienstprogramme. Am einfachsten ist es, solche Komponenten aus dem gemeinsam genutzten Modul erneut zu exportieren.
Die Komponente muss nicht wissen, welche bestimmte Bibliothek wir verwenden.
//
import { Button } from 'semantic-ui-react'
import DatePicker from 'react-datepicker'
//
import { Button, DatePicker } from '@modules/common/components'
Eine Komponente - ein Verzeichnis
Ich erstelle für jedes Modul in meinen Anwendungen ein Komponentenverzeichnis. Zuerst erstelle ich eine Komponente. Wenn dann zusätzliche Dateien für die Komponente erforderlich sind, z. B. Stile oder Tests, erstelle ich ein Verzeichnis für die Komponente und platziere alle Dateien darin.
Es wird empfohlen, eine "index.js" -Datei zu erstellen, um die Komponente erneut zu exportieren. Auf diese Weise können Sie die Importpfade nicht ändern und Doppelarbeit des Komponentennamens vermeiden - "Formular aus 'Komponenten / UserForm / UserForm' importieren". Sie sollten den Komponentencode jedoch nicht in die Datei "index.js" einfügen, da dies das Auffinden der Komponente anhand des Namens der Registerkarte im Code-Editor unmöglich macht.
//
├── components
├── Header.jsx
├── Header.scss
├── Header.test.jsx
├── Footer.jsx
├── Footer.scss
├── Footer.test.jsx
//
├── components
├── Header
├── index.js
├── Header.jsx
├── Header.scss
├── Header.test.jsx
├── Footer
├── index.js
├── Footer.jsx
├── Footer.scss
├── Footer.test.jsx
Performance
Vorzeitige Optimierung
Stellen Sie vor Beginn der Optimierung sicher, dass es Gründe dafür gibt. Das blinde Befolgen von Best Practices ist Zeitverschwendung, wenn dies keine Auswirkungen auf die Anwendung hat.
Natürlich müssen Sie über Dinge wie Optimierung nachdenken, aber Sie sollten lieber lesbare und wartbare Komponenten entwickeln. Gut geschriebener Code ist einfacher zu verbessern.
Wenn Sie ein Problem mit der Anwendungsleistung feststellen, messen Sie es und ermitteln Sie die Ursache. Es macht keinen Sinn, die Anzahl der Renderings mit einer großen Bundle-Größe zu reduzieren.
Beheben Sie die Probleme nach dem Erkennen in der Reihenfolge ihrer Auswirkungen auf die Leistung.
Baugröße
Die Menge an JavaScript, die an den Browser gesendet wird, ist ein Schlüsselfaktor für die Anwendungsleistung. Die Anwendung selbst kann sehr schnell sein, aber niemand wird davon erfahren, wenn Sie 4 MB JavaScript vorladen müssen, um sie auszuführen.
Zielen Sie nicht auf ein Bündel. Teilen Sie Ihre Anwendung auf Routenebene und mehr auf. Stellen Sie sicher, dass Sie die Mindestmenge an Code an den Browser senden.
Laden Sie im Hintergrund oder wenn der Benutzer beabsichtigt, einen anderen Teil der Anwendung abzurufen. Wenn durch Klicken auf eine Schaltfläche ein PDF-Download gestartet wird, können Sie die entsprechende Bibliothek herunterladen, sobald Sie den Mauszeiger über die Schaltfläche bewegen.
Re-Rendering - Rückrufe, Arrays und Objekte
Sie sollten sich bemühen, die Anzahl der erneuten Rendern von Komponenten zu verringern. Beachten Sie dies, aber auch, dass unnötige Renderings selten einen signifikanten Einfluss auf die Anwendung haben.
Senden Sie keine Rückrufe als Requisiten. Bei diesem Ansatz wird die Funktion jedes Mal neu erstellt und ein erneutes Rendern ausgelöst.
Wenn Sie auf Leistungsprobleme stoßen, die durch Schließungen verursacht werden, beseitigen Sie diese. Aber machen Sie Ihren Code nicht weniger lesbar oder zu ausführlich.
Das explizite Übergeben von Arrays oder Objekten fällt in dieselbe Kategorie von Problemen. Sie werden anhand von Referenzen verglichen, sodass sie keine oberflächliche Prüfung bestehen und ein erneutes Rendern auslösen. Wenn Sie ein statisches Array übergeben müssen, erstellen Sie es als Konstante, bevor Sie die Komponente definieren. Auf diese Weise kann jedes Mal dieselbe Instanz übergeben werden.
Testen
Schnappschuss-Test
Einmal stieß ich beim Testen von Schnappschüssen auf ein interessantes Problem: Der Vergleich von "new Date ()" ohne Argument mit dem aktuellen Datum ergab immer "false".
Darüber hinaus führen Snapshots nur dann zu fehlgeschlagenen Assemblys, wenn die Komponente geändert wird. Ein typischer Workflow lautet wie folgt: Nehmen Sie Änderungen an der Komponente vor, bestehen Sie den Test nicht, aktualisieren Sie den Snapshot und fahren Sie fort.
Es ist wichtig zu verstehen, dass Snapshots keine Tests auf Komponentenebene ersetzen. Persönlich verwende ich diese Art von Tests nicht mehr.
Testen des korrekten Renderns
Der Hauptzweck des Tests besteht darin, zu bestätigen, dass die Komponente die erwartete Leistung erbringt. Stellen Sie sicher, dass die Komponente sowohl mit Standard- als auch mit übergebenen Requisiten das richtige Markup zurückgibt.
Stellen Sie außerdem sicher, dass die Funktion für bestimmte Eingaben immer die richtigen Ergebnisse zurückgibt. Überprüfen Sie, ob alles, was Sie brauchen, korrekt auf dem Bildschirm angezeigt wird.
Teststatus und Ereignisse
Eine zustandsbehaftete Komponente ändert sich normalerweise als Reaktion auf ein Ereignis. Erstellen Sie einen Ereignismock und überprüfen Sie, ob die Komponente korrekt darauf reagiert.
Stellen Sie sicher, dass Handler aufgerufen und die richtigen Argumente übergeben werden. Überprüfen Sie die korrekte Einstellung des internen Zustands.
Kantenfälle testen
Fügen Sie nach dem Abdecken des Codes mit grundlegenden Tests einige Tests hinzu, um nach Sonderfällen zu suchen.
Dies kann bedeuten, dass ein leeres Array übergeben wird, um sicherzustellen, dass nicht ohne Überprüfung auf den Index zugegriffen wird. Dies kann auch bedeuten, dass ein Fehler in einer Komponente (z. B. in einer API-Anforderung) aufgerufen wird, um zu überprüfen, ob er korrekt behandelt wurde.
Integrationstests
Integrationstest bedeutet das Testen einer ganzen Seite oder einer großen Komponente. Diese Art des Testens bedeutet das Testen der Leistung einer bestimmten Abstraktion. Es liefert ein überzeugenderes Ergebnis, dass die Anwendung wie erwartet ausgeführt wird.
Einzelne Komponenten können Komponententests erfolgreich bestehen, aber Interaktionen zwischen ihnen können Probleme verursachen.
Stilisierung
CSS-to-JS
Dies ist ein sehr kontroverses Thema. Ich persönlich bevorzuge Bibliotheken wie Styled Components oder Emotion, mit denen Sie Stile in JavaScript schreiben können. Eine Datei weniger. Denken Sie nicht an Dinge wie Klassennamen.
Der Baustein von React ist eine Komponente, daher ist die CSS-in-JS-Technik, genauer gesagt All-in-JS, meiner Meinung nach die bevorzugte Technik.
Bitte beachten Sie, dass andere Styling-Ansätze (SCSS, CSS-Module, Bibliotheken mit Stilen wie Tailwind) nicht falsch sind, ich empfehle jedoch weiterhin die Verwendung von CSS-in-JS.
Stilisierte Komponenten
Normalerweise versuche ich, die gestalteten Komponenten und die Komponente, die sie verwendet, in derselben Datei zu belassen.
Wenn jedoch viele gestaltete Komponenten vorhanden sind, ist es sinnvoll, sie in eine separate Datei zu verschieben. Ich habe diesen Ansatz in einigen Open-Source-Projekten wie Spectrum gesehen.
Daten empfangen
Bibliotheken zum Arbeiten mit Daten
React bietet keine speziellen Tools zum Abrufen oder Aktualisieren von Daten. Jedes Team erstellt eine eigene Implementierung, die normalerweise einen Service für asynchrone Funktionen enthält, die mit der API interagieren.
Dieser Ansatz bedeutet, dass es in unserer Verantwortung liegt, den Download-Status zu verfolgen und HTTP-Fehler zu behandeln. Dies führt zu Ausführlichkeit und viel Boilerplate-Code.
Stattdessen ist es besser, Bibliotheken wie React Query und SWR zu verwenden . Sie machen Serverinteraktionen auf idiomatische Weise zu einem organischen Teil des Komponentenlebenszyklus - mithilfe von Hooks.
Sie verfügen über eine integrierte Unterstützung für Caching, Lastzustandsverwaltung und Fehlerbehandlung. Sie machen es auch überflüssig, dass Zustandsverwaltungsbibliotheken diese Daten verarbeiten müssen.
Vielen Dank für Ihre Aufmerksamkeit und einen guten Start in die Arbeitswoche.