Next.js v14: Quando il Client ed il Server diventano un’unica Astrazione (2/2)

Nel precedente articolo, abbiamo visto cosa sono e a cosa servono i nuovi Server Components di React.js e Next.js. Adesso vedremo un'altra funzionalità del nuovo Next.js 14: le Server Actions.

 

Cosa sono le Server Actions

Le Server Actions sono delle funzioni asincrone eseguite sul server, generalmente usate per gestire la fase di submission dei form e la mutazione di dati.

 

IMG3 (1).jpg

 

Gestione dei form

In Vanilla React, la gestione dei form avviene tendenzialmente leggendo i singoli campi degli input e inviandoli attraverso una fetch intercettando il click del pulsante submit. Buona prassi è anche quella di usare librerie esterne per la validazione, come React Hook Form.

Questo approccio cambia radicalmente con le Server Actions di Next.js 14, potendo inserire una funzione all’interno dell’attributo action del form:

 

1export default function NewPostPage() {
2  const createPost = async (formData: FormData) => {
3    "use server";
4    console.log(formData)
5  }
6
7  return (
8    <form action={createPost}>
9      <label>Title</label>
10      <input type="text" name="title" />
11      <button type="submit">Submit</button>
12    </form>
13  );
14}

 

In questo component dobbiamo attenzionare alcune nozioni importanti:

  • Stiamo passando all’attributo action del form una funzione asincrona marchiata con la direttiva “use server”; è questa direttiva che richiede a Next.js di eseguire la funzione nel server, diventando a tutti gli effetti una server action.
  • La Server Action prenderà come argomento di input un oggetto formData, i quali campi saranno i valori del form. Al submit, i dati del form saranno automaticamente inviati al server ed elaborati con la server action indicata in action, astraendo la chiamata fetch al server.

 

unnamed.png

Astrazione della fetch in POST al Submit

⚠️ L’attributo action del form non è quello di default di HTML, ma è una versione estesa da Next.js la quale la crea in maniera astratta l’endpoint POST

 

Vantaggi e Svantaggi delle Server Actions

I vantaggi di tale approccio sono molteplici:

  • Eliminazione della necessità di creare API endpoints in un backend separato (tutto astratto da Next.js).
  • Miglioramento della sicurezza tramite protezione integrata da diversi tipi di attacchi (POST requests, closure criptate, strict input checks, etc.).
  • Progressive enhancement: il form funziona anche se JS nel client è disabilitato.
  • Integrazione col sistema di caching e rivalidazione di Next.js.

L’unico svantaggio invece è il seguente:

  • Il backend è un tutt’uno col frontend, ma in questo modo gli altri client non possono usare l’action (in tale caso meglio usare le Route Handlers, ex API Routes).

 

File actions.ts

Per convezione e buona prassi, possiamo spostare tutte le Server Actions (le funzioni indicate con “use server") in un file separato denominato action.ts all’interno della rotta, con la direttiva in cima al file.

 

1import { createPost } from "./action";
2
3export default function NewPostPage() {
4  return (
5    <form action={createPost}>
6      <label>Title</label>
7      <input type="text" name="title" />
8      <button type="submit">Submit</button>
9    </form>
10  );
11}
1"use server";
2
3export async function createPost(formData: FormData) {
4  // ...
5}

 

In questo modo, tutte le funzioni dentro action.ts saranno server actions grazie alla corrispondente direttiva in cima al file.

 

Operazioni CRUD

L’oggetto FormData che ci arriva come parametro dall’action del form, è composto da un insieme di chiave/valore corrispondenti alle entry del form.

Possiamo quindi usare dei metodi specifici per prendere le informazioni del form e usarle per manipolarle, validarle o salvare nel DB.

 

1"use server";
2
3import { sql } from "@vercel/postgres";
4
5export async function createPost(prevState: any, formData: FormData) {
6  const title = formData.get("title") as string;
7  const content = formData.get("content") as string;
8  const publishedAt = new Date(Date.now()).toISOString();
9
10    await sql`
11    INSERT INTO posts (title, content, publishedAt)
12    VALUES (${title}, ${content}, ${publishedAt})
13  `;
14}

 

Le Server Actions sono dunque il posto perfetto dove implementare tutte le operazioni CUD (create, update, e delete) per il DB (il read lo abbiamo già visto con le fetch di un un normale server component).

⚠️ Anche se facciamo operazioni di update e delete, le Server Actions sono sempre inizializzate con metodo POST.

 

Hooks useFormStatus e useFormState

Insieme a questo nuovo approccio per la gestione dei form attraverso le Server Actions, Next.js 14 mette a disposizione due nuovi hook per gestire lo stato dei form.

In base allo stato della rete, le actions potrebbero prendere del tempo per ritornare una risposta della corretta esecuzione. Possiamo quindi mostrare lo stato della submission usando l’hook useFormStatus.

Caratteristiche dell’hook:

  • Dato che è strettamente correlato al form, deve essere definito come child dell’elemento Form.
  • Essendo un hook React con elementi di interattività, deve essere usato come Client Component.

 

1import SubmitPostButton from "@/app/components/SubmitPostButton";
2import { createPost } from "./action";
3
4export default function NewPostPage() {
5  return (
6    <form action={createPost}>
7      <label>Title</label>
8      <input type="text" name="title" />
9      <SubmitPostButton />
10    </form>
11  );
12}
1"use client";
2
3import { useFormStatus } from "react-dom";
4
5export default function SubmitPostButton() {
6  const { pending } = useFormStatus();
7
8  return (
9    <button type="submit" disabled={pending}>
10      Add
11    </button>
12  );
13}

 

Oltre allo stato del form, possiamo leggere una eventuale risposta custom dalla Server Actions, o un errore, ritornando un oggetto dalla action e leggendolo con l’hook useFormState.

Per fare ciò dobbiamo fare alcune modifiche incisive sia al form che all’action.

Per quanto riguarda l’action:

  • Cambiare i parametri in ingresso dell’action in maniera tale da ricevere un prevState come primo argomento.
  • Ritornare il messaggio o l’errore come oggetto.

 

1"use server";
2
3export async function createPost(prevState: any, formData: FormData) {
4  //...
5  try {
6    throw new Error("Something went wrong");
7  } catch (error) {
8    return { error: (error as Error).message };
9  }
10  return { message: "Post created successfully" };
11}

 

Modifiche al form:

  • Indicare il form come “use client” (necessario poiché la risposta cambierà in maniera interattiva il component).
  • Decorare l’action importata con l’hook useFormState.
  • Invocare l’action del formState al submit del form.
  • Leggere, infine il messaggio dallo stato o l’errore.

Così abbiamo a disposizione nel component form l’oggetto message o error precedentemente tornato dalla createPost.

 

1"use client";
2
3import { useFormState } from "react-dom";
4
5export default function NewPostPage() {
6  const [state, formAction] = useFormState(createPost, {
7    message: "",
8  });
9
10  if (state?.error) {
11    return <p>{state?.error}</p>;
12  }
13
14  return (
15    <form action={formAction} className={styles.container}>
16      {/* ... */}
17      <p>{state?.message}</p>
18    </form>
19  );
20}

 

Conclusione

Come abbiamo visto, Next.js 14 sta inglobando al suo interno molte caratteristiche che prima erano esclusivamente di dominio del backend. L’astrazione che implementa per passare da ambiente client a ambiente server è una peculiarità unica nel mondo web development, mai vista in un framework nato per il frontend.

Questo nuovo approccio fa riflettere sulle barriere che ci piace darci per definire il nostro dominio di competenza, anche se dovremmo essere concentrati più a realizzare delle web app efficaci, e non a darci dei limiti. Ma se proprio non potete fare a meno dei titoli, adesso che state a tutti gli effetti sviluppando anche sul server, se prima eravate frontend developer vi autorizziamo a scrivere fullstack developer sull’headline di LinkedIn :)

Ultimi Articoli