Eine für die Produktion bereite React-Baseline: Was jedes Paket tatsächlich tut
Einführen Sie Vite, Tailwind v4, Redux Toolkit, React Router, Jest und Prettier für eine React-Anwendung ein und verstehen Sie, warum jedes Paket sowie jede Konfigurationszeile vorhanden ist.
Durch Ausführung von npm create vite erhält man eine React-Anwendung, die zwar angezeigt werden kann, aber nicht so ist, wie man sie echten Nutzern zur Verfügung stellen würde: Es gibt kein Styling-System, keinen gemeinsamen Zustand, keine Routenverwaltung, keine Tests und kein einheitliches Codeformat. Diese Anleitung schafft Schritt für Schritt diese fehlenden Grundlagen mithilfe von Tailwind CSS, Redux Toolkit, React Router, Jest zusammen mit der React Testing Library sowie Prettier. Für jedes Paket werden zwei Fragen beantwortet: Was tut es eigentlich, und was bricht, wenn man es weglässt? Am Ende wird man eine funktionierende Basis haben, auf der weitere Funktionen entwickelt werden können – und genauso wichtig: Man wird in der Lage sein, seinen eigenen package.json zu lesen und jede Zeile darin zu erklären.
Der Technologiestack im Überblick:
- Tailwind CSS für das Styling
- Redux Toolkit für gemeinsame Anwendungsdaten
- React Router für die Navigierung zwischen Seiten
Einige davon lassen sich in einer Zeile installieren. Andere verbergen überraschende Details; „React Testing Library“ beispielsweise besteht tatsächlich aus drei Paketen mit jeweils unterschiedlichen Aufgaben.
Fangen Sie mit einem neuen Vite-Projekt unter Verwendung des React + TypeScript-Vorlagen an:
npm create vite@latest react-production-stack -- --template react-ts
cd react-production-stack
npm install
Tailwind CSS: Styling wird zuerst integriert
Pakete: tailwindcss, @tailwindcss/vite
Da das Styling jeden Komponenten betrifft, ist es sinnvoll, zuerst zu überprüfen, ob alles funktioniert, bevor weitere Elemente hinzugefügt werden.
npm install tailwindcss @tailwindcss/vite
Dies installiert beide Pakete als reguläre Abhängigkeiten statt als devDependencies. Genau genommen läuft keines der Pakete im Browser – das Vite-Plugin erledigt seine Arbeit während des Builds und nur der generierte CSS landet im Produktionspaket. Viele Teams ordnen sie daher unter devDependencies ein, und bei einer gebündelten Single-Page-App ergibt sich mit beiden Optionen dasselbe Ergebnis. Wählen Sie eine Konvention und bleiben Sie dabei konsequent.
Dann registrieren Sie das Plugin zusammen mit dem React-Plugin in der Vite-Konfiguration:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [react(), tailwindcss()],
})
Ersetzen Sie anschließend den Inhalt von src/index.css durch eine einzige Import-Anweisung. Das ist der gesamte Inhalt des Files:
/* Tailwind v4 is CSS-first. No config file, no content globs. */
@import 'tailwindcss';
Das ist wirklich die gesamte Einrichtung. Tailwind v4 arbeitet klassenbasiert: Es gibt weder ein tailwind.config.js noch eine Liste von Inhaltseinheiten, da es selbstständig nach Klassennamen in Ihren Quelldateien sucht.
Überprüfen, ob es funktioniert
Fügen Sie vorübergehend einige Utility-Klassen zu einem Überschriftstext in App.tsx hinzu, zum Beispiel text-3xl font-bold text-blue-600, führen Sie npm run dev aus und überprüfen Sie, ob sich die Überschrift ändert. Wenn das der Fall ist, sind das Plugin und die CSS-Importe miteinander verbunden.
Warum Utility-Klassen statt separater Stylesheets
Tailwind platziert die Styles direkt im Markup, das sie beeinflussen. Bei einem separaten CSS-Datei ist es leicht, ein Komponenten zu bearbeiten und dessen Stylesheet zu vergessen, wodurch sich allmählich nutzlose und veraltete Regeln ansammeln. Insbesondere Dashboards verwenden immer wieder dieselben Bausteine (Karten, Abzeichen, Schaltflächen), und deren Zusammenstellung aus einem gemeinsamen Satz an Utility-Klassen sorgt für visuelle Konsistenz bei geringerem Wartungsaufwand. Der Trade-off besteht in einer Gewöhnungsänderung: Anstatt Klassennamen wie .card-header-active zu erfinden, werden die einzelnen Elemente aus kleinen, vordefinierten Klassen zusammengesetzt.
Redux Toolkit: ein Store und eine Brücke zu React
Pakete: @reduxjs/toolkit, react-redux
Diese beiden Pakete lassen sich leicht verwechseln, doch sie erfüllen unterschiedliche Aufgaben:
@reduxjs/toolkitist der Store selbst: Er speichert die Anwendungsdaten und führt Updates an ihnen durch.react-reduxist die Verbindung zu React: Er stellt<Provider>sowie die Hooks bereit, die von den Komponenten verwendet werden, um diese Daten zu lesen und zu aktualisieren.
Sie benötigen beide, da keines von ihnen die Aufgaben des anderen übernehmen kann.
npm install @reduxjs/toolkit react-redux
Erstellen Sie den Store in src/app/store.ts. Er beginnt mit einem leeren Reducer-Map und exportiert zwei aus dem Store abgeleitete Typen, damit der Rest der Anwendung sie nicht manuell aufschreiben muss:
import { configureStore } from '@reduxjs/toolkit'
export const store = configureStore({
reducer: {},
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
Das Objekt reducer: {} bleibt vorerst leer. Slices werden hinzugefügt, sobald echte Funktionen wie Projekte oder Aufgaben sie benötigen; es hat keinen Sinn, Zustände vorher zu erfinden, bevor irgendeine Ansicht sie verwendet.
Anschließend werden typisierte Hooks in src/app/hooks.ts definiert. Die in neueren React Redux-Versionen verfügbaren withTypes-Hilfsfunktionen binden useDispatch und useSelector einmal an die Typen Ihres Stores, sodass Komponenten eine vollständige Typinferenz erhalten, ohne jede Aufrufstelle annotieren zu müssen:
import { useDispatch, useSelector } from 'react-redux'
import type { AppDispatch, RootState } from './store'
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
Übermittlung des Stores an den Komponentenbaum
Zu diesem Zeitpunkt existiert der Store bereits, aber React hat keine Kenntnis davon. <Provider> macht ihn für jede darunterliegende Komponente verfügbar, weshalb er ganz oben im Baum in src/main.tsx platziert wird:
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { Provider } from 'react-redux'
import { store } from './app/store'
import App from './App'
import './index.css'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<Provider store={store}>
<App />
</Provider>
</StrictMode>,
)
Jedes Element, das innerhalb von <Provider> dargestellt wird, kann nun useAppSelector und useAppDispatch aufrufen.
Überprüfung der Funktionsfähigkeit
Starten Sie die Anwendung und stellen Sie sicher, dass die Seite weiterhin ohne den Fehler "could not find react-redux context" angezeigt wird. Dieser Fehler tritt auf, wenn eine Komponente die Redux-Hooks außerhalb eines Providers verwendet. Mit einem leeren Store gibt es vorerst nichts anderes zu testen.
Wann Redux erforderlich ist und wann useState ausreicht
Nicht alles gehört in Redux, und das Hineinpacken aller Zustände in den Store ist genauso falsch wie das Behalten von allem lokal. Eine praktische Faustregel:
useStatefür Daten, die nur für einen einzigen Bildschirm oder eine Komponente relevant sind: ob ein Modal geöffnet ist, der aktuelle Wert eines Eingabefeldes oder die ausgewählte Option in einem Dropdown-Menü.
Falls ein Zustandswert sonst über mehrere Ebenen weitergegeben oder auf verschiedenen Bildschirmen dupliziert werden müsste, ist das ein gutes Zeichen dafür, dass er in den Store gehört.
React Router: Routing vor der ersten eigentlichen Seite
Paket: react-router
Das Hinzufügen von Routing, bevor es eine eigentliche Seite gibt, mag vorzeitig erscheinen, bringt aber schnell Vorteile: Jeder neue Bildschirm wird zu einer zusätzlichen <Route>, anstatt dass später eine Umstrukturierung der Anwendung notwendig wird.
npm install react-router
Platzieren Sie die Route-Tabelle in einem eigenen Modul, src/routes/AppRoutes.tsx. Derzeit wird / auf ein Platzhalterkomponente abgebildet, das mit Tailwind-Utilities gestylt ist:
import { Route, Routes } from 'react-router'
function Placeholder() {
return (
<div className="flex min-h-screen items-center justify-center">
<p className="text-slate-600">Routes coming soon</p>
</div>
)
}
export function AppRoutes() {
return (
<Routes>
<Route path="/" element={<Placeholder />} />
</Routes>
)
}
src/App.tsx rendernt anschließend einfach diese Route-Tabelle:
import { AppRoutes } from './routes/AppRoutes'
function App() {
return <AppRoutes />
}
export default App
Zum Schluss umschließen Sie die Anwendung mit <BrowserRouter> in src/main.tsx, neben dem Redux-Provider:
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { Provider } from 'react-redux'
import { BrowserRouter } from 'react-router'
import { store } from './app/store'
import App from './App'
import './index.css'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
</StrictMode>,
)
Die resultierende Kette ist main.tsx → <App /> → <AppRoutes /> → die jeweilige <Route>, die zur URL passt. Redux und der Router sind unabhängig, daher spielt die Reihenfolge ihrer Nestung keine Rolle; die einzige Voraussetzung ist, dass beide <App> umschließen.
Überprüfung der Funktionalität
Führen Sie npm run dev aus und öffnen Sie /. Wenn der Platzhaltext angezeigt wird, sind <BrowserRouter>, <Routes> und <Route> alle korrekt verbunden.
Jest und React Testing Library: vier Aufgaben, elf Pakete
Pakete: jest, @testing-library/react, babel-jest sowie weitere
Dies ist der schritt, der am längsten dauert. Die Konzepte sind nicht schwierig, doch „Tests hinzufügen“ bedeutet in Wirklichkeit das Installieren von etwa elf Paketen, die vier unterschiedliche Aufgaben übernehmen, und anschließend das Erreichen, dass ein Test mit dem Router funktioniert. Das Gruppieren der Pakete nach Aufgabe macht das Ganze viel verständlicher.
Gruppe A: der Testausführer und ein simulierter Browser
npm install -D jest jest-environment-jsdom
- jest ist der Ausführer. Er entdeckt
*.test.tsx-Dateien, führt sie aus und meldet Erfolge sowie Misserfolge. Ohne ihn funktioniert nichts anderes in diesem Abschnitt. - jest-environment-jsdom ist notwendig, weil Jest in Node läuft, wo es kein
documentgibt. Es stellt einen simulierten DOM bereit, damit Komponenten etwas zum Rendern haben.
Gruppe B: React Testing Library besteht aus drei Paketen
npm install -D @testing-library/react
npm install -D @testing-library/jest-dom
npm install -D @testing-library/user-event
Was die Leute als „React Testing Library“ bezeichnen, sind eigentlich drei Bibliotheken, jede mit ihrer eigenen Aufgabe:
- @testing-library/react rendernt eine Komponente auf der simulierten Seite und bietet Abfragen wie
screen.getByText(...)an.
toBeInTheDocument() hinzu, sodass Sie die Ergebnisse von Abfragen nicht manuell mit null vergleichen müssen.Kurz gesagt: Rendern, überprüfen, interagieren. Drei Aufgaben, drei Pakete – und Sie möchten fast immer alle davon.
Gruppe C: Die Babel-Toolkette, die es Jest ermöglicht, TSX zu lesen
Diese Gruppe existiert aus einem einzigen Grund: Jest kann TypeScript- oder JSX-Dateien nicht von selbst verstehen.
- babel-jest verbindet die beiden. Jest leitet jede Datei vor der Ausführung durch Babel.
: string sowie ähnliche Syntaxe aus.Im Gegensatz zur Gruppe B sollten diese Präsets zusammen in einem Befehl installiert werden. Alle Präsets erfordern ein kompatibles @babel/core, und ihre getrennte Installation in einem Projekt, das bereits Jest enthält (das seine eigenen Babel-Abhängigkeiten mitbringt), kann dazu führen, dass npm versucht, inkompatible Versionen miteinander in Einklang zu bringen. Ein bekanntes Symptom ist der Fehler ERESOLVE unable to resolve dependency tree bei der nächsten Installation eines einzelnen Pakets. Die gleichzeitige Installation der gesamten Gruppe ermöglicht es npm, eine einheitliche Version zu finden.
Eine zweite, subtilere Falle entsteht durch das Kopieren langer Befehle aus PDFs oder Webseiten. Text, der in weichen Zeilenformatierungen dargestellt wird, kann beim Einfügen zu echten Zeilenumbrüchen führen – dadurch wird ein Paketname wie @babel/preset-typescript in zwei Teile geteilt, und die Shell führt den zweiten Teil als separaten, sinnlosen Befehl aus. Explizite Zeilenerweiterungen sorgen dafür, dass die Brüche genau dort entstehen, wo man es möchte. Im Folgenden wird die Syntax des Windows Command Prompt verwendet:
npm install -D babel-jest ^
@babel/core ^
@babel/preset-env ^
@babel/preset-react ^
@babel/preset-typescript
Das am Ende stehende ^ signalisiert cmd.exe, dass der Befehl in der nächsten Zeile fortgesetzt wird. In PowerShell ist das Fortsetzungszeichen ein Backtick, und in bash oder zsh ein Backslash. Unabhängig von der Shell handelt es sich dabei dennoch um genau einen npm install-Befehl.
Gruppe D: Typen nur für Ihren Editor
npm install -D @types/jest
Dieses Paket hat keinen Einfluss darauf, wie Tests ausgeführt werden; Babel hat zu diesem Zeitpunkt bereits alle Typen entfernt. Es existiert, damit TypeScript und Ihr Editor globale Funktionen wie test(...) und expect(...) erkennen, anstatt sie als Fehler zu melden.
Hinzufügen der Testskripte
Durch das Installieren von Jest erhalten Sie keinen npm test-Befehl, daher fügen Sie die Skripte selbst in package.json hinzu:
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"test": "jest",
"test:watch": "jest --watch"
}
npm test führt den gesamten Testumfang einmal aus. npm run test:watch läuft weiter und führt nur die Tests aus, die durch die gerade gespeicherte Datei betroffen sind; halten Sie es in einem zweiten Terminal geöffnet, während Sie arbeiten.
Zwei weitere Befehle sind für den Fall, dass etwas schiefgeht, erwähnenswert:
npx jest src/App.test.tsx # run one file only
npx jest --clearCache # when Jest keeps showing an error
# you already fixed
Der Cache-Befehl ist wichtiger, als es auf den ersten Blick scheint. Jest speichert transformierte Dateien, sodass nach einer Änderung von babel.config.cjs oder jest.config.cjs weiterhin die alten Ausgaben bereitgestellt werden können und ein Fehler gemeldet wird, den Sie bereits behoben haben. Wenn eine Lösung scheinbar nicht funktioniert, leeren Sie den Cache, bevor Sie zu dem Schluss kommen, dass die Lösung falsch ist.
Die vollständige Testkonfiguration
Nachfolgend finden Sie alle Konfigurationsdateien in voller Länge, zusammen mit einer Erklärung dazu, wofür jeder Teil zuständig ist.
babel.config.cjs
Die Vorlagen spiegeln Gruppe C wider: Sie richten sich auf die aktuelle Node-Version aus, verwenden den automatischen JSX-Runtime, sodass Dateien React importieren müssen, und entfernen TypeScript. Das eingebettete Plugin kümmert sich um etwas, was Jest nicht kann: import.meta, das Vite-Code für Funktionen wie import.meta.env und den Hot-Module-Replacement verwendet, aber in der von Jest hier ausgeführten CommonJS-Ausgabe nicht gültig ist.
function stripImportMeta() {
return {
visitor: {
MetaProperty(path) {
path.replaceWithSourceString('({ url: "", hot: undefined })')
},
},
}
}
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
['@babel/preset-react', { runtime: 'automatic' }],
'@babel/preset-typescript',
],
plugins: [stripImportMeta],
}
Es handelt sich um ein echtes Babel-Plugin, das als Inline-Funktion statt als installiertes Paket geschrieben wurde; Babel akzeptiert beide Formen. MetaProperty ist der AST-Node-Typ, den Babel für import.meta verwendet, und der Visitor ersetzt jeden Vorkommen durch ein einfaches Objekt mit einem leeren url-Wert und einem undefinierten hot-Wert. Beachten Sie, dass dadurch auch alle Werte von import.meta.env vor dem getesteten Code versteckt werden, weshalb Komponenten, die Umgebungsvariablen lesen, diese separat emuliert haben müssen.
jest.config.cjs
Diese Datei verbindet den Runner mit allem anderen. Sie wählt die jsdom-Umgebung aus, lädt nach Bereitstellung der Umgebung eine Setup-Datei, leitet jede JavaScript- und TypeScript-Datei über babel-jest weiter und weist Importe von Styles und Bildern auf Stubs-Module zu.
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'mjs', 'json'],
transform: {
'^.+\\.(ts|tsx|js|jsx|mjs)