- Viel statischer Content
- Viel JavaScript
- ...gleichzeitig wenig Interaktion
- Die Seiten sollen möglichst schnell für den Benutzer sichtbar und bedienbar sein
- Viel JavaScript-Code, der...
- ...vom Browser geladen werden muss
- ...interpretiert und ausgeführt werden muss
- ...mit jeder Komponente mehr wird
-
Bei SSR wird die Anwendung auf dem Server ausgeführt
-
Der Server schickt fertiges HTML zum Client
- Gut: Client braucht HTML nur anzuzeigen (schnell!)
- Gut: Kein JavaScript für die Darstellung notwendig
-
Ebenfalls wird der komplette Anwendungscode zum Client geschickt
- 😢 Auch für statische Komponenten
- 😢 Bandbreite! Performance!
- 👉 SSR löst Probleme... aber nicht alle
Routing und Data Fetching benötigen Bibliotheken
JavaScript-Code (im Browser) wächst mit jedem Feature
Laden von Daten kann Eindruck langsamer App erzeugen
Frühe Darstellung auch bei schlechtem Netzwerk/Hardware
Ausführung von React-Code auf Server/im Build ist kompliziert
- Teilt ihr die Einschätzung des React-Teams?
- Habt ihr andere Probleme, die in der Aufzählung fehlen?
- Komponenten, die auf dem Server, Client und im Build gerendert werden können
- Data Fetching "integriert"
- Platzhalter für "langsame" Teile einer Seite
- Mit Streaming können diese Teile einer Seite "nachgeliefert" werden, sobald sie gerendert sind
- Server Components erfordern Rendern auf dem Server oder im Build
- Dazu braucht man ein "Fullstack-Framework"
- "Framework" ist verharmlosend, weil es sich in der Regel um einen kompletten Stack samt Build-Tools und Laufzeitumgebung handelt
- Deswegen werden solche Frameworks auch als "Meta-Frameworks" bezeichnet (=> Sammlung von Frameworks)
- Next.js entspricht den Vorstellungen des React-Teams
- Remix (vom React Router Team) unterstützt noch keine RSC, hat aber ähnliche Features
- Unterstützung für RSC in Planung
- Idee: Komponenten werden nicht im Client ausgeführt
- Sie stehen auf dem Client nur fertig gerendert zur Verfügung
- Der Server schickt lediglich eine Repräsentation der UI, aber keinen JavaScript-Code
- Das Format ist (im Gegensatz zu SSR) nicht HTML
- Kann aber mit SSR kombiniert werden
- React bzw. JavaScript muss also im Client laufen
- Client-Komponenten (wie bisher)
-
Werden auf dem Client gerendert
-
oder auf dem Server 🙄
-
Wie bisher:
- JavaScript-Code wird vollständig zum Client gesendet
- Der JavaScript-Code wird auf dem Client ausgeführt
- Die Komponenten können interaktiv sein
- Event-Listener etc.
-
- Neu: Server-Komponenten
-
werden auf dem Server gerendert
-
oder im Build 🙄
-
liefern UI (!) zum React-Client zurück (kein JavaScript-Code)
-
Werden im Client nicht "ausgeführt"
-
...und können folglich nicht interaktiv sein (nur ohne JS)
-
- Die Komponenten gemischt werden:
- Server-Komponenten können Client-Komponenten einbinden
- (umgekehrt geht es nicht)
- Dann wird alles bis zur ersten Client-Komponente gerendert an den Client gesendet
- (Mit SSR auch die Client-Komponenten)
- Server-Komponenten können Client-Komponenten einbinden
- App-Router: aktueller Router (seit Version 13.4), der RSC unterstützt
- (Pages-Router: ohne RSC)
- File-system-basierter Router, es spielt sich alles unterhalb von
app
ab - In Next.js ist ein Ordner unterhalb von
app
eine Route, wenn darin einepage.tsx
-Datei liegtpage.tsx
vergleichbar mitindex.html
in klassischem Web-Server- Pfade, die keine
page.tsx
-Datei haben, tauchen zwei in der URL auf, können aber nicht aufgerufen werden
- Diese Datei exportiert per
default export
eine React-Komponente, die für die Route dargestellt werden soll -
// /app/page.tsx export default function LoadingPage() { return <h1>Hello World!</h1> } // /app/posts/page.tsx export default function PostListPage() { return <h1>Blog Posts</h1> }
- In einem Route-Verzeichnis kann es weitere Dateien geben, die einen festgelegten Namen haben und jeweils per
default export
eine React-Komponente zurückliefern: layout.tsx
: Definiert die Layout-Komponente.- Damit kann über mehrere Routen ein einheitliches Layout festgelegt werden, denn wenn eine Seite gerendert wird, werden alle Layout-Komponenten aus den Pfaden darüber verwendet. So kann eine Hierarchie von Layouts gebaut werden.
error.tsx
: Eine Komponente, die als Error Boundary fungiert und gerendert wird, wenn beim Rendern derpage
ein Fehler aufgetreten istloading.tsx
: Loading-Spinner o.ä., der dargestellt wird, bis die Seite gerendert werden kann (dzau später mehr)not-found.tsx
: Kann verwendet werden, um einen Fehler darzustellen, wenn eine SeitenotFound
zurückliefert
- Jede Route kann eine Layout-Komponente haben
- Dieser Komponente wird die darzustellende Seite als
children
-Property übergeben - Layout-Komponenten können verschachtelt sein
- Wenn eine Route keine Layout-Komponente hat, wird im Baum oberhalb nach der nächstgelegenen Layout-Komponente gesucht
- Die Layout-Komponente für die Root-Route ist pflicht. Hier muss eine ganze HTML-Seite beschrieben werden
-
// /app/layout.tsx export default function Layout({children}) { return <html> <head>...</head> <body> <header><Navigation /></header> <main>{children}</main> </body> <html> }
- Zum Rendern von Links bringt Next.js eine eigene
Link
-Komponente mit- Mit einem entsprechenden Plug-in für TypeScript soll die sogar typsicher sein, so dass man keine Routen-Angaben hinschreiben kann, die es gar nicht gibt
- (hat bei mir beim letzten Versuch nur eingeschränkt funktioniert)
- Mit einem entsprechenden Plug-in für TypeScript soll die sogar typsicher sein, so dass man keine Routen-Angaben hinschreiben kann, die es gar nicht gibt
- Verwendung ähnlich wie auch vom React Router (und
a
-Element) gewohnt:<Link href="/">Home</Link>
- Alle Komponenten in Next.js sind per Default Server Components
- Ausnahmen (Client Komponenten) müssen explizit gekennzeichnet werden (dazu später mehr)
- Landing-Page `/page.tsx`
- `/layout.tsx`
- `console.log` in `page`-Komponente
- Klonen des Repositories
- Bitte klonen: https://nilshartmann.github.io/react-fullstack-workshop
- Darin bitte das Verzeichnis
nextjs/nextjs-workspace
im Editor/IDE öffnen
- Zum Ausführen der Übungen bitte die benötigten Packages installieren
- Das funktioniert mit
pnpm
sollte aber auch mit einem anderen Package Manager verwenden - Wenn Du kein
pnpm
hast, kannst Du den aktivieren, in dem Ducorepack enable
ausführst - Dann das Backend starten
- Verzeichnis:
backend
pnpm install
pnpm dev
- Verzeichnis:
- Dann Next.js im Verzeichnis
nextjs/nextjs-workspace
ausführen:pnpm install
pnpm dev:clean
- Die leere Anwendung läuft dann auf http://localhost:3000 (da gibt's aber erstmal noch nichts zu sehen...)
- Achtung! Next.js hat sehr aggressives Caching eingebaut
- Wenn ihr "komisches" Verhalten feststellt, meine Empfehlung:
pnpm dev:clean
neu ausführen- Im Browser neuen Tab öffnen, oder in den Dev Tools Caching ausschalten oder Inkognito Modus verwenden
- Wenn ihr "komisches" Verhalten feststellt, meine Empfehlung:
- Baue eine LandingPage (
/
-Route) - Die muss nicht hübsch sein
- wenn Du willst, kannst Du CSS-Modules und/oder Tailwind für Styling verwenden
- unter
shared/components
findest Du auch ein paar Basis-Komponenten (Button, Überschriften etc.), die Du benutzen kannst, wenn Du möchtest
- Die Komponente soll einen Link auf
/blog
rendern- Verwende dazu die
Link
-Komponente des Next.js Routers
- Verwende dazu die
- Füge außerdem ein
console.log
-Statement in deine Komponente hinzu, das beim Rendern die aktuelle Uhrzeit ausgibt - Lege außerdem eine Komponente für
/blog
an- Es reicht, wenn diese Komponente erstmal nur "Hello World" ausgibt.
- Die
page.tsx
-Datei soll in das Verzeichnisapp/(content)/blog/page.tsx
- Wenn die Seite fertig ist:
- Baue die "Anwendung" (
pnpm build
) - Starte die fertige Anwendung, die auf Port 3080 läuft (
pnpm start
) - Wann und wo wird dein
console.log
ausgegeben?
- Baue die "Anwendung" (
- Mögliche Lösung findet ihr in
steps/10_getting_started
-
Komponente, die Daten benötigen, können diese direkt in der Komponente laden
-
Kann Latenz sparen und bessere Performance bringen
- "No Client-Server Waterfalls"
-
Server Components können die Server-Infrastruktur nutzen (DB, Filesystem)
-
👉 Server-Komponenten können dazu asynchron sein
-
Das ist ein React-Feature!
- Next.js-spezifisch nur die
page
-Konvention
- Next.js-spezifisch nur die
- PostListPage anlegen
- DB-Zugriff mit `getBlogTeaserList`
- statische Komponente bislang! Build! console.log!
- Kennt ihr zod? https://zod.dev/ 🤔
- Suspense unterbricht das Rendern, wenn in einer Komponente "etwas" fehlt
- "Etwas" ist im Fall von RSC ein Promise, das nicht aufgelöst ist
- Dazu kann um eine Komponente die
Suspense
-Komponente von React gelegt werden -
async function loadData(...) {} async function PostList() { const posts = await loadData(); return <>...</>; } function PostListPage() { return <Suspense> fallback={"Please wait"}> <PostList /> </Suspense> }
- Hier würde React zunächst die
fallback
-Komponente (Please wait
) rendern und darstellen - Wenn das Promise aufgelöst wird, rendert React dann die Komponente erneut für die finale Darstellung
- Das geht auf dem Server und dem Client
- Client für Lazy-Loading und Data-Fetching (letzteres noch unstabil)
-
Um die oberste Komponente einer Route (
page.tsx
) legt Next.js eine automatisch eineSuspense
-Komponente -
Den
fallback
dafür implementieren wir in der Dateiloading.tsx
, die eine Komponente perdefault export
exportieren muss -
Konzeptionell sieht das so aus:
- Eure Route:
-
// loading.tsx export default function Spinner() { return "Please Wait" }; // page.tsx export default async function PostListPage() { const data = await loadData(); return <>...</> }
- Next.js (dummy code)
-
// Next.js (dummy code): import Fallback from "loading.tsx" import Page from "page.tsx"; function Route() { return <Suspense fallback={Fallback}> <Page /> </Supsense>; }
-
(Suspense auf dem Client gucken wir uns später unabhängig von Next.js an)
- Vervollständige die
/blog
-Route- Darin mit
getBlogTeaserList
die Daten laden und anzeigen- Zur Darstellung der geladenen Posts kannst Du die Komponente
PostTeaser
verwenden oder was eigenens bauen
- Zur Darstellung der geladenen Posts kannst Du die Komponente
- Was passiert, wenn die Daten nur sehr langsam geladen werden?
- Verlangsame dazu den Zugriff auf die Datenbank künstlich, in dem Du in
backend-queries.ts
mit der KonstantegetBlogTeaserListSlowdown
eine künstliche Verzögerung festlegst (in Millisekunden, z.B. 1600)
- Verlangsame dazu den Zugriff auf die Datenbank künstlich, in dem Du in
- Füge eine Loading-Komponente (
loading.tsx
hinzu), die eine Warte-Meldung ausgibt - Anstatt der Loading-Komponenten:
- kannst Du in
layout.tsx
um diechildren
eineReact.Suspense
-Komponente legen? Was passiert dann?
- kannst Du in
- Darin mit
- Neben den "klassischen" Verzeichnisnamen, die URL-Segementen entsprechen, gibt es noch weitere Konventionen:
- Ein Pfad in Klammern (
(path)
) taucht in der URL nicht auf. Kann z.B. für eine gemeinsame Layout-Datei oder zur besseren Organisation verwendet werden, wenn man das nicht über die Hierarchie machen kann. -
// /admin/user // /admin/articles // /admin/tags
- Wenn
articles
undtags
sich ein Layout teilen soll (aber/user
nicht), kann die Verzeichnisstruktur dafür so aussehen: -
// /admin/user/page.tsx // /admin/(blog)/layout.tsx // /admin/(blog)/articles/page.tsx // /admin/(blog)/tags/page.tsx
- Ein Pfad in eckigen Klammern (
/blog/[postId]
) definiert einen Platzhalter. Der Wert für das Segment in der URL wird der Komponente dann zur Laufzeit als Property übergeben:/blog/P1
,/blog/xyz
etc.
-
// /app/blog/[postId]/page.tsx type BlogPostPageProps = { params: { postId: string }; }; export default function PostPage({params}: BlogPostPageProps) { // params.postId enthält den Wert aus der URL (P1, xyz, ...) const postId = params.postId; }
- Mit der
notFound
-Funktion kann dienot-found
-Komponente aufgerufen werden - Das ist zum Beispiel nützlich, wenn Daten geladen wurden, die es nicht gibt
notFound
bricht die Ausführung der Komponenten-Funktion ab, man braucht keinreturn
hinzuschreiben-
// /app/blog/[postId]/page.tsx export default async function PostPage({params}: BlogPostPageProps) { const postId = params.postId; const blogPost = await getBlogPost(postId); if (!blogPost) { notFound(); // kein return notwendig } return <Post post={blogPost} />; }
- Beispiel zeigen. Suspense-Verhalten ?!
- Durch die Verwendung eines Platzhalters wird eine Route zu einer dynamischen Route, d.h. sie wird nicht im Build gerendert, sondern nur zur Laufzeit
- Next.js kann hier nicht im Vorwege wissen, welche Werte für das variable Segment verwendet werden
- Mit
getStaticPaths
kann das geändert werden
- Auch die Verwendung einiger Next.js APIs führt dazu, dass eine Route nicht mehr statisch, sondern dynamisch ist
- Das betrifft Funktionen, die mit Daten aus einem Request arbeiten (
headers()
undcookies()
)
- Das betrifft Funktionen, die mit Daten aus einem Request arbeiten (
- Ggf. wird das Ergebnis auf dem Server gecached
- Wenn eine Komponente auf dem Server gerendert wird, kann React das Rendern bei einer
Suspense
-Komponente unterbrechen - Dann wird der Rest der Seite schon zum Client gesendet
- Sobald die Komponenten unterhalb von
Suspense
gerendert werden konnten, werden diese zum Client nachgesendet - Dieses Verhalten wird auch Streaming genannt.
- Die
BlogPostPage
-Komponente benötigt Daten aus zwei Quellen: Den Blog-Post und die Kommentare - Die Antwortzeit der beiden Requests dafür kann bei jedem Aufruf unterschiedlich lang sein
- In einer klassischen React-Architektur könnte es zu einem "Request-Wasserfall" kommen:
- BlogPost lädt den Artikel (z.B.
useFetch
) und rendert sich dann damit - Beim rendern bindet sie die
Comments
-Komponente ein. Diese lädt nun (ebenfalls) perfetch
ihre Daten und stellt sich dar. - Die beiden Requests starten also nicht zeitgleich, und die Dauer, bis die Daten vollständig angezeigt werden können, setzt sich aus der Dauer der beiden Requests zusammen
- BlogPost lädt den Artikel (z.B.
- Kennt ihr das Problem? Meint ihr das ist ein Problem? Was könnte man dagegen tun 🤔
- Page
- static vs dynamic rendering
- Mit
Suspense
können wir grundsätzlich priorisieren, was uns wichtig(er) ist:- Die Seite wird erst dargestellt, wenn alle Daten geladen sind
- Sobald "irgendwelche" Daten (Artikel oder Kommentare) geladen wurden, diese Daten sofort anzeigen.
- Erst wenn die Artikel geladen wurden, diese darstellen (Falls Kommentare "schneller" sind, die Kommentare nicht vorab anzeigen)
- Die ersten beiden Beispiel durchgehen
- Wie können wir das dritte Umsetzen? 🤔
- Mit
Suspense
können wir grundsätzlich priorisieren, was uns wichtig(er) ist:- Die Seite wird erst dargestellt, wenn alle Daten geladen sind
- Sobald "irgendwelche" Daten (Artikel oder Kommentare) geladen wurden, diese Daten sofort anzeigen.
- Erst wenn die Artikel geladen wurden, diese darstellen (Falls Kommentare "schneller" sind, die Kommentare nicht vorab anzeigen)
- Für 1. setzen wir ein
Suspense
um die ganze Seite (z.B. inloading.tsx
) - Für 2. setzen wir jeweils ein
Suspense
um die Komponente, in der die Daten geladen werden - Für 3. starten wir beide Requests sofort parallel beim Rendern der Page-Komponente
- Diese wartet dann auf den Artikel-Request (
await articleRequestPromise
) - Das Promise für den Kommentare-Request wird an die
Comments
-Komponente gegeben - In der
Comments
-Komponente wird auf die Daten gewartet (await commentsRequestPromise
) - Um die
Comments
-Komponente herum wird eineSuspense
-Komponente gelegt.
- Diese wartet dann auf den Artikel-Request (
- Implementiere die Route zur Darstellung eines einzelnen BlogPosts (
/app/blog/(content)/post/[postId]/page.tsx
) - Lade in der Komponente die Daten des Artikels und dessen Kommentare
- Dazu kannst Du aus
backend-queries.ts
die FunktionengetBlogPost
undgetComments
verwenden
- Dazu kannst Du aus
- Zeige die geladenen Daten an (Du kannst die
Post
bzwCommentList
-Komponente verwenden) - Überlege dir, wo Du Suspense-Blöcke setzen möchtest, und füge sie dementsprechend ein
- Um die Ladezeiten künstlich zu verlangsamen, kannst Du die Konstanten
getBlogPostSlowdown
undgetCommentsSlowdown
inbackend-queries.ts
verwenden.
- Um die Ladezeiten künstlich zu verlangsamen, kannst Du die Konstanten
- Füge in
/app/blog/page.tsx
für jeden Post-Teaser einen Link auf die jeweilige BlogPost-Seite hinzu- Wenn du die
PostTeaser
-Komponente zur Darstellung verwendest, passiert das schon automatisch
- Wenn du die
- Lösung in
steps/30_suspense
- Eine "normale" React-Anwendung im Browser:
- State befindet sich oben
- Daten werden runtergereicht ("props")
- Callbacks werden runtergereicht
- Über Callbacks kann State-Veränderung ausgelöst werden
- Komponenten auf dem Server:
- Auf dem Server gibt es keinen State!
- ...und keine Interaktion
- Wir haben nur statischen Content (RSC)
- Wir haben Daten
- z.B. aus DB, Microservice, Filesystem...
- Bestimmte Teile müssen auf den Client
- alles was mit Interaktion zu tun hat
- z.B. Event-Handler
- alles was Browser-spezifisch ist
- z.B.
window
- z.B.
- alles was mit Interaktion zu tun hat
- Properties müssen Client-Server-Grenze überwinden
- Müssen serialisierbare Daten sein
- Keine (Callback-)Funktionen!
- Keine Props und State-Änderungen
- Stattdessen: Server-Requests
- z.B. URL ändern
- z.B. Search-Parameter
- Eine Client-Komponente
- wird mit
use client
gekennzeichnet - Alle Komponenten darunter werden dann als Client-Komponenten angenommen
- Ist auf Client-seite interaktiv (JavaScript-Code im Browser vorhanden)
- Muss eine neue Darstellung vom Server anfordern
- Beispiel, das die Search-Parameter in der URL verändert:
- wird mit
-
"use client"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; export default function OrderByButton({ orderBy, children }) { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); const handleClick = () => { const newParams = new URLSearchParams(searchParams); newParams.set("order_by", orderBy); // REQUEST NEW PAGE FROM SERVER: router.push(`${pathname}?${newParams.toString()}`); }; return ( <button onClick={handleClick}> {children} </button> ); }
- Auf der Server-Seite:
- Statt "klassischer" Props werden hier nun Search Params verwendet
- Page/Top-Level-Komponenten in Next.js können sich die Search-Parameter als Property
searchParams
übergeben lassen
-
type BlogListPageProps = { searchParams: { order_by?: OrderBy }; }; export default async function BlogListPage({ searchParams }: BlogListPageProps) { const orderBy = searchParams.order_by || "desc"; const response = await getBlogTeaserList(orderBy); return ( <div> <div> <OrderByButton orderBy={"desc"}>Desc</OrderByButton> <OrderByButton orderBy={"asc"}>Asc</OrderByButton> </div> {response.posts.map((p) => ( <PostTeaser key={p.id} post={p} /> ))} </div> ); }
- Alle Komponenten, die von einer Client-Komponente (
use client
) aus gerendert werden (direkt oder indirekt) sind Client Komponenten - Das heißt deren JavaScript-Code wird ebenfalls zum Client geschickt und dort ausgeführt
- Komponeten, die nicht explizit gekennzeichnet sind, können beide Rollen einnehmen
- Sie müssen dann aber auch Anforderungen beider Komponenten-Typen erfüllen:
- keine Verwendung von Server-APIs wie Datenbanken
- keine Verwendung von Browser-spezifischen APIs (z.B.
window
oder hooks)
- Wenn sie als Server Component verwent werden, wird ihr JavaScript-Code nicht zum Client geschickt
- Erst wenn sie (auch) als Client Component benötigt werden, wird ihr JS-Code gesendet
- Beispiel:
Post
-Komponente- In der
BlogPostPage
fungiert sie als RSC - Im
PostEditor
ist sie dann aber eine Client-Komponente - Demo!
- In der
-
Wird Code durch URL-Handling komplexer?
-
Wo ziehen wir Server/Client-Grenze?
- Button? Ganzes Formular?
-
Ganze Seite (oder Teile) werden neu gerendert
- Fertiges UI kommt dafür vom Server
- Das kann mehr Daten als bei (REST-)API-Call bedeuten!
-
Was fällt euch noch ein? 🤔
- Implementiere den Order By-Button oder einen Such-Filter
- Die Blog-Liste (
(content)/page.tsx
) verwendetgetBlogTeaserList
, um Daten aus dem Backend zu lesen - Du kannst
getBlogTeaserList
orderBy
und/oderfilter
übergeben - Implementiere also entweder einen Button zum Sortieren oder ein Text-Feld, mit dem man einen Filter (Suche) übergeben kann
- Zum Testen des Filters kannst Du z.B. den Ausdruck
redux
verwenden, dann müssten zwei Artikel zurückkommen
- Zum Testen des Filters kannst Du z.B. den Ausdruck
- In jedem Fall musst Du eine Client-Komponente erzeugen, die in der Lage ist, die Search-Parameter der Anwendung zu verändern
- Die Search-Parameter verwendest Du dann in
(content)/page.tsx
, um damit zu ermitteln, wie sortiert/nach was gesucht werden soll.- Den aktuell gerenderten Pfad (URL ohne Search-Parameter) bekommst Du mit dem Next.js Hook (
usePathname
)[https://nextjs.org/docs/app/api-reference/functions/use-pathname] - An die aktuellen Search-Parameter kommst Du mit dem Next.js Hook
useSearchParams
- Einen Seitenwechsel kannst mit
router.push()
machen, wobei durouter
mit dem Next.js HookuseRouter()
bekommst.
- Den aktuell gerenderten Pfad (URL ohne Search-Parameter) bekommst Du mit dem Next.js Hook (
- Deine Client-Komponte kannst Du einfach in
(content)/page.tsx
einbinden - Analysier doch mal mit Hilfe von
console.log
bzw. der Ausgabe auf der Konsole desbackend
-Prozesses, wann neu gerendert wird
- : `OrderByButton` mit Transition
- Mit dem
useTransition
-Hook von React (18) können Updates priorisiert werden - Dazu wird eine Funktion angegeben, in der eine "Transition" beschrieben ist (z.B. durch das Setzen eines States)
- Wenn React die Komponente daraufhin neu rendert, und eine weitere/andere State-Änderung durchgeführt wird, bricht React das rendern ab (und startes es ggf. später neu)
- Mit
useTransition
kann also ausgedrückt werden: dieses Rendern ist nicht so "wichtig" (nicht so "dringend") - Mit Client-seitigem React kann auf diese Weise zum Beispiel sichergestellt werden, dass Updates, die durch Benutzer-Eingaben entstehen, nicht vom Rendern eines Suchergebnisses unterbrochen werden
- Hier wäre das Aktualisieren des Suchergebnisses weniger "dringend", als die Darstellung der aktualisierten Eingabe
- Der
useTransition
-Hook liefert zwei Parameter zurück:const [isPending, startTransition] = useTransition()
- Mit
startTransition
kann die Transition gestartet werden (Funktion übergeben) isPending
liefert zurück, ob die Transition gerade läuft
- Wenn man einen von einer Seite auf eine andere Seite mit dem Next.js Router durchführt, kann man mit
useTransition
auf der Ursprungsseite bleiben, bis die Ziel-Seite fertig gerendert ist- Die Ziel-Seite wird dann in Hintergrund gerendet, und solange ist
isPending
true
- Die Ziel-Seite wird dann in Hintergrund gerendet, und solange ist
-
export function OrderByButton() { const router = useRouter(); const [isPending, startTransition] = useTransition(); const handleClick = () => { startTransition( () => router.push("/...")); } return isPending ? <button>Sorting...</button> : <button onClick={handleClick}>Order by date</button>; }
- Next.js implementiert ein sehr aggressives Caching auf vielen Ebenen
- Gecached werden z.B. Komponenten, aber auch fetch-Requests
- Wenn du
fetch
in deinem Code verwendest, werden die GET-Requests von Next.js gecached!
- Wenn du
- Das kann man alles ausschalten, aber es ist am Anfang gewöhnungsbedürftig
- Deswegen auch das
dev:clean
-Script in derpackage.json
- Deswegen auch das
- Meiner Erfahrung nach ist das nicht trivial zu verstehen und scheint auch noch Bugs zu haben
- Es gibt eine ausführlichen Dokumentation, welche Caches es gibt und wie die jeweils funktionieren
- Darin enthalten ist auch eine Matrix, aus der hervorgeht, welche Next.js Funktionen Auswirkungen auf den Cache haben
- 👨🏫 Morgen machen wir eine Klassenarbeit, dann frage ich die Matrix ab 😈
- Man kann die einzelen Cachings ausschalten, bzw. revalidieren lassen
- Bei
fetch
-Requests kann man ein Next.js-proprietäres Property angeben: -
fetch("https://blog-api.de", { // Next-proprietäre Erweiterung der fetch-API: next: { // Nach einer Minute Cache verwerfen revalidate: 60 } });
- Einem
fetch
-Request können außerdem Tags zugeordnet werden - Diese kann man verwenden, um den Cache-Eintrag per API als veraltet zu markieren
-
const r = await fetch( `http://localhost:7002/posts`, { next: { tags: ["teaser"], }, }, );
-
// Invalidieren des Caches: import { revalidateTag } from "next/cache"; revalidateTag("teaser");
- Alternativ geht das auch mit Pfaden (
revalidatePath
), aber das scheint noch Buggy zu sein. - Wie lange eine statische Route gecached werden soll, kann mit
revalidate
festgelegt werden- Davon unbenommen ist aber das fetch-Caching (s.o.)
- Wichtig! Das funktioniert nur in serverseitigem Code!
- Das Schreiben von Daten kann grundsätzlich so wie bislang auch umgesetzt werden:
- Zum Beispiel in dem ein
form
übertragen wird - Oder, wie in React üblich, ein REST-Aufruf an den Server mit
fetch
gemacht wird
- Zum Beispiel in dem ein
- Aber!
- Nach dem Verändern von Daten muss die UI aktualisiert werden
- Mangels State auf dem Client geht das aber nicht wie bislang
- Der Server muss nach Datenänderungen aktualisierte UI liefern
- Möglichkeit 1:
- Client-seitig kann man mit
Router.refresh
die aktuelle Route - unabhängig vom Cache - aktualsieren lassen. Next.js rendert die Route dann auf dem Server neu und liefert aktualisierte UI
- Client-seitig kann man mit
- Möglichkeit 2:
- Invalidieren des Caches mit
revalidatePath
bzw.revalidateTags
- Invalidieren des Caches mit
- Möglichkeit 3:
noStore()
verwenden, damit wird eine Route vom Caching ausgenommen- Das scheint aber aber nur zu funktionieren, wenn eine Route erneut vom Browser abgefragt wird. Wenn eine Route bereits im Client-Cache ist, wird diese ausgeliefert
- Server Actions sind (asynchrone) Funktionen, die auf dem Server aufgerufen und aus einer (Client-)Komponente aufgerufen werden können
- Eine Art remote-procedure Call
- React bzw. Next.js stellt für jede Server-Action-Funktion transparent einen HTTP-Endpunkt zur Verfügung
- Die Funktion kann beliebige Parameter entgegen nehmen und zurückliefern
- Einschränkung: Werte müssen serialiserbar sein
-
export async function savePost(title: string, body: string) { try { await db.save(title, body); } catch (e) { return { success: false, error: e.toString() }; } return { success: true }; }
- Der Aufruf erfolgt aus der Komponente wie bei einer normalen Funktion
-
function PostEditor() { const onSaveClick = async () => { // SERVER REQUEST ! savePost(title, body) }; // ... }
- Server-Actions können als Inline-Funktion direkt innerhalb einer Server Komponente implementiert werden
- Dann muss die Funktion mit der Direktive
"use server"
gekenntzeichnet werden - Die Funktion kann dann als Property an eine Client-Komponente weitergegeben werden
-
// RSC export default async function PostEditorPage() { async function savePost(title: string, body: string) { "use server"; // ... } return <PostEditor savePost={savePost} /> }
-
"use client" type Props = { savePost: (title: string, body: string) => Promise<Result> } export function PostEditor({savePost}: Props) { const handleSave = async () => { // SERVER REQUEST! await savePost(title, body); router.refresh(); } }
- Dann muss die Funktion mit der Direktive
- In Client Komponenten können keine Server Actions implementiert werden. Alternativ können sie aber in einer eigenen Datei implementiert werden.
- Dann muss diese Datei mit
"use server"
gekennzeichnet weden. - In dem Fall werden alle exportierten Funktionen in der Datei als Server Actions interpretiert (und entsprechende Endpunkte zur Verfügung gestellt)
-
// blog-server.actions.ts "use server" export async function savePost(title: string, body: string) { /* ... */ } export async function deletePost(postId: string) { /* ... */ }
- Eine Client Komponente darf die Server Actions dann importieren und verwenden
-
"use client"; import { savePost } from "./blog-server.actions.ts" export function PostEditor() { const onSaveClick = async () => { // SERVER REQUEST ! await savePost(title, body); router.refresh(); }; // ... }
-
Schöne neue Welt? 🤔
- Vervollständige die PostEditor-Komponente!
- Füge eine neue Route (
/add
) hinzu. In der zugehörigen Komponente (page.tsx
) gibst du einfach die (fast fertige)PostEditor
-Komponente zurück - In der Blog List musst Du einen
Link
auf/add
hinzufügen, dass man den PostEditor über die Oberfläche öffnen kann. - In der
PostEditor
-Komponente musst du das Speichern implementieren, wenn auf denSave
-Button gedrückt wird - Zum speichern muss deine Server Action aufgerufen werden. Nach dem Aufruf der Server Actions kannst Du mit
router.push("/blog")
zur Übersichtsseite wechseln. Dein neuer Post sollte hier angezeigt werden.- Den
router
bekommst Du mit demuseRouter
-Hook von Next.js
- Den
- In der Datei
server-actions.ts
musst Du die zugehörige Server Action-Funktion implementieren:- Diese muss
title
undbody
aus dem Formular entgegen nehmen - Den Post kannst Du in der Server Action mit der fertigen Funktion
savePostToBackend
speichern - Wenn die Daten gespeichert wurden, musst Du Next.js anweisen, den Cache neu zu machen. Dazu verwende bitte:
revalidateTag("teaser")
in der Server Action
- Diese muss
- Mit Next.js (bzw. künftigen React APIs) soll es möglich sein, Formulare so zu bauen, dass man sie auch ausfüllen und absenden kann, wenn kein JavaScript im Browser läuft (Progressive enhancement)
- Wofür könnte das relevant sein? 🤔
- Welche Einschränkungen könnte es dabei geben? 🤔
- Um Formulare ohne JavaScript absenden zu können, muss es genauso aussehen, als wenn man ein Formular in einer statischen HTML-Seite beschreibt:
- dazu muss ein HTML
form
-Element mit einemaction
-Attribute verwendet werden - Damit das Formular abgesendet werden kann, muss es einen
submit
-Button geben
- dazu muss ein HTML
- In "regulärem" HTML wird der Form-Inhalt dann an den in der
action
angegebenen Endpunkt geschickt - Der Payload ist ein
FormData
-Objekt - Mit Next.js (bzw. React) können wir als
action
eine Server-Action-Funktion angeben - Die angegebene Server Action muss als Parameter ein
FormData
-Objekt entgegennehmen -
export function PostEditor() { async function saveForm(data: FormData) { "use server" // AUF DEM SERVER: Formular speichern const title = data.get("title"); // ... } return <form action={saveForm}> <input name="title" /> <input name="body" /> </form> }
- Zur Arbeit mit Formularen, die mittels progressive enhancement umgesetzt werden sollen, gibt es auch noch eine Reihe neuer Hooks, die dafür sorgen, dass das Formular (eingeschränkt) ohne JavaScript funktioniert.
- Ist JavaScript aktiv und der Code geladen, werden dann weitere Features angeboten
- useFormState: Hält die Daten eines Formulars (ähnlich wie lokaler State), funktioniert aber ohne JavaScript
- useFormStatus: Liefert einen Status zurück, ob ein Formular gerade submitted wird (z.B. um den Speichern-Button zu disablen, während das Speichern läuft)
- useOptimistic: Eine Art lokaler State, dem man vorrübergehend einen "angenommenen" Wert übergeben kann, solange ein asynchrone Operation läuft. Man kann damit schon das (erwartet) Ergebnis der asynchronen Operation "simulieren", um dem Benutzer schneller Feedback zu geben.