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.

Abbildung 1. Screenshot-Album / Tracks-Seite

 

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:

  1. 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.
  2. 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:

Albums-Komponente
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.

Listing: Track-Komponente
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 Beiträge von Richard Attermeyer

Richard Attermeyer arbeitet als Chief Solution Architect bei OPITZ CONSULTING. Er ist seit vielen Jahren als Entwickler, Architekt und Coach für die Themen Modernisierung, Architektur und agile Projekte tätig und hilft Unternehmen, mit motivierten Teams erfolgreiche Projekte zu realisieren.

Schreibe einen Kommentar