- Letztes Update von React: Juni (!) 2022 (!!)
- ...es gibt aber mittlerweile einen "Canary Channel", auf dem Updates veröffentlicht werden, die für Bibliotheksanbieter gedacht sind
- auf diese Weise können die schon neue React-Features integrieren, bevor diese dann in einer stabilen Version erscheinen
- Auf diesem Kanal ist recht viel los, insb. wegen den neuen Server-Features
- Ein Hook mit dem man eindeutige Ids generieren kann
- Zum Beispiel für
aria-labelledbyetc in Komponentenbibliotheken - Das kann nützlich sein, wenn man SSR macht, und beim rendern aus Server- und Clientseite dieselbe "eindeutige" Id generiert werden soll
- und deswegen z.B. eine zufällige uuid nicht funktioniert
-
export function Input({label}) { const labelId = useId(); return <div> <label id={labelId}>{label}</label> <input htmlFor={id} /> </div> }
- Voraussetzung für Suspense
- Kann Rendern abbrechen
- Braucht man Anwendungsfälle für...
- Haben wir schon bei Next.js gesehen und sehen wir auch später nochmal
- Habe bislang keine guten Anwendungsfälle daür gefunden
- Als Debounce/Throttling in der Beispiel-Anwendung nicht gut geeignet
use-Hook undcacheAPI- Suspense mit TanStack Query
- loader und actions mit dem React Router
- Auf dem Server können wir mittlerweile asynchrone Komponenten schreiben (RSC)
- Das geht auf dem Client nicht
- Um das arbeiten mit Promises zu vereinfachen, gibt es künftig:
- Mit dem
use-Hook kann man auf ein Promise warten. - Darum kann eine Suspene-Komponente mit einem Platzhalter liegen
-
async function loadBlogPost() { /* ... */ } function BlogPost() { const post = use(loadBlogPost()); return <>...</>; } function BlogPostPage() { return <Suspense fallback={<h1>Posts loading...</h1>}> <BlogPost /> </Suspense> }
- Achtung!!: Der Aufruf von
loadBlogPosterzeugt bei jedem Rendern ein neues Promise!- Damit kann man in eine Endlosschleife kommen
- Dafür wahr wohl mal der
cachegedacht, aber der ist jetzt nur noch für die Serverseite
- Der
use-Hook darf im Gegensatz zu anderen Hooks überall in einer Komponente verwendet werden- auch in if-Blöcken, Schleifen etc.
- Man kann mit dem
use-Hook auf einen Context zugreifen. Das funktioniert genau wieuseContext, nur dass der Hook überall verwendet werden kann: -
function PostEditor() { const handleSave = async () => { await savePost(); use(RouterContext).push(); } }
- Grundlagen: "Klassische" TanStack Query API
- Queries mit Suspense
- Verwendung mit
useTransition
- Beispiel-Anwendung
- Architektur
- Klassische Single-Page-Anwendung: Frontend ist Frontend, Backend ist Backend (mit REST API)...
- Es gibt diverse Parameter, die einstellen, was wann passieren soll
- Auch das ist nicht immer einfach zu verstehen und zu behalten
- Tipp: In jedem Fall die Default-Options lesen und immer wieder parat halten!
- Insbesondere die
refetch-Einstellungen können sehr verwirrend und überraschend sein refetchOnWindowFocusmache ich z.B. mindestens während der Entwicklung aus
- Insbesondere die
- Zentrales Konfigurationsobject:
QueryClient - React-unabhängig
- Wird beim Starten der Anwendung initialisiert
- Oft reichen Default-Einstellung
- Es können aber z.B. globale Refetch-Policies eingestellt werden
- Das Objekt wird per QueryClientProvider in die Anwendung gereicht
-
const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false } } }); root.render( <QueryClientProvider client={queryClient}> <App /> </QueryClientProvider> );
- Queries werden mit dem useQuery-Hook ausgeführt
- Der Hook erwartet ein Konfigurationsobjekt
queryKey: Array mit Query Keys (zur Interaktion mit dem Cache)queryFn: Funktion zum Laden der Daten- Weitere Konfigurationen (optional)
-
import { useQuery } from "react-query"; import { loadBlogPosts } from "./blog-api"; function BlogListPage() { const result = useQuery({queryKey: ['posts'], queryFn: loadBlogPosts}); // ... }
useQueryliefert ein Objekt zurück:isLoading: Der Query lädt noch (und es sind keine Daten im Cache)isSuccess: Daten sind geladenisError: Es ist ein Fehler aufgetretendataenthält die geladenen Datenerror: Fehlerobjekt aus der Query-Funktion- Weitere siehe Doku
-
function BlogPage({blogPostId}) { const result = useQuery(/* ... */); if (result.isLoading) { return <h1>Loading!</h1> } if (result.isError) { // result.error ist hier gesetzt return "Error: " + result.error; } if (result.isSuccess) { // data ist jetzt hier gesetzt return <BlogPost post={data} /> } }
- Mit den Query Keys wird ein Ergebnis im Cache gespeichert
- Ein Query Key besteht aus einem Array von Werten
- Üblicherweise ist es ein Name (z.B. "posts") und dann ggf. weitere Parameter, zum Beispiel die Id eines Posts ("P1")
oder die Sortierreihenfolge
- Also alle Daten, die den Query exakt beschreiben
-
import { useQuery } from "react-query"; import { loadBlogPosts } from "./blog-api"; function BlogPage({blogPostId}) { // Für jeden Aufruf mit einer neuen blogPostId // wird das Ergebnis separat in den Cache gelegt const result = useQuery({ queryKey: ['blogPost', blogPostId], queryFn: () => loadPost(blogPostId) }); // ... }
- Wenn ein Query mit denselben Query Keys in mehr als einer Komponente ausgeführt wird...
- ...stellt TanStack Query sicher, dass der Query nur einmal ausgeführt wird
- ...wenn sich das Ergebnis ändert, werden alle Komponenten, die den Query verwenden, automatisch aus dem Cache aktualisiert
useQueryerwartet eine Query-Function, die den eigentlichen Request ausführt- Die Signatur ist fast beliebig, die Funktion muss aber ein Promise zurückliefern:
- Wenn die Daten erfolgreich geladen wurden, muss das Promise mit den Daten "aufgelöst" werden
- Wenn es einen Fehler gab, muss die Funktion einen Fehler werfen
-
// async function gibt IMMER ein Promise zurück export async function loadBlogPost(postId) { const response = await fetch("http://localhost:7000/posts" + postId); if (!response.ok) { throw new Error("Could not load blog post: " + response.status); } return response.json(); }
- Queries werden oft in eigenen Hooks zusammengefasst. Dann braucht man Query-Key und -Funktion und weitere Einstellungen nicht jedesmal neu anzugeben
-
function useBlogPostQuery(blogPostId: string) { return useQuery({ queryKey: ['blogPost', blogPostId], queryFn: () => loadPost(blogPostId) }); } function BlogPostPage() { const result = useBlogPostQuery("P1"); // ... }
- Da Query Keys oft an mehr als einer Stelle gebraucht werden (z.B. invalidieren von Queries, dazu später mehr), legt man diese so ab, dass man Zugriff darauf hat. Man kann dafür auch eine "Factory-Funktion" bauen
-
export const blogPostPageQueryKey = (postId: string) => (['blogPost', blogPostId]); function useBlogPostQuery(blogPostId: string) { return useQuery({ queryKey: blogPostPageQueryKey(postId), queryFn: () => loadPost(blogPostId) }); }
- Die Query-Funktionen würde ich - sofern sie einigermaßen trivial sind - direkt in dem Custom-Hook implementieren (anders als in den Beispielen bislang sehen)
-
function useBlogPostQuery(blogPostId: string) { return useQuery({ queryKey: blogPostPageQueryKey(postId), queryFn: async () => { const r = fetch("..."); // ... } }); }
- Validierung der Server-Antworten (und evtl. auch der Requests) mit Zod.
- wir arbeiten jetzt in
spa/spa-workspace - hierin bitte Packages installieren und Anwendung starten
pnpm install,pnpm dev- Die Anwendung läuft auf http://localhost:3200
- Auf der Blog-Übersichtsseite fehlen die Daten 😱
- Implementiere bitte die
BlogListPage-Komponente. Der Rumpf der Komponente ist schon inBlogListPage.tsxvorhanden. Es fehlt "nur" der Code zum Laden der Daten... - 👮 CoPilot ist verboten!
- Als Query-Funktion kannst Du
getBlogTeaserListausbackend-queries.tsangeben - Du kannst
useQueryoderuseSuspenseQueryverwenden- Bei
useSuspenseQueryan denSuspense-Boundary danken!
- Bei
- Zeige eine Warte-Meldung an, während die Daten geladen werden
- Zum künstlichen Verzögern der Daten
getBlogTeaserListSlowdowninbackend-queries.tssetzen
- Zum künstlichen Verzögern der Daten
- Mutations werden verwendet, um Daten zu schreiben
- Mutations haben keinen Cache-Key und werden auch nicht automatisch ausgeführt
- Die Mutation-Funktion entspricht der Query-Function (nur dass sie Daten schreibt und nicht liest)
- Auch der
useMutation-Hook liefert Informationen über den Zustand der Mutation zurück - Außerdem wird eine Funktion (
mutate) zurückgeliefert, die die Mutation ausführt - Übergeben wird der Funktion die zu schreibenden Daten
-
import { useMutation } from "react-query"; import { savePost } from "./blog-api"; function PostEditorPage() { const mutation = useMutation({ mutationFn: savePost, onSuccess() { // optional: wird aufgerufen, wenn die Mutation erfolgreich war // ... } }); if (mutation.status === "error") { return <h1>Error!</h1>; } if (mutation.status === "loading") { return <h1>Saving, please wait!</h1>; } return <PostEditor onSavePost={mutation.mutate} />; }
- Die
mutationFnkann genau einen Parameter annehmen (oder keinen) - Den Wert für diesen Parameter gibst Du beim Ausführen der Mutation an die
mutate-Funktion- (diese ruft die
mutationFndann ihrerseits mit diesem Parameter auf)
- (diese ruft die
- Damit kannst du z.B. die zu speichernden Daten übergeben
- Wenn Du mehrere Informationen übergeben willst, musst Du ein Objekt übergeben
-
function PostEditor() { const mutation = useMutation({ mutationFn: ({ title, body }: NewPost) => { return addPost(title, body); } // ... }); const handleSave = () => { mutation.mutate({ title: "...", body: "..." }) } }
- Neben der
mutate-Funktion gibt liefertuseMutationeinemutateAsync-Funktion zurück - Diese liefert ein Promise zurück, das mit dem Ergebnis der Mutation aufgelöst wird
- Das kannst Du in einer Komponente z.B. nutzen, um nach der erfolgreichen Mutation eine weitere Aktion auszulösen
-
function PostEditor() { const mutation = useMutation({ /* ... */ }); const handleSave = async () => { const result = await mutation.mutateAsync(post, title); if (result.status === "success") { router.push("/blog"); } } }
- Alle Query-Ergebnisse von
useQuerywerden automatisch gecached - Alle Komponenten werden aktualisiert, wenn sich der Cache aktualisiert
- Per Default werden Queries automatisch neu ausgeführt:
- Komponente wird (neu) gemounted
- Browser-Fenster bekommt den Focus
- Nachdem das Netzwerk offline war
- Queries können per API manuell erneut ausgeführt werden
- Das kann zum Beispiel nach einer Mutation sinnvoll sein, um die geänderten/gespeicherten Daten im Cache zu aktualisieren
- Dazu wird die Funktion
invalidateQueriesoderrefetchQueriesvomQueryClientverwendet - Übergeben werden die Query Keys, deren Queries erneut ausgeführt werden sollen
-
import { useMutation, useQueryClient } from "react-query"; import { savePost } from "./blog-api"; function PostEditorPage() { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: savePost, onSuccess() { // PostPage-Query als "veraltet" markieren, // beim nächsten Rendern einer Komponente, die darauf zugreift, // werden die Daten neu gelesen queryClient.invalidateQueries(['posts']); // Queries unmittelbar neu ausführen queryClient.refetchQueries(['posts']); } }); // ... }
- Ein einzelnes BlogPost kann im Cache verbleiben, da es sich in unserer App nicht ändert/nicht ändern kann
- Mit den
refetch-Optionen kann die automatische Aktualisierung ausgeschaltet werden -
function PostPage() { // ... const result = useQuery({queryKey: ["blogPost", postId], queryFn: () => loadBlogPost(postId!)}, { refetchOnMount: false, refetchOnWindowFocus: false }); // ... }
- Das von
useQueryzurückgeliefert Objekt enthält auch einerefetch-Funktion um einen Query manuell neu auszuführen -
function PostListPage() { const result = useQuery({queryKey: ['posts'], queryFn: readPosts}, { // nicht automatisch aktualisieren refetchOnMount: false, refetchOnWindowFocus: false }) // ... result.status === loading, status === error ... return <div> <button onClick={refetch}>Reload Posts</button> <PostList posts={data} /> </div> }
- Vervollständige die
PostEditor-Komponente - In der
handleSave-Funktion soll eine Mutation ausgeführt werden, die du implementieren musst- Die Mutation kann als
queryFndie FunktionaddPostausserver-actions.tsverwenden, um das Post auf dem Server zu speichern - Wenn die Mutation fehlschlägt, sollte im PostEditor eine Fehlermeldung angezeigt werden
- Das kannst Du testen, in dem du einen
titleeingibst bzw. speicherst, der weniger als fünf Zeichen lang ist. - Wenn der Benutzer nach dem fehlerhaften Speichern Änderungen macht (Eingabe in eins der Eingabefelder) soll die Fehlermeldung wieder verschwinden
- Das kannst Du testen, in dem du einen
- Wenn die Mutation erfolgreich war, soll zur
/blog-Übersichtsseite navigiert werden (dafür kannst DuuseNaviagatevom React Router verwenden) - Kannst Du einen Custom Hook für die Mutation schreiben (
useSavePostMutation) ?- Die Navigation soll nicht Bestandteil des Hooks sein
- Der neue, gespeicherte, Blog Post soll auf der
/blog-Übersichtsseite natürlich dann auch sichtbar sein...
- Die Mutation kann als
- Seit Version 5 bietet TanStack Query Support für Suspense
- Damit können wir - wie in Next.js gesehen - per
Suspense-Komponente festlegen, an welcher Stelle in der Komponentenhierarchie auf ausstehende Daten gewartet werden soll - Anstatt
useQueryverwenden wiruseSuspenseQuery - Der Hook funktioniert sehr ähnlich zu
useQuery, aber:- das Ergebnis enthält immer Daten (
data), denn wenn es noch keine Daten gibt, wird solange die Suspenseplaceholder-Komponente angezeigt
- das Ergebnis enthält immer Daten (
-
function BlogListPage() { return <React.Suspense fallback={<h1>Posts loading...</h1>}> <BlogList /> </React.Suspense> } function BlogListPage() { const { data } = useSuspenseQuery({ queryFn: () => { /* ... */ }, queryKey: ["blog-list"] }); // data ist hier in jedem Fall definiert return <PostList posts={data} /> }
- Mit den default
refetch-Einstellungen wird ein Query erneut ausgeführt, wenn eine Komponente gemounted wird, oder das Fenster den Focus bekommt - Wenn der Query zu dem Zeitpunkt bereits einmal ausgeführt wurde, und sich Daten dafür im Cache befinden, werden zunächst diese alten Daten angezeigt
- Der
placeholderwird dann nicht angezeigt, da aus Supsene-Sicht auf keine Daten gewartet wird
- Der
useQueryunduseSuspenseQuery-Hook liefern das PropertyisFetchingzurück, mit dem Du erkennen kannst, ob im Hintergrund gerade deine Daten aktualisiert werden- Fehler werden über einen Error Boundary behandelt
- Damit kannst Du z.B. einen Loading Spinner oder einen anderen Hinweis anzeigen, wenn die Daten neu geladen werden
-
function BlogListPage() { const { data, isFetching } = useSuspenseQuery({ queryFn: () => { /* ... */ }, queryKey: ["blog-list"] }); // data ist auch hier in jedem Fall definiert return <div> {isFetching && "Updating data..."} <PostList posts={data} /> </div> }
- Stelle die PostListPage auf
useSuspenseQueryum - Du musst außerdem ein
Suspense-Boundary festlegen. - Wenn Du den Aufruf des Queries verzögerst (
getBlogTeaserListSlowdowninbackend-queries.ts) und zwischen Blog Post-Ansicht und Übersichtsseite wechselt, was passiert dann? - Stelle die beiden
useQuery-Aufrufe inBlogPostPageundCommentListum, die ein einzelnes BlogPost bzw. dessen Kommentare darstellen- Für die beiden Queries ist in
BlogPostPageRouteschon einSuspense-Boundary definiert - Diese Komponente (
BlogPostPageRoute) ist in der Router-Konfiguration für/blog/:postId]gemappt
- Für die beiden Queries ist in
- Was passiert, wenn Du dann um den Aufruf der
CommentList-Komponente (k)einSuspenselegst? - Zum besseren Testen kannst Du die Aufrufe des Backends in der Datei
backend-queries.tsverzögern (s. Konstanten am Anfang der Datei):getBlogTeaserListSlowdown: Blog ÜbersichtsseitegetBlogPostSlowdown: lesen eines Einzelnen Blog PostsgetCommentsSlowdown: Lesen der Kommentare zu einem Blogpost- Achtung: die Angaben sind etwas verschieden, aber du findest Beispiel in
backend-queries.ts
- In der
BlogPostPagehaben wir einen "Request-Wasserfall":- die Artikel werden gelesen und
- erst danach wird die
CommentListgerendert und darin werden die Kommentare gelesen
- Um die Darstellung zu beschleunigen, wäre es hilfreich, wenn beide Queries zeitgleich starten würden
- Zum "Vorladen" von Daten können wir die Funktion
QueryClient.ensureQueryDataverwenden - Diese erwartet dieselben Angaben wie
use(Suspense)Query, nämlich u.a.queryKeyundqueryFn - Wenn sie ausgeführt wird, und für den angegeben
queryKeynoch keine Daten im Cache sind, lädt die Funktion die Daten im Hintergrund - Damit können wir in der
BlogListPageschon das Laden der Daten triggern. Wenn dann dieCommentListgerendert wird, sind die Daten evtl. schon im Cache, aber zumindest läuft der Request schon -
function BlogPostPage({ postId }: BlogPostPageProps) { // start early fetching of comments... const queryClient = useQueryClient(); queryClient.ensureQueryData({ queryKey: ["blogpost", postId, "comments"], queryFn: () => getComments(postId), }); const { data: post } = useSuspenseQuery({ queryKey: ["blogpost", postId], queryFn: () => getBlogPost(postId), }); return ( <div className={"space-y-4"}> <Post post={post} /> <Suspense fallback={<LoadingIndicator>Comments loading...</LoadingIndicator>} > <CommentList postId={postId} /> </Suspense> </div> ); }
- Bei der Arbeit mit Mutations ändert sich gegenüber der "konventionellen" Variante zunächst nichts, wir nutzen weiterhin den
useMutation-Hook. - Allerdings können wir sicherstellen, dass nach der Mutation solange gewartet wird, bis der Cache aktualisiert wurde
- Damit können wir erreichen, dass wir auf einer Seite (in dem Fall z.B.
PostEditor) bleiben, bis die Ziel-Seite (Blog Übersichtsseite) vollständig neu gerendert wurde - Aus Benutzer-Sicht sieht es dann so aus als ob es ein Vorgang wäre und nicht zwei (1. Speichern und 2. Neuladen der Posts)
- Dazu musst Du in der
onSuccess-Methode einer Mutation so lange warten, bis auch das Aktualisieren des Caches abgeschlossen ist- Aus Sicht des Aufrufers der Mutatio mit
mutateAsyncsieht es dann auch wie eine Operation aus
- Aus Sicht des Aufrufers der Mutatio mit
- wenn Du den Cache der Ziel-Seite (bzw. dessen Query) mit
invalidateQueriesnur invalidierst oder aus dem Cache entfernst, werden die Daten nicht neu geladen, sondern erst, wenn der Query wieder "benötigt" wird- Das ist dann erst auf der Ziel-Seite der Fall, so dass hier die Ziel.Seite sofort aufgerufen und der
placeholderangezeigt wird (oderisRefetching: trueist) - wenn Du stattdessen in
onSuccessawait refetchQueries()verwendest, wird der Query sofort ausgeführt undonSuccesskehrt erst zurück, wenn die Daten geladen und der Cache aktualisiert wurde
- Das ist dann erst auf der Ziel-Seite der Fall, so dass hier die Ziel.Seite sofort aufgerufen und der
-
export default function PostEditor() { const queryClient = useQueryClient(); const addPostMutation = useMutation({ mutationFn: ({ title, body }) => { /* ... */ }, onSuccess: async (data) => { // await ist wichtig! await queryClient.refetchQueries({ queryKey: ["blog-list"], }); }, }); // mutateAsync wie gesehen }
- Identisches Setup wie TanStack Query: Frontend ist Frontend, Backend ist Backend
- Seit Version 6 vom React Router gibt es in React Router das Konzept eines DataRouters
- Diese kombinieren das Routing mit dem Data-Fetching (und Data-Mutating)
- Das ganze ist aus Remix hervorgegangen bzw. geht auch wieder dorthin zurück
- (Für Remix 3 ist ein Migrationspfad von SPA mit React Router nach Fullstack Remix geplant)
- Mitterweile ist der herkömmliche BrowserRouter auch ein DataRouter, d.h. unterstüzt das Laden von Daten
- Traditionell werden im React Router URL (Segmente) auf Komponenten abgebildet.
-
const router = createBrowserRouter([ { path: "/", element: <RootLayout />, children: [ { index: true, element: <LandingPage /> }, { path: "blog", children: [ { path: "add", element: <AddRouteLayout />, children: [{ index: true, element: <PostEditor /> }], }, { element: <BlogContentLayout />, children: [ { path: "post/:postId", element: <BlogPostPageRoute /> }, ], }, ], }, ], }, ]);
- Zusätzlich zu den "normalen" Routing-Angaben kann man pro Route eine
loader-Funktion angeben - Diese ist dafür zuständig, die Daten für eine Route zu laden
- Erst wenn die Funktion die Daten geladen (und an den React Router zurückgegeben) hat, wird die Route dann gerendert
- Die geladenen Daten kann man in den Komponenten mit
useLoaderDataabfragen -
const router = createBrowserRouter( /* ... */ { path: "post/:postId", element: <BlogPostPageRoute />, loader: blogPageLoader, } ); async function blogPageLoader({params}) { const post = await fetch(`http://localhost:7020/post/${params.postId}`); // ... return post ; } function BlogPostPage() { const post = useLoaderData(); // render Post }
- Für die
loader-Funktionen gibt es den TypeScript-TypenLoaderFunction. Dieser beschreibt, wie Signatur und Rückgabe-Wert aussehen müssen. - Als Parameter werden euch an die loader-Funktion übergeben:
params: die URL-Parameter bei variablen Segmenten (postId)request: Eine Instanz desfetchrequest-Objektes. Über diesen bekommt ihr u.a. Zugriff auf die aktuelle URL, um z.B. die SearchParameter auszulesen- Bin nicht ganz sicher, was ich davon halten soll
-
export const blogPageLoader: LoaderFunction = ({ params, request }) => { const postId = params.postId const url = new URL(request.url); const includeComments = url.searchParams.get("include_comments"); return fetch( /* ... */) }
- Innerhalb der
loader-Funktion kannst Du denQueryClientvon TanStack Query verwenden, um darüber deine Daten zu laden - Dann kann deine Anwendung auch von den Cache-Features von TanStack Query profitieren
- Da eine
loader-Funktion kein React Hook ist, kannst Du nicht mituseQueryoderuseQueryClientarbeiten - Stattdessen stellst Du die
QueryClient-Instanz über einen Modul export zur Verfügung: -
const queryClient = new QueryClient(/* ... */); export {queryClient};
- In deinen
loader-Funktionen kannst Du auf denQueryClientzugreifen und mitfetchQuerydie Daten laden fetchQueryfunktioniert im Grunde wieuseQuerynur "ohne Komponente"-
import {queryClient} from "./query-client"; export function blogListPageLoader() { return queryClient.fetchQuery({ queryKey: ["blog-list"], queryFn: () => fetch( /* ... */ ), }) }
- Wir arbeiten im Verzeichnis
datarouter/datarouter-workspace - Dieses Verzeichnis in Editor/IDE öffnen
- Darin Packages installieren und Vite starten:
pnpm installpnpm dev
- Die Anwendung läuft auf http://localhost:3300
- In der
PostListPagefehlt die loader-Funktion! - Bitte implementiere die
loader-Funktion (kannst Du direkt inBlogListPage.tsxmachen)- Verwende den
QueryClientvon TanStack Query - Zum Laden der Daten mit der
queryFn-Funktion kannst DugetBlogTeaserListausbacken-queries.tsverwenden
- Verwende den
- Gib die
loader-Funktion in der Routen-Beschreibung inmain.tsxan - Zeige die mit der
loader-Funktion gelesenen Blog-Psts in derBlogListPage-Komponente an - Hinweis: in
shared/api/types.tsfindest Du TypeScript-Typen für die Objekte, die über die API gelesen werden (z.B.BlogPostTeaser,BlogPostundComment) - Lösung:
steps/10-loader-function
- Das Rendern einer Route wird verzögert, bis alle Daten geladen wurden (bzw. das Promise der loader-Funktion aufgelöst wurde)
- Wir können aber auch mit dem React Router die
Suspense-Komponente von React verwenden, um Inhalte zu priorisieren - Dazu können Daten innerhalb der
loader-Funktion als "defered" (verzögert, bzw. "kommen später") gekennzeichnet werden. - Die
loader-Funktion gibt dann ein Objekt zurück, das mitdefererstellt wird. Darin enhalten können sein:- geladene Daten (auf die gewartet werden soll)
- (ausstehende) Promises für Daten (auf die nicht gewartet werden soll)
-
export const blogPageLoader: LoaderFunction = async ({ params }) => { const { postId } = params; return defer({ // auf BlogPost wird beim Rendern gewartet blogPost: await queryClient.fetchQuery( /* ... */ ), // Auf die Kommentare wird NICHT gewartet, es wird ein Promise zurückgeliefert commentsPromise: queryClient.fetchQuery( /* ... */) }); };
- Mit
deferwird ein oder mehrere Promises in die Komponente gegeben - Diese kann auf das aufgelöste Promise mit der
Await-Komponente warten - Die
Await-Komponente erwartet alsresolve-Property ein Promise und als Kind-Elemente eine Funktion (wie "früher" vor den Hooks...🙀) - Diese Funktion ruft
Awaitmit dem aufgelösten Promise-Wert auf - (Außerdem kann die Kompoente mit
errorElementnoch eine Komponente angeben, die gerendert wird, falls das Promise fehlschlägt) -
- Damit React in der Zwischenzeit einen Platzhalter anzeigen kann, muss darum eine
Suspense-Komponente gelegt werden
- Damit React in der Zwischenzeit einen Platzhalter anzeigen kann, muss darum eine
-
function BlogPostPage() { const { blogPost, commentsPromise } = useLoaderData(); return <> <Post post={blogPost} /> <Suspense fallback={"Comments loading..."}> <Await resolve={commentsPromise}> { comments => <CommentList comments={comments} /> } </Await> </Suspense> </> }
- Als Alternative zur Render-Funktion (
children) derAwait-Komponente kann deruseAsyncValue-Hook genutzt werden - Dieser kann unterhalb einer
Await-Komponente eingesetzt werden, um den Wert des Promises zu bekommen, auf den (das in der Hierarchie am nächsten darüberliegende)Awaitgewartet hat. -
function BlogPostPage() { const { blogPost, commentsPromise } = useLoaderData(); return <> <Post post={blogPost} /> <Suspense fallback={"Comments loading..."}> <Await resolve={commentsPromise}> <CommentList /> </Await> </Suspense> </> } function CommentList() { const comments = useAsyncValue(); return <>...</> }
- Die Daten für die
BlogPostPagewerden schon gelesen, aber nicht verzögert - Stell die Loader-Funktion
blogPageLoader(BlogPostPage.tsx) aufdeferum, so dass Blog-Post und Kommentare als Promises zurückgegeben werden. - Baue die
BlogPostPageso um, dass sie mitAwaitauf die beiden Promises wartet- Das
Awaitfür die Blog-Post-Daten soll mit einerchildren-Funktion arbeiten - Baue die
CommentList-Komponente so um, dass sieuseAsyncValueverwendet - Welcher Ansatz gefällt dir besser?
childrenoderuseAsyncValue?
- Das
- Kannst Du die
BlogPostPagemitSuspenseso bauen, dass der eigentlich Blog-Post schon angezeigt wird, auch wenn auf die Kommentare noch gewartet wird?- Dazu die
slowDown-Konstanten inbackend-queries.tssetzen (SlowDown für Kommentare muss länger sein als SlowDown für Artikel) - Beispiel:
const getBlogPostSlowdown = "?slowDown=1200"; const getCommentsSlowdown = "?slowDown=1600";
- Dazu die
- Lösung:
steps/20_defer
- Eine Route kann eine
actiondefinieren - Diese Funktion wird vom React Router aufgerufen, wenn die Route mit nicht-lesendem Zugriff (POST, PATCH, DELETE, PUT, ...) aufgerufen wird
loaderfür GET,actionfür (fast) alles andere
- Auch die
action-Funktion bekommtparamsundrequestübergeben - Das
Request-Objekt kann in diesem Fall zum Beispiel den den Inhalt eines Formulars enthalten - Mit der React Router
Form-Komponente kann ein Formular "submitted" werden, und die entsprechendeaction-Funktion wird dann aufgerufen-
const router = createBrowserRouter({ path: "add", element: <PostEditor />, action: addPostAction }); const addPostAction: ActionFunction = async ({ params, request }) => { const { title, body } = await request.json(); const result = await fetch("/...", { method: "POST", body: JSON.stringify({title, body})); if (result.status === "success") { return redirect("/blog"); } return result; }; function PostEditor() { // Form-Komponente vom React Router (identische API mit HTML form-Element) return <Form method="POST"> <input name="title" /> <input name="body" /> </Form> }
-
- Was ist eure Meinung dazu? 🤔
--
- Stimmt das: a good e2e test lasts forever, no matter how often you change the implementation (https://x.com/acdlite/status/1731354993230319974?s=20)?
--