Nachdem wir im ersten Teil dieser Serie die Vorteile von Typescript Fullstack Frameworks wie Remix betrachtet haben, wollen wir in diesem zweiten Teil damit anfangen, Remix in Action zu bringen. Dazu bauen wir eine Trackliste für Musikalben. Also: Kopfhörer anlegen und los geht’s.
Wir deployen Remix
Remix lässt sich auf unterschiedliche Arten deployen. Es hat keine feste Bindung etwa an Node.JS. Alles, was es braucht, ist ein kleiner Adapter. Die gibt es als offizielle Varianten vom Remix Team oder auch von der Community.
Wer Remix selbst betreibt, wird wahrscheinlich Node.JS, Deno oder Express verwenden. Wer Remix auf einem CDN betreibt, kann auch einen Adapter für Cloudflare oder Netlify verwenden. Wir werden Remix mit einem Node.JS Adapter verwenden. Den Source Code findet ihr auf github.
Wir verwenden für unser Beispiel eine einfache Datenbank, die Chinook-Datenbank. Die Chinook-Datenbank repräsentiert eine Musikverkaufsplattform mit Tabellen für Künstler, Alben, Tracks, Kunden, Bestellungen, …?
Damit wir uns um die Remix-bezogenen Aspekte kümmern können, gibt es mit dem branch blog-start
einen Startpunkt. Dort sind alle Tools und Technologien schon so aufgesetzt, dass wir loslegen können.
Wir legen unsere Track-Seite an
Wir wollen eine Seite aufbauen, die es erlaubt über eine Select-Box ein Album auszuwählen. Im unteren Bereich werden die Tracks angezeigt.
Wir definieren Remix-Routen
Die Frage, die wir uns stellen müssen, ist wie wir die Seiten und angezeigte Daten auf Remix-Routen mappen. Generell gibt es dazu mehrere Möglichkeiten:
- Wir bauen zwei Seiten und zwei Routen
/albums
und/albums/:albumId/tracks
. Dabei müssen wir aber überlegen, wie wir vermeiden, dass die Select Box nicht doppelt genutzt wird. - Wir bauen verschachtelte Routen
/albums
und/albums/:albumId/tracks
. Die erste Route stellt das generelle Layout bereit und die zweite Route wird an der entsprechenden Stelle eingebettet.
Wir werden die zweite Möglichkeit nutzen.
So baut ihr Layouts und Routen
Der Layout-Code für die /albums
-Route sieht wie folgt aus:
export default function Albums() {
// einige Zeilen ausgelassen
// ...
return (
<>
<h1>Albums</h1>
<Select data={[...comboBoxData]} placeholder={"Pick an album"} searchable name={"album"}
value={selectedAlbum}
onChange={(value) => {
setSelectedAlbum(value)
const album = getAlbum(value)
album ? navigate(`/albums/${album?.album_id}/tracks`) : navigate(`/albums`)
}}
label={"Albums"}
clearable
/>
<Outlet/> // rendered die verschachtelte Route
</>
);
}
Der Layout-Code für die /albums/:albumId/tracks
-Route sieht wie in Listing Track-Komponente aus. Er stellt im Wesentlichen eine Tabelle dar.
export default function Tracks() {
// einige Zeilen ausgelassen
return <>
<h1 id={"tracks"}>{album?.album_title}</h1>
<div>{album?.artist_name}</div>
<div>{tracks.length} tracks</div>
<MantineReactTable table={table}/>
</>
}
Die Konvention ist, dass die Dateien im /routes
-Ordner die Routen repräsentieren.
src/
app/
routes/
albums.tsx // Die Album Route
albums.$id.tracks.tsx // Tracks Route, mit einem dynamischen Segment $id
Es gibt eine sehr gute Visualisierung von Dilum Sanjaya, die zeigt, wie Routen auf dem Dateisystem zu URLs gemappt werden.
Die hier dargestellte Konvention ist die bevorzugte Methode, um Routen in Remix zu definieren. Es gibt aber auch andere Möglichkeiten, etwa über Ordner oder eine komplett manuelle Konfiguration.
So funktioniert die Verschachtelung
Was man hier sieht ist: ein Punkt .
im Dateinamen wird zu einem /
in der URL und sorgt für die Verschachtelung (nesting) der Routen.
Da man mit den Dateinamen auch die möglichen URLs definiert, gibt es weitere Möglichkeiten, je nach Anwendungsfall, etwa
- verschachtelte URLs ohne verschachteltes Layout
- verschachtelte Layouts ohne verschachtelte URLs
- Optionale URL Segmente
- Splat Routes, die beliebig viele Segmente aufnehmen können (den Rest der URL)
Die Dokumentation über Routen sollte man sich auf jeden Fall gründlich durchlesen, gerne auch nochmal, wenn man ein paar Seiten gebaut hat.
Wir nutzen das Route Modul
Nachdem wir die Routen definiert haben, müssen wir uns um das Data Loading kümmern. Unsere Routen enthalten aktuell nur den default export.
Die Dateien (Routen), die wir oben erzeugt haben, sind Typescript-(ESM)-Module. Das Route Modul kann verschiedene Exporte haben, die von Remix interpretiert werden.
Zu den typischen Exporten gehören:
- loader
- action
- Component (default export)
- ErrorBoundary
- headers
Letztere Funktion wird häufig verwendet, um die Cache-Control Header (für öffentliche Routen) zu setzen. Details zu den verschiedenen Exporten findet sich unter dem Stichwort Route Module in der Remix-Dokumentation.
So funktionert das Data Loading
Wir wollen jetzt alle Musikalben laden, um sie in der Select Box anzuzeigen. Dazu verwenden wir den loader-
Export.
export const loader = async ({request}: LoaderFunctionArgs) => {
const albums = await db.query.album_viewInChinook.findMany();
return json({albums});
};
Auf das Laden aus der Datenbank gehen wir hier nicht weiter ein. Die Anwendung verwendet drizzle-orm als ORM.
Interessant ist die Rückgabe über die json-Hilfsfunktion. Sie erzeugt ein JSON Response Object, welches in der Komponente verwendet werden kann:
export default function Albums() {
const {albums} = useLoaderData<typeof loader>();
// ...
}
Dabei ist unerheblich, ob die Komponenten auf dem Server oder auf dem Client gerendert wird. Remix sorgt dafür, dass die Daten, die im Loader geladen wurden, in der Komponente genutzt werden können. Danach kann man einfach auf das Typescript-Objekt zugreifen. Dies vereinfacht das Zusammenspiel mit dem Backend enorm.
So klappt die Navigation
Für die Navigation verwenden wir eine Mantine Select Box. Mittels onChange
navigieren wir dort zur nächsten Route. Dazu nutzen wir einen weiteren Remix Hook useNavigate
.
onChange={(value) => {
setSelectedAlbum(value)
const album = getAlbum(value)
album ? navigate(`/albums/${album?.album_id}/tracks`) : navigate(`/albums`)
}}
Wenn ein Album ausgewählt wurde, navigieren wir die Route an, die auch die Tracks darstellt. Im anderen Fall, wenn etwa die Select-Box geleert wird, navigieren wir zurück zur Album-Route.
Ähnlichkeiten: Remix und React Router
Übrigens stammt Remix von den Machern von React Router. Deshalb heißt es auf der Webseite mittlerweile „Made by Remix“. Und so kommt es auch, dass sich <Outlet/>
, loader()
, useNavigate()
, json()
und viele andere Hooks und Utilities auch in React Router finden. Bei Remix sind die Module hingegen aus @remix-run/react
zu importieren. Dies bedeutet aber auch, dass das Wissen für beide Frameworks wiederverwendet werden kann. React Router kennt ebenso das Konzept von verschachtelten Routen. Ein Tipp: Manchmal ist es hilfreich, in die Dokumentation von React Router zu schauen, wenn man etwas in der Remix-Dokumentation nicht findet!
Wir verwenden Track-Route und ErrorBoundaries
Hier gibt es nichts Neues bzgl. des Ladens und Anzeigens der Tracks. Auch hier nutzen wir Mantine React Table, um die Tracks anzuzeigen. Spannender ist es, die Funktion der Error Boundaries zu betrachten:
Dazu definieren wir im Track-Modul eine Funktion ErrorBoundary
:
export function ErrorBoundary() {
const error = useRouteError()
console.log(error)
return <div>
<h1>Nothing to Display</h1>
</div>
}
Wenn wir jetzt im Loader einen Fehler werfen:
export const loader = async ({request, params}: LoaderFunctionArgs) => {
// ausgeblendeter Code...
throw new Error('test')
return json({tracks, album});
}
Dann sehen wir statt der Trackliste die Fehlermeldung „Nothing to Display“. Es ist also nur ein Teil der Seite betroffen und nicht die gesamte Anwendung.
Auch in der neuesten React Router Version 6.4 gibt es für die Data Router die Möglichkeit, Error Boundaries zu definieren.
Summary
Das Laden von Daten, Navigation und Error Boundaries sind die wichtigsten Konzepte von Remix. Dadurch, dass Remix das Thema REST Kommunikation kapselt, vereinfacht es die Kommunikation mit dem Backend enorm.
Im nächsten Teil dieser Serie geht es darum, in Remix Daten zu erstellen und zu ändern. Insbesondere, wenn ihr mit Geschäftsanwendungen zu tun habt, solltet ihr den Blogpost nicht verpassen!
Alle Teile dieser Blogserie:
Teil 1: Remix – eine Alternative für Geschäftsanwendungen
Teil 2: Remix – Routes und Data Loading
Teil 3: Remix – Formulare, Validierung und Datenänderungen