Next.js Server Actions

Super Simple

14.04.2024

next.jsreactnextjsde
Gregor Wedlich
Gregor Wedlich
Life, the Universe and Everything.
Donate with:
Lightning
Alby

Inhaltsverzeichnis:

    In diesem Beitrag zeige ich euch, wie man mit Server Actions in Next.js arbeitet. Keine Sorge, ich werde es einfach halten und mich nicht in die hitzige Diskussion einmischen, ob es eine gute Idee ist, Server-Logik und Client-Logik zu vermischen. Aber weil diese Diskussion so kontrovers ist (und es immer noch bleibt), möchte ich euch dieses amüsante Meme dazu nicht vorenthalten.

    Um dieses Bild richtig zu verstehen, solltet ihr euch den Vortrag von Vercel ansehen, in dem das Feature der Server Actions erstmals vorgestellt wurde.

    Da einige ziemlich irritiert davon waren, serverseitigen Code innerhalb von clientseitigem Code auszuführen, und dies von vielen Seiten kritisiert wurde, hat das Internet natürlich direkt ein paar Memes dazu erfunden. Ein gutes Beispiel dafür ist das 2D-Game Redstone-Meme hier.

    Ob sich die Server Actions letztendlich durchsetzen werden, wird die Zeit zeigen. Die Community ist dazu gespalten.

    Kopf in den Nacken & Los geht's

    Leider komme auch ich nicht ganz um ein bisschen Einführung und Theorie herum. Also hier das Wichtigste: Mit der Einführung des App-Routers in Next.js 13.4 wurde ein neues Feature namens Server Actions vorgestellt. Dieses Konzept soll die Entwicklung von Webanwendungen optimieren, indem es die Kommunikation zwischen Client und Server verbessert und unnötige Komplexität reduziert. Mit Server Actions können bestimmte Aktionen direkt auf dem Server ausgeführt werden, anstatt sie auf der Client-Seite zu behandeln und dann über die /api an den Server weiterzugeben. Dadurch wird die Anwendungsarchitektur vereinfacht, die Leistung verbessert und eine klarere Trennung zwischen Client- und Server-Aufgaben erreicht.

    Seit Next.js 14 sind Server Actions offiziell als stabiles Feature verfügbar, und wir können jetzt von den Vorteilen profitieren, wie zum Beispiel der Reduzierung von Client-Server-Roundtrips und der Möglichkeit, serverseitige Mutationen durchzuführen. Allerdings gab es auch einige Kritikpunkte an der Einführung von Server Actions (siehe unser Meme). Einige sahen Ähnlichkeiten zur Arbeitsweise von PHP und kritisierten die Vermischung von Zuständigkeiten zwischen Client und Server. Auch Bedenken hinsichtlich der Skalierbarkeit und Serverbelastung wurden geäußert. Daher ist es wichtig, sorgfältig abzuwägen, ob und wie Server Actions in einem Projekt eingesetzt werden sollten, um die bestmögliche Architektur und Leistung zu erzielen.

    Server Actions müssen explizit als solche markiert werden. Das geschieht mit der Anweisung "use server";. Wir werden im Laufe unserer Beispiele noch näher darauf eingehen. Ein Vorteil von Server Actions ist, dass wir Async/Await in unseren Komponenten verwenden können, da Server Actions auf dem Server ausgeführt werden. Was jedoch nicht möglich ist, sind clientseitige React Hooks wie useEffect, useMemo usw. Diese können wie gewohnt nur in Client Components verwendet werden.

    Es gibt verschiedene Möglichkeiten, wie wir Server Actions einsetzen können: In einem Formelement, durch Event Handler oder auch in einem useEffect-Hook. Ich werde hier auf alle Varianten mit Beispielen eingehen und hoffe, dass es euch weiterhilft und ihr danach etwas mehr Klarheit habt.

    Server Actions in form Elementen

    React hat das HTML

    -Element mit der Prop action erweitert, die es uns ermöglicht, Server Actions direkt beim Absenden des Formulars auszuführen.

    1 return ( 2 <form action={createPost}> 3 <label className="block">Title</label> 4 <input type="text" name="title" className="block mb-2" /> 5 6 <label className="block">Text</label> 7 <textarea 8 name="post-text" 9 rows={4} 10 className="block mb-2" 11 placeholder="Post Text" 12 ></textarea> 13 <button type="submit" className="bg-slate-600 p-2"> 14 Create Post 15 </button> 16 </form> 17 );

    Wenn wir die Action in einem <form>-Element aufrufen, erhält sie automatisch das FormData-Objekt. Mit diesem Objekt sind wir nicht mehr auf den React Hook useState angewiesen, um die Felder zu verwalten. Stattdessen können wir die nativen Methoden von FormData nutzen und darauf zugreifen.

    1export default function CreatePostForm() { 2 async function createPost(formData: FormData) { 3 'use server'; 4 5 const data = { 6 title: formData.get('title') as string, 7 body: formData.get('post-text') as string, 8 }; 9 10 await createPostAction(data); 11 }

    Zuerst kennzeichnen wir mit "use server"; unsere Funktion explizit als Server Action.

    Die eigentliche Verarbeitung haben wir in eine extra Datei namens "action.ts" ausgelagert. Es wäre aber auch möglich, die Funktionalität direkt in unsere Server-Komponente einzufügen, und es würde sogar als Inline-Code innerhalb der action-Prop in unserem Formelement funktionieren.

    Hier geht es zu unserem Beispielcode.

    Schaut euch auch die Dokumentation dazu an dort wird das ganze noch weiter vertieft.

    Server Actions mit Event Handler

    Wie schon am Anfang gesagt, lassen sich Server Actions auch außerhalb von Formelementen aufrufen.

    Lasst uns also die Möglichkeit betrachten, Server Actions mit Event-Handlern (onClick, onChange, ...) aufzurufen.

    Dazu haben wir ein kleines Programm geschrieben, das durch einen Klick auf den Button "Click me" die Server Action aufruft. Dabei wird ein Fake-Eintrag bei unserer API erstellt und die Antwort an unsere Client-Komponente "ShowPost.tsx" weitergegeben.

    1'use client'; 2 3import { createPostAction } from '@/lib/actions'; 4import { useState } from 'react'; 5import ShowPost from '@/components/ShowPost'; 6import { Post } from '@/lib/types'; 7 8export default function CreatePost() { 9 const [post, setPost] = useState<Post>(); 10 11 return ( 12 <> 13 <button 14 className="bg-stone-500 p-1 rounded" 15 onClick={async () => { 16 const createPost = await createPostAction(); 17 setPost(createPost); 18 }} 19 > 20 Click me 21 </button> 22 <div className="pt-10">{post && <ShowPost post={post} />}</div> 23 </> 24 ); 25}

    Ich denke, dass mein Beispiel hier ganz gut erklärt, was eigentlich passiert. Die onClick-Methode ruft in einer Async-Funktion die Server Action auf und schreibt die Antwort mit dem Hook useState in die Variable "post".

    Den kompletten Code dazu könnt ihr euch hier anschauen.

    Server Actions mit useEffect aufrufen

    Eine weitere Möglichkeit ist es, innerhalb von Client-Komponenten mit dem React useEffect-Hook Server Actions aufzurufen. Ich werde hier kurz darauf eingehen, und am Ende findet ihr wieder den Link zu meinem Code-Beispiel.

    Bei jedem Reload der Seite holen wir uns mit unserem useEffect-Hook alle Einträge von unserer API.

    1'use client'; 2 3import { useEffect, useState } from 'react'; 4import { getPostsAction } from '@/lib/actions'; 5import { Post } from '@/lib/types'; 6 7export default function ShowPosts() { 8 const [posts, setPosts] = useState<Post[]>([]); 9 10 useEffect(() => { 11 const fetchPosts = async () => { 12 try { 13 const posts = await getPostsAction(); 14 setPosts(posts); 15 } catch (error) { 16 console.log(error); 17 } 18 }; 19 20 fetchPosts(); 21 }, []); 22 23 return ( 24 <div> 25 <ul> 26 {posts.map((posts) => ( 27 <li className="my-5" key={posts.id}> 28 {posts.id}. {posts.title} 29 </li> 30 ))} 31 </ul> 32 </div> 33 ); 34}

    Es wird bei jedem Seiten-Reload die API aufgerufen const posts = await getPostsAction();, die Antwort unseres Servers speichern wir nun mit useState in einer Variable und liefern sie an unser Frontend aus.

    Hier geht es zu dem Beispielcode.

    Error Handling mit Server Actions

    In unseren bisherigen Beispielen haben wir gänzlich auf die Fehlerbehandlung im Frontend verzichtet, daher würde ziemlich großer Wahrscheinlichkeit unsere App bei Fehlern abstürzen. Deswegen zeige ich hier noch, wie wir in unserem Beispiel mit <form>-Elementen und Server Actions Fehler abfangen können.

    Damit wir dem Frontend die Fehlermeldung übergeben können, benötigen wir eine Client-Komponente. Damit unser Programm aber nicht abstürzt, müssen wir unsere Komponente dazu etwas umbauen. Da wir Server Actions inline innerhalb unseres Action-Elements in unserem <form>-Element aufrufen können, refaktorieren wir unseren Code aus dem ersten Beispiel so, dass wir bei einem Fehler mit useState den Fehler speichern und an unser Frontend übergeben.

    Der Code vorher:

    1// CreatePost.tsx 2 3import { createPostAction } from '@/lib/actions'; 4 5export default function CreatePostForm() { 6 async function createPost(formData: FormData) { 7 'use server'; 8 9 const data = { 10 title: formData.get('title') as string, 11 body: formData.get('post-text') as string, 12 }; 13 14 await createPostAction(data); 15 } 16 17 return ( 18 <form action={createPost}> 19 <label className="block">Title</label> 20 <input type="text" name="title" className="block mb-2" /> 21 22 <label className="block">Text</label> 23 <textarea 24 name="post-text" 25 rows={4} 26 className="block mb-2" 27 placeholder="Post Text" 28 ></textarea> 29 <button type="submit" className="bg-slate-600 p-2"> 30 Create Post 31 </button> 32 </form> 33 ); 34}

    Wenn ihr hier auf "Create Post" geht wenn ihr nichts angegeben habt wird euch der Fehler nicht angezeigt nur wenn ihr innerhalb der Server Action die Antwort loggen würdet könntet ihr in der Server Console den Fehler sehen. Im schlimmsten fall stürzt aber auch eure App ab und um das zu verhinden zeigen weißen wir den User darauf hin das er Titel und Text angeben muss.

    1// actions.ts 2'use server'; 3import { Post, PostForm } from '@/lib/types'; 4 5export async function createPostAction(postData: PostForm): Promise<Post> { 6 try { 7 const res = await fetch('https://jsonplaceholder.typicode.com/posts', { 8 method: 'POST', 9 body: JSON.stringify({ 10 title: postData.title, 11 body: postData.body, 12 userId: 1, 13 }), 14 headers: { 'Content-type': 'application/json; charset=UTF-8' }, 15 }); 16 // Fake response from API 17 const data: Post = await res.json(); 18 19 // Show Post when click on "Create Post" 20 console.log(data); 21 22 return data; 23 } catch (error) { 24 console.log(error); 25 throw new Error('Failed to create post'); 26 } 27}

    Der Code nach dem wir ihn Refaktoriert haben:

    1//CreatePost.tsx 2'use client'; 3import { createPostAction } from '@/lib/actions'; 4import { useState } from 'react'; 5 6export default function Form() { 7 const [error, setError] = useState(''); 8 9 return ( 10 <form 11 action={async (formData: FormData) => { 12 const data = { 13 title: formData.get('title') as string, 14 body: formData.get('post-text') as string, 15 }; 16 17 const result = await createPostAction(data); 18 if ('error' in result) { 19 setError(result.error); 20 } 21 }} 22 > 23 <label className="block">Title</label> 24 <input type="text" name="title" className="block mb-2" /> 25 26 <label className="block">Text</label> 27 <textarea 28 name="post-text" 29 rows={4} 30 className="block mb-2" 31 placeholder="Post Text" 32 ></textarea> 33 {error && <p className="text-red-400">{error}</p>} 34 <button type="submit" className="bg-slate-600 p-2"> 35 Create Post 36 </button> 37 </form> 38 ); 39}
    1// actions.ts 2'use server'; 3import { Post, PostForm } from '@/lib/types'; 4 5export async function createPostAction( 6 postData: PostForm 7): Promise<Post | { error: string }> { 8 // Check if title and body is not empty 9 if (!postData.title || !postData.body) { 10 return { error: 'Title and body are required.' }; 11 } 12 13 try { 14 const res = await fetch('https://jsonplaceholder.typicode.com/posts', { 15 method: 'POST', 16 body: JSON.stringify({ 17 title: postData.title, 18 body: postData.body, 19 userId: 1, 20 }), 21 headers: { 'Content-type': 'application/json; charset=UTF-8' }, 22 }); 23 // Fake response from API 24 const data: Post = await res.json(); 25 26 // Show Post when click on "Create Post" 27 console.log(data); 28 29 return data; 30 } catch (error: any) { 31 console.log(error); 32 return { error: error.message }; 33 } 34} 35

    Das war's auch schon! So einfach könnt ihr in Formelementen mit Fehlern umgehen. Diese können natürlich auch anders dargestellt werden, z.B. mit einer Toast-Message oder Ähnlichem.

    Fazit

    In diesem Beitrag habe ich euch gezeigt, wie man mit Server Actions in Next.js arbeitet. Wir haben uns angesehen, wie Server Actions in Formelementen, durch Event-Handler und in Kombination mit dem useEffect-Hook verwendet werden können. Dabei habe ich auch erklärt, wie ihr mit Fehlern umgehen könnt.

    Server Actions bieten einige Vorteile, wie die Reduzierung von Client-Server-Roundtrips und die Möglichkeit, Async/Await in euren Komponenten zu nutzen. Allerdings gibt es auch Kritikpunkte, wie die Vermischung von Zuständigkeiten zwischen Client und Server und Bedenken hinsichtlich der Skalierbarkeit.

    Ob sich Server Actions letztendlich durchsetzen werden, bleibt abzuwarten, da die Community dazu gespalten ist. Aber mit etwas Übung und Anpassung könnt ihr die Vorteile dieser neuen Funktionalität voll ausschöpfen und eure Anwendungen optimieren.

    Ich hoffe, dieser Beitrag hat euch einen guten Überblick gegeben und ihr habt nun mehr Klarheit darüber, wie ihr Server Actions in euren Projekten einsetzen könnt. Viel Spaß beim Ausprobieren und Entwickeln!

    Comments: