Ausgangssituation
Die Ausgangssituation ist ein React-Frontend, das als Tooling noch auf react-scripts setzt. Um dies zu modernisieren und alle Dependencies auf den aktuellsten Stand bringen zu können (vor allem TypeScript), habe ich das Tooling zu Vite migriert.
Warum Vite?
Vite wird als moderne Tooling-Variante beworben, die schnell und einfach ist und die Entwicklererfahrung verbessern soll. Dabei bietet Vite vor allem die folgenden Vorteile out-of-the-box:
- Schnellere Dev-Server-Startzeiten durch native ES-Module
- Hot Module Replacement (HMR)
- Vereinfachte Konfiguration
Vite installieren und react-scripts deinstallieren
In diesem Deep Dive verwende ich npm als Paketmanager, daher benutze ich die entsprechenden npm-Kommandos.
Als Erstes installiere ich Vite und das benötigte React-Plugin:
npm install --save-dev vite @vitejs/plugin-react
Danach entferne ich react-scripts aus den Dependencies:
npm uninstall react-scripts
package.json aktualisieren
In meiner package.json kann ich nun einfach die react-scripts-Befehle durch die entsprechenden Vite-Befehle ersetzen, zum Beispiel:
Vorher:
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build"
}
Nachher:
"scripts": {
"start": "vite",
"build": "vite build"
}
Umgebungsvariablen anpassen
In meinem Projekt gab es hier schon eine erste Schwierigkeit. Für die lokale Entwicklung benutze ich Umgebungsvariablen, die über mein Startscript dem Prozess mitgegeben wurden (über env-cmd aus einer .env-Datei):
"start": "docker-compose -f localdeployment/docker-compose.yml up -d && env-cmd -f .env.local react-scripts start"
Vite ersetzt Umgebungsvariablen statisch, und ich kann nicht mehr über process.env auf diese zugreifen. Stattdessen musste ich die Variablen in den einzelnen Dateien der jeweiligen Stage anpassen und wie folgt ändern:
REACT_APP_BASE_IMAGE_URL=/
wird zu
VITE_BASE_IMAGE_URL=/
Durch den Prefix VITE_ erkennt Vite die jeweilige Variable. Nach der Änderung habe ich nun Zugriff darauf über import.meta.env.VITE_BASE_IMAGE_URL und kann die entsprechende Stelle im Code anpassen.
Das zuvor benutzte Startscript ändert sich zu:
"start": "docker-compose -f localdeployment/docker-compose.yml up -d && vite"
Die Vite-Konfiguration erstellen
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import svgr from 'vite-plugin-svgr';
import eslintPlugin from 'vite-plugin-eslint';
export default defineConfig(({ mode }) => {
let base = '';
if (mode !== 'development') {
base = '/test/basepath';
}
return {
plugins: [react(), eslintPlugin(), svgr()],
base,
build: {
outDir: 'build',
sourcemap: false,
assetsDir: '',
cssCodeSplit: true,
rollupOptions: { output: {
entryFileNames: 'static/js/main.[hash].js',
chunkFileNames: (assetInfo) => {
if (assetInfo.name === 'index') {
return 'static/js/main.[hash].js';
}
return 'static/js/[name].[hash].chunk.js';
},
assetFileNames: (assetInfo) => {
if (assetInfo.name && assetInfo.name.endsWith('.css')) {
return 'static/css/main.[hash].css';
} else if (
assetInfo.name &&
/\.(png|jpe?g|gif|svg|webp|ico|avif)$/.test(assetInfo.name)
) {
return 'static/media/[name].[hash][extname]';
} else if (
assetInfo.name &&
/\.(woff2?|eot|ttf|otf)$/.test(assetInfo.name)
) {
return 'static/media/[name].[hash][extname]';
} else {
return 'static/media/[name].[hash][extname]';
}
},
},
},
},
server: {
port: 3000,
},
};
});
Um dasselbe Verhalten wie zuvor zu erhalten, musste ich einige Dinge konfigurieren.
Der wichtigste Punkt ist das React-Plugin, damit Vite weiß, dass ich eine React-App habe. Außerdem bietet Vite keinen integrierten ESLint-Support; diesen musste ich über ein Plugin hinzufügen:
npm install --save-dev vite-plugin-eslint
Zudem verwende ich in meiner Anwendung viele SVGs als React-Komponenten, was von Vite nicht nativ unterstützt wird. Dafür nutze ich das svgr-Plugin:
npm install --save-dev vite-plugin-svgr
Ein aufwendiger Punkt war, das Build so anzupassen, dass ich in meiner Pipeline und beim Einbinden des Frontends keine großen Änderungen im Vergleich zu react-scripts vornehmen musste. Deshalb habe ich die Struktur des vorherigen Builds weiterverwendet und in Vite entsprechend konfiguriert. Beispielsweise mussten das Bundling der Assets, deren Namensgebung sowie die Ordnerstruktur angepasst werden, um das Build korrekt auszuliefern.
Import von SVGs als React-Komponenten
Ursprünglich wurde das SVG wie folgt importiert:
import { ReactComponent as IconClose } from '../assets/icons/shared/close.svg';
Nach der Umstellung auf Vite erhielt ich jedoch den Fehler:
TS2614: Module "*.svg" has no exported member 'ReactComponent'. Did you mean to use 'import ReactComponent from "*.svg"' instead?
Durch die Nutzung des svgr-Plugins kann ich das SVG nun wie folgt importieren:
import IconClose from '../../assets/icons/shared/close.svg?react';
Damit habe ich buildseitig alles abgeschlossen, und die Anwendung kann lokal gestartet und in der Pipeline ausgeliefert werden.
Testing
react-scripts wurde auch zum Starten meiner Tests benutzt. Ein erster naiver Ansatz war, die bestehenden Tests direkt mit Jest auszuführen, da react-scripts unter der Haube ebenfalls auf Jest setzt. Nach vielen Fehlermeldungen und Anpassungen von Konfigurationen ohne großen Erfolg habe ich auf Vitest gesetzt. Obwohl auch hier einige Fehlermeldungen auftraten und eine Konfiguration erforderlich war, empfand ich das Aufsetzen als einfacher.
Letztlich habe ich folgende vitest.config.js erstellt:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import svgr from 'vite-plugin-svgr';
export default defineConfig({
plugins: [react(), svgr()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: 'src/setupTests.ts',
},
});
Wichtige Punkte hierbei waren der Import von svgr, damit die SVG-Komponenten in den Tests korrekt importiert werden können. Zudem benutze ich globale Funktionen und Werte, die gemockt werden müssen. Daher habe ich globals auf true gesetzt und ein Test-Setup konfiguriert.
Zusätzlich musste ich im Code jede Stelle, an der auf Jest-Funktionen zugegriffen wird, zu vi ändern. Zum Beispiel:
Vorher:
setClickable = jest.fn().mockImplementation((flag: boolean): void => { // ... });
Nachher:
setClickable = vi.fn().mockImplementation((flag: boolean): void => { // ... });
Nachdem ich diese Änderungen vorgenommen hatte, gab es noch einen Fehler mit SVGs aus einer Library. Diese wurden von den Tests als React-Komponenten importiert, aber in der Library selbst nicht als solche exportiert. Hier musste ich einen Mock anlegen, der zuvor nicht nötig war, um Fehler in den Tests zu vermeiden.
Fazit:
Die Umstellung auf Vite war mit einigen Hindernissen verbunden, die ich jedoch in angemessener Zeit lösen konnte. Insgesamt belief sich die Zeit für die Migration auf circa zwei Tage. Die größte Herausforderung war, das Ganze in ein bereits bestehendes Projekt zu integrieren.
Hinsichtlich der Vorteile oder Nachteile im Vergleich zu react-scripts fehlen noch Erfahrungswerte aus dem Projekt. Bis jetzt verhält sich der Wechsel aber mindestens positiv-neutral, hat keine ungeplanten Folgen gehabt, und ich setze nun ein modernes Build-Tool ein.



