Blog / 19. Mai 2022

Von remix.run zurück zu Next.js

Michel Smola
AutorinMichel Smola, Softwareentwicklung

Moin, ich bin Michel. Ich entwickle seit vielen Jahren professionell Webseiten und momentan besteht mein Berufsalltag bei farbenmeer aus Anwendungsentwicklung, technischer Führung und Beratung in Kundenprojekten mit Next.js.

Ich bastle seit Jahren nebenbei an einer Web-App, die sich primär mit der Darstellung verschachtelter Daten beschäftigt. Für diese App habe ich bereits diverse Technologie-Stacks durchprobiert, weshalb sie sich als Playground für den Vergleich anbietet.

Dieses Anwendungsfeld erscheint als perfektes Beispiel für das neue Framework remix.run , von dem gerade alle reden und dessen nested routing features . Daher habe ich mich hingesetzt und versucht, die App in remix.run nachzubauen. Zum Vergleich steht natürlich Next.js, das ich täglich nutze und das man wohl als Industriestandard bezeichnen darf.

Routing

Das routing funktioniert auf den ersten Blick ähnlich wie in Next.js und fühlt sich daher vertraut an. remix.run's loader funktionieren ähnlich wie Next.js' getStaticProps und getServerSideProps-Methoden. Der essenzielle Unterschied ist, dass in remix.run mehrere Routen (und damit auch mehrere loader) ineinander verschachtelt werden können, während in Next.js eine Seite immer genau durch eine React-Komponente und eine getStaticProps/getServerSideProps abgebildet wird.

// Next.js
pages/
 |- index.tsx
 |- blog/
 |   |- von-remix-zurueck-zu-next-js.tsx
 
// remix.run
routes/
 |- index.tsx
 |- blog.tsx
 |- blog/
 |   |- von-remix-zurueck-zu-next-js.tsx

In diesem Beispiel müsste ich in Next.js also alle Datenabhängigkeiten dieses Blog-Posts in blog/von-remix-zurueck-zu-next-js.tsx abbilden, während ich in remix.run gemeinsame Abhängigkeiten und gemeinsames Layout der unterschiedlichen Blog-Posts in blog.tsx abbilden kann. Das ist erstmal nützlich, insbesondere in einer App die verschachtelte Datensätze darstellt.

Ein Beispiel: eine SaaS Plattform kennt Organisationen, denen jeweils Arbeitsgruppen und diesen wiederum Angestellte zugeordnet sind. Die Seite unter der URL /organisationen/farbenmeer/arbeitsgruppen/blog/mitglieder/michel zum Beispiel, profitiert davon. Während hier in Next.JS etwas wie

// organisationen/[organization]/arbeitsgruppen/[workgroup]/mitglieder/[member].tsx
const getServerSideProps = async ({ params }) => {
  const organization = getOrganization(params.organization)
  const workgroup = getWorkgroup(params.workgroup)
  const member = getMember(params.member)
 
  [...]
}

notwendig ist, wovon sich ein Großteil auf weiteren Seiten wie /organisationen/farbenmeer oder /organisationen/farbenmeer/apps wiederholt, glänzt in diesem Beispiel remix.run:

// organisationen/$organization.tsx
const loader = async ({ params }) => {
  const organization = getOrganization(params.organization)
  [...]
}
// organisationen/$organization/arbeitsgruppen/$workgroup.tsx
const loader = async ({ params }) => {
  const workgroup = getWorkgroup(params.workgroup)
  [...]
}
// organisationen/$organization/arbeitsgruppen/$workgroup/mitglieder/$member.tsx
const loader = async ({ params }) => {
  const member = getMember(params.member)
  [...]
}

Ich muss mich also weniger wiederholen. Das wäre soweit schön und gut, wenn meine Abhängigkeiten immer sauber getrennt wären.

Sind sie aber nicht. In der Praxis habe ich schnell gemerkt dass dieses Prinzip zur Folge hat dass häufig mehrere loader-Funktionen dieselben Daten abfragen. Zum Beispiel möchte ich in der Ansicht zur Workgroup auch informationen über die Organisation an sich anzeigen. Umgekehrt sollen in der Organisation Informationen über bestimmte Arbeitsgruppen oder Mitglieder angezeigt werden. Das führt dann tatsächlich dazu, dass remix bei einem page change 3 requests an die drei unterschiedlichen loader macht, die zum Teil redundante Daten aus der Datenbank abfragen:

URL                                       Database Table
/organization/$organization/_data.json   -> Organization
                                         -> Workgroup
                                         -> Member
.../arbeitsgruppen/$workgroup/_data.json -> Organization
                                         -> Workgroup
.../mitglieder/$member/_data.json        -> Member

Ich will nicht behaupten, dass es nicht möglich ist, die überflüssigen Requests wegzuoptimieren aber dann verliere ich auch die DX-Vorteile der mehreren loader. Damit schließt thematisch passend an:

GraphQL

Ich nutze gerne GraphQL und graphql-codegen , um mir typensichere bindings zu meinem Backend in Typescript zu erzeugen und mit großartigem Editor-Support und schemabasierter Autovervollständigung meine Queries gegen ein CMS oder Backend zu bauen. Einer der Hauptvorteile von GraphQL ist, dass man (im idealfall) nur eine einzige Query für alle Daten für die jeweilige View braucht. Das funktioniert schlicht und ergreifend nicht mit mehreren loader-Funktionen.

Typescript

Die Konvention in remix.run ist etwas weniger strikt mit Dingen, wie Typisierung umzugehen. Zum Beispiel bietet der Typ LoaderFunction, der für die loader verwendet wird, keine Type-Parameter an (der return type des loader ist immer any). Die Konsquenz ist, dass ich, um ein Typechecking für die geladenen Daten zu bekommen, etwas schreiben muss wie

type Data = { name: string }

const loader: LoaderFunction = async ({ params }) => {
  const data: Data = { name: params.name }
  return data
}

function Page() {
  const data = useLoaderData<Data>()
  return <div>Hello, {data.name}</div>
}

Hier (und an diversen anderen Stellen) fühlt sich das für mich nach einem Rückschritt an. Die lose Typisierung führt dazu, dass ich selbst darauf achten muss, meine Schnittstellen zwischen server-side code (loader) und client-side code (Page) richtig zu typisieren. Das geht manchmal unter und führt zu unnötigen Fehlern.

Internationalisierung

Die Internationalisierung ist nicht eingebaut. Ist natürlich kein must-have und nicht schwer von Hand zu implementieren, trotzdem werden die routen schnell lang und das Next.js-Internationalisierungs-Feature macht einem das Leben schon erheblich leichter. Das gleiche gilt für Bildoptimierung.

Authentifizierung & Autorisierung

Für die Authentifizierung und Autorisierung nutzt man in Next.js-Apps in der Regel (wenn keine bestehende Lösung vorliegt) Next-Auth . Next-Auth tut viel von selbst, funktioniert mit viel Magie und einigen hässlichen Hacks (wie zum Beispiel der getSession-Funktion, die einen internen endpoint aufruft). Das Äquivalent für remix.run ist remix-auth . remix-auth ist modular aufgebaut und orientiert sich an passport.js . Es braucht kaum Magie und nutzt die Stärken von remix wie zum Beispiel das eingebaute cookie/session handling. Insgesamt hat mir remix-auth deutlich besser gefallen, schon alleine weil ich das Gefühl habe, die Prozesse viel besser zu verstehen.

Interaktion

remix.run verfolgt das Prinzip, die Interaktion mit der Webanwendung über Elemente abzuwickeln, die im Kern sehr ähnlich funktionieren wie klassische Web-Formulare. Ich baue ein Formular, beim Absenden wird ein POST-Request an den Server gemacht, der ruft eine Funktion auf. Diese Funktion heißt in remix.run action und wird analog zum loader definiert. Das Ganze funktioniert im Zweifel sogar ohne javascript.

Diese Konzept ist brilliant und auch das, was ich am meisten vermisse beim Schritt zurück zu Next.js. Es nimmt mir das gesamte state handling für fast alle User-Interaktionen ab und ersetzt dieses mit deklarativem Code. Und dabei erfindet remix.run noch nicht einmal das Rad neu sondern ich muss mich nur zurückerinnern an die vor über 10 Jahren gelernten Grundlagen.

Hier sollte Next.js definitiv nachziehen und ein next/form-Modul oder etwas ähnliches bereitstellen.

Error Boundaries

Next.js bietet die Möglichkeit, selbst Fehler-Seiten zu definieren. remix.run erweitert dieses Konzept und bietet die Möglichkeit, in jeder Route zusätzlich eine ErrorBoundary und eine CatchBoundary zu definieren. Dabei fängt die eine javascript-Errors ab und die andere HTTP-Errors, also responses mit status 404, 500 etc.

Das scheint auf den ersten Blick overkill und verwirrend und genau das ist es auch.

Fazit

Alles in allem habe ich mich entschieden, mit meiner App auf Next.js zu setzen statt auf remix.run.

Next.js ist schlicht und ergreifend der Platzhirsch. Es ist weit verbreitet, sehr sehr gut dokumentiert und supportet und bietet einfach einen Haufen kleiner Features an, für die remix.run noch keine eingebaute oder kanonische Lösung hat. Das ist am Ende, wofür mein Framework da ist. Insbesondere das Error-reporting von Next.js ist deutlich ausgefeilter. In remix.run bin ich doch deutlich öfter in kryptische Fehlermeldungen gerannt. Das mag aber bias sein, weil ich Next.js natürlich besser kenne.

Das nested routing und die (mehereren) loader waren das Feature auf das ich am meisten gespannt war aber waren mir am Ende mehr im Weg als sie genützt haben. Mag sein, dass ich hier einfach noch nicht richtig durchgestiegen bin.

Native <form>-Elemente für die Interaktion finde ich so gut, dass ich versucht habe, das Konzept in Next.js nachzubauen, allerdings mit mäßigem Erfolg. Ein eingebautes next/form-element und besserer Support für POST-Requests in getServerSideProps wären großartig.

LinkedIn

Diskutiere diesen Artikel mit uns auf LinkedIn


Umfrage

Welches Thema interessiert dich am meisten?