Coding

È arrivato il momento di passare a Next.js?

Ti sei mai chiesto perché si parla tanto di Next.js nel mondo React? Gabriele esplora l'evoluzione del panorama React e il ruolo di Next.js nell'adozione delle nuove funzionalità full-stack, discutendo pro e contro e confrontandolo con altri framework.

Gabriele Petronella Gabriele Petronella & Vincenzo Guerrisi Vincenzo Guerrisi
21 marzo 2024 — 14 min

React.js ha più di 10 anni ed è di gran lunga il framework JavaScript front-end più popolare del settore. Nonostante non sia più l’ultimo arrivato, negli ultimi anni React è cambiato in modo piuttosto drastico.

In Buildo usiamo React dal 2014 e storicamente abbiamo sviluppato SPA con esso (usando librerie come Create React App (CRA) finché è stata rilevante e, più recentemente, il template React di Vite). Tuttavia cerchiamo sempre di migliorare, e abbiamo deciso di guardare più da vicino i framework full-stack basati su React.

Perché? Vediamo prima un po’ di contesto.

Dalla libreria al framework

Quando React divenne popolare, veniva presentato come una soluzione leggera, focalizzata sul rendering della UI in modo semplice ed efficiente con JavaScript. Era famosa la definizione “the V in MVC”, che metteva in evidenza il suo focus su una parte molto specifica dello stack frontend. Questo lo poneva in contrasto diretto con soluzioni come Angular.js (oggi Angular), che offrivano un’esperienza più batteries-included, con router, data fetching, dependency injection e altri componenti opinati. In altre parole, React era solo una libreria, mentre Angular era un framework. Questo significa che usare React comportava anche scegliere un insieme di librerie di accompagnamento per gestire cose come state management, routing, data fetching e così via.

Oggi non è più così. O meglio, non è più strettamente così. Se guardiamo con attenzione, negli ultimi 2-3 anni React ha iniziato a:

a) raccomandare l’uso di framework specifici per costruire app. Sulla home page di React si legge molto esplicitamente: “To build an entire app with React, we recommend a full-stack React framework”

b) includere aspetti architetturali nel proprio design, come data fetching e caching

Quindi sì, React è ancora una libreria, ma è ora una libreria che fornisce molti building block architetturali per i framework. Puoi ancora usare React senza un framework, ma non otterrai tutte le funzionalità come previsto dai maintainer di React (ad esempio, i React Server Components richiedono un’integrazione profonda con il framework per essere utili).

__wf_reserved_inherit

Il ruolo di Vercel e Next.js

Questo cambiamento non è stato del tutto casuale. Vercel, l’azienda dietro Next.js, uno dei framework React più popolari, ha sempre collaborato da vicino con il core team di React in Meta e ha finito per assumere diversi membri di rilievo del team.

Questo ha ovviamente influito molto sul design di React e di Next. Da un lato ha spinto React a entrare in territori da cui storicamente era rimasto alla larga (come data fetching, full-stack rendering, caching e così via). Dall’altro ha portato Vercel a reimmaginare il funzionamento di Next in modo più integrato con React, portando a Next 13 e alla App Directory, che è sostanzialmente una riscrittura di Next.

La situazione oggi è leggermente ambigua: molte nuove funzionalità di React compaiono prima come feature sperimentali di Next.js, e vengono poi standardizzate e documentate come feature core di React. È il caso di React Server Components (RSC), Server Actions, Caching API, l’hook optimistic e così via.

Per complicare ulteriormente le cose, React non rilascia una major da quasi due anni (siamo ancora a React 18 al momento in cui scriviamo), e tutte queste feature sono rilasciate in un canale React canary pensato per i framework, sebbene di fatto lo usi praticamente solo Next.js. Quindi, in sostanza, l’unico modo per usare qualsiasi feature importante di React sviluppata negli ultimi 2 anni è attraverso Next.js.

__wf_reserved_inherit

E Remix?

Remix è un altro popolare framework React full-stack, anch’esso raccomandato dalla documentazione ufficiale di React. Remix è essenzialmente un pioniere in questo campo: ha plasmato il modo in cui i framework React full-stack potrebbero apparire, e ora si trova nella scomoda posizione di dover inseguire le nuove API di React.

In altre parole, Remix ha introdotto molte funzionalità che ora sono fornite da React stesso in modo leggermente diverso, e le sta adottando gradualmente. Questo tweet di Ryan Florence, uno dei creatori di Remix, lo riassume bene.

Remix sembra molto promettente, lo teniamo d’occhio, ed è probabilmente il prossimo framework che vogliamo esplorare. Tuttavia, per lo scopo del nostro esperimento, abbiamo deciso di guardare più da vicino Next, poiché volevamo testare le feature più recenti e interessanti offerte da React, e al momento Next è l’unica opzione praticabile per farlo.

Next.js? Quale?

Next.js non è un framework nuovo: il primo rilascio è del 2016, ma — come abbiamo visto — da allora molte cose sono cambiate. Oggi Next.js arriva in due varianti: il Pages Router e l’App Router. In breve:

  • Il Pages Router è il modo vecchio (e stabile!) di scrivere applicazioni Next.js. Fornisce tutte le funzionalità essenziali per organizzare e costruire applicazioni React renderizzate lato server.
  • L’App Router è il nuovo modo di scrivere applicazioni Next.js, introdotto in Next.js v13, che adotta tutte le nuove feature (stabili e instabili) di React come i React Server Components.

Poiché il nostro obiettivo è imparare il più possibile sulle nuove funzionalità di React, abbiamo deciso di testare Next.js usando l’App Router.

Un approccio diverso allo sviluppo di applicazioni web

Come abbiamo visto, Next.js fornisce un framework completo per le applicazioni React, offrendo nuovi paradigmi per tutto ciò che non era gestito direttamente dalla libreria React. Questo include, per esempio, routing, data fetching e server-side rendering.

Routing basato su file

Come molti framework sul mercato, Next.js offre un modo per generare la logica di routing dell’app direttamente dai file e dalle directory che compongono il codice dell’applicazione. In breve, le diverse viste dell’app vengono create semplicemente aggiungendo nuovi file in una gerarchia specifica all’interno delle directory pages/ o app/. Questo è un approccio completamente diverso rispetto a quello seguito usando React in modo diretto, dove la logica di routing deve essere espressa programmaticamente tramite librerie come react-router.

Con il rilascio di Next 13, Vercel ha anche rivisto completamente il meccanismo di routing introducendo la App Directory. Questa directory fa un uso molto più massiccio di alcune feature offerte da React stesso che prima non potevano esistere con la struttura della legacy Pages directory, come Suspense, Concurrent Rendering ed Error Boundaries.

✅ Pro

Nella nostra esperienza, il routing basato su file offre un approccio più semplice alla struttura dell’applicazione rispetto all’uso diretto di librerie di routing come react-router, poiché si basa su un principio semplice: il filesystem è la fonte di verità principale. I file all’interno delle directory pages o app diventano automaticamente una route, eliminando la necessità di definirle manualmente e semplificando notevolmente la fase di setup. Inoltre, l’approccio alle route annidate permette di strutturare il layout dell’applicazione in modo molto semplice ma potente, definendo le parti comuni dell’interfaccia in una gerarchia facilissima da comprendere.

Aggiungendo funzionalità extra più avanzate come Route Groups, Parallel Routes e Routes Interception, il meccanismo di routing di Next.js si è dimostrato estremamente potente, senza complicare la gestione dei casi d’uso più basilari.

👎🏻 Contro

Soprattutto nelle prime fasi di un progetto, ci siamo spesso trovati a rivedere la gerarchia di routing a causa di layout che potevano essere fattorizzati in un elemento comune, di nuove aree dell’applicazione che non dovevano conformarsi al layout generale e della conseguente creazione di Route Groups o ristrutturazione della gerarchia di cartelle. Sebbene dal punto di vista dello sviluppo questo processo sia piuttosto intuitivo, non si allinea bene con la gestione del version control. Le modifiche alla struttura del filesystem hanno un impatto significativo sugli sviluppi in corso e possono creare problemi di coordinamento e conflitti su tutti i branch attivi.

Server-side rendering e RSC

L’SSR consente di renderizzare i componenti React sul server e inviare al client HTML già completo, che viene poi solo “idratato” per diventare interattivo. Questo approccio ha alcuni benefici in termini di performance (il tempo di caricamento iniziale della pagina si riduce grazie al pre-rendering lato server) e — presumibilmente, non è mai una scienza esatta — avvantaggia anche la SEO, poiché il contenuto della pagina è subito disponibile per l’indicizzazione e non richiede l’esecuzione di JavaScript.

Nelle versioni precedenti di Next.js, l’SSR veniva realizzato principalmente tramite utility come getServerSideProps, una funzione chiamata al momento della richiesta (cioè quando l’utente richiede una pagina) ed eseguita sul server. Questa funzione, per esempio, esegue il data fetching a ogni richiesta e popola la UI con dati in tempo reale, in modo simile a come funziona il loader in Remix.

Con l’avvento dei RSC (React Server Components) in React e l’introduzione della App Directory, Next si è spostato verso un approccio che ottimizza meglio il processo di rendering e migliora l’esperienza per gli sviluppatori.

I Server Components vengono renderizzati esclusivamente sul server, una volta per richiesta. Non vengono idratati sul client, quindi non possono contenere parti interattive. In questo senso sono molto simili ai “template” di altri framework orientati al server (come PHP, Rails e così via). La parte interessante del design è che si compongono (quasi) senza attriti con i componenti React regolari, quindi puoi per esempio avere pattern come:

export async function UsersPage() {
  const users = await userService.getUsers();
  return <UsersList users={users} />
}

dove UsersPage è un server component renderizzato una sola volta sul server, e UsersList può essere un componente React regolare.

La composizione può spingersi molto oltre, ed è molto potente. È anche completata dalle Server Actions, una funzionalità che definisce funzioni che girano sul server ma possono essere richiamate in modo trasparente da qualsiasi componente, inclusi quelli che girano sul client. Puoi pensarle come a un’API in stile RPC per definire endpoint backend.

✅ Pro

Ci è piaciuto il pattern di spostare il data fetching sul server e associarlo strettamente alla logica di rendering. Non è un’idea esclusiva dei RSC: è stata esplorata a lungo da Remix e dalle versioni precedenti di Next.js, ma i RSC rendono questo pattern privo di attriti. Ecco alcuni confronti.

In una normale applicazione SPA potremmo aver usato qualcosa come React Query in questo modo (in questo esempio il data fetching avviene nel browser, quindi getUsers() dovrebbe essere in qualche modo un endpoint autenticato):

export function UsersPage() {
  const { data: users } = useQuery({ queryKey: ["users"]; queryFn: () => userService.getUsers(); });
  return <UsersList users={users} />
}

In Next.js con Pages Router apparirebbe così:

export async function getServerSideProps() {
  const users = await userService.getUsers();
  return { props: { users } };
}

export function UsersPage({ users }: InferGetServerSidePropsType<typeof getServerSideProps>) {
  return <UsersList users={users} />
}

In Remix apparirebbe così:

export async function loader() {
  const users = await userService.getUsers();
  return json({ users });
}

export function UsersPage() {
  const { users } = useLoaderData<typeof loader>();
  return <UsersList users={users} />
}

Infine, in Next.js con App Router appare così:

export async function UsersPage() {
  const users = await userService.getUsers();
  return <UsersList users={users} />
}

Potrebbe non sembrare molto, ma co-localizzare data fetching e logica di rendering riduce l’attrito e — soprattutto — abilita composizioni prima impossibili sul server. Infatti, mentre tutte le altre soluzioni server erano limitate al data fetching nelle route di primo livello, i Server Components non hanno la stessa limitazione.

Questo significa che possiamo avere — per esempio — un componente StripeCheckOut che incapsula sia la UI sia la logica server necessaria per eseguire un check-out su Stripe, e che possiamo semplicemente inserire nel nostro albero di rendering.

Un vantaggio interessante del data fetching lato server, in generale, è poter usare l’applicazione “frontend” come proprio BFF (Backend for Frontend). I BFF sono tradizionalmente usati per aggregare dati da più fonti e fornire un’API più comoda per il front end. Con il data fetching lato server possiamo fare la stessa cosa, ma direttamente nell’applicazione front-end. Alcuni potrebbero storcere il naso nel fondere queste due “responsabilità” in un singolo componente architetturale, ma si potrebbe sostenere che aggregare e consumare dati per un front-end siano, in ultima analisi, la stessa responsabilità.

👎🏻 Contro

I pattern di composizione che usano i React Server Components sembrano magici, forse un po’ troppo. Poiché l’implementazione tecnica dietro è molto sofisticata e molto giovane, la developer experience non è ottima quando si tratta di debugging. Per esempio, è molto difficile ispezionare una richiesta RSC, poiché usa un content type personalizzato che l’ispettore del browser fa fatica a interpretare.

Un altro contro è che, pur essendo la composizione ben pensata, devi comunque essere molto consapevole se ti trovi in un server component oppure no, perché l’astrazione a volte “perde”. Alcuni esempi:

  • Se lavori con un SDK come Supabase, devi usare due API diverse per creare un client a seconda che tu sia in un server component o in un client component
  • Lavorare con react-i18next ha presentato sfide simili e ci ha costretti a scegliere manualmente un’API diversa per tradurre le stringhe a seconda del componente in cui ci trovavamo.

Questo rende un po’ tedioso il refactoring quando decidi di passare da un Server a un Client component, cosa che può capitare abbastanza spesso (per esempio, se vuoi aggiungere stato client-side o un comportamento interattivo).

Non è tutto oro quello che luccica

In Buildo abbiamo anni di esperienza nello sviluppo di app React nel modo tradizionale SPA.

In definitiva, ci è piaciuta l’idea generale della direzione full-stack verso cui React si sta muovendo. Vediamo anche il merito di Next.js nel provare a mettere tutto insieme in un framework davvero utilizzabile. Detto questo, ci sono alcune aree di Next.js in particolare che abbiamo trovato molto carenti e per le quali probabilmente aspetteremo ancora un po’, esplorando alternative nel frattempo.

Mancanza di controllo

Anche se Next.js è, in ultima analisi, un modo di scrivere un’applicazione web client-side alimentata da un server dedicato, il framework non ti dà accesso all’API sottostante.

Questo significa che alcune cose basilari che ti aspetteresti di poter fare sono praticamente impossibili, per esempio:

  • Vuoi restituire uno status code personalizzato da una server action? Scordatelo.
  • Puoi eseguire l’intera app su Node.js, ma il middleware di Next.js non ti dà comunque accesso al runtime Node, il che significa che non puoi leggere un file nel middleware.
  • Vuoi ispezionare dati arbitrari dalla Web Request in un Server Component? No: devi usare un set specifico di API di Next per accedere a informazioni limitate (come headers() , cookies(), e così via).

Testing

A un certo punto volevamo scrivere test automatici che coinvolgessero i RSC e abbiamo scoperto che il panorama è piuttosto desolato. Strumenti di test popolari come Jest e React Testing Library non hanno ancora trovato un modo per affrontarli. Esistono hack e workaround, ma sembra che una gran parte dell’ecosistema sia rimasta indietro e non abbia ancora colmato il gap.

Politica di caching pericolosa

Next.js patcha automaticamente fetch per introdurre semantiche di caching proprie. È tanto brutto quanto suona, e sembra che alla fine verrà sostituito da API più esplicite.

La parte peggiore è che fetch è stato patchato per cachare tutto per sempre di default, il che è un’idea pessima per una ragione molto semplice: sotto-cachare è un potenziale problema di performance, mentre over-cachare è un bug.

Next.js lavora sotto un’ipotesi di closed-world in cui, di default, i dati possono cambiare solo in reazione ad azioni all’interno dell’app. Nella pratica questo è raramente il caso.

Per esempio:

export async function ItemsPage() {
  const items = await getItems(); // getItems uses fetch under the hood
  return <ItemList items={items} />
}

Cosa succede se quegli elementi cambiano nel sistema esterno? Congratulazioni, hai appena introdotto un bug, poiché Next.js cacherà gli elementi per sempre sul server, non importa quanto insisti con il pulsante di refresh nel browser.

Come risolverlo? Devi sperare che qualunque funzione di data fetching tu stia usando permetta di passare opzioni a fetch, così da poterle passare { cache: "no-store" }. Se usa fetch sotto il cofano ma non ti consente di personalizzarlo, allora devi:

  • rendere l’intero componente dinamico con export const dynamic = force-dynamic
  • oppure invocare unstable_noStore() prima di effettuare la chiamata

Frustrante: questo default si applica anche alle chiamate POST e PUT, il che è ancora più controintuitivo. Allo stesso tempo non può essere cambiato globalmente, quindi è praticamente un bug in agguato ogni volta che interagisci con un sistema esterno via HTTP.

Dev Server

Sapevamo che il dev server di Next.js fosse un’area problematica, ma sperimentarlo in prima persona è un’altra cosa. In un’applicazione relativamente piccola siamo arrivati al punto in cui la navigazione verso una nuova pagina per la prima volta richiedeva oltre 30 secondi. Ci sono modi per conviverci e vari workaround suggeriti dal team di Vercel, ma nel migliore dei casi abbassano i tempi a qualche secondo. Confrontando questa esperienza con altri strumenti moderni come Vite, lavorare con il dev server di Next sembra di essere tornati al 2018.

Vercel sta lavorando attivamente per migliorarlo con un nuovo bundler, Turbopack, che secondo loro offrirà prestazioni drasticamente migliori. Tuttavia non era pronto per la produzione al momento della stesura (lo abbiamo provato su uno dei nostri progetti e non compilava, quindi abbiamo abbandonato l’esperimento). Lo stato di prontezza di Turbopack può essere seguito qui: https://areweturboyet.com/.

Conclusioni

React si sta espandendo per includere primitive e pattern che permettono di creare ricche applicazioni full-stack, ed è molto entusiasmante. Anche se tutto sembra ancora molto prematuro e non del tutto rifinito, possiamo già apprezzare alcuni dei benefici che questo comporta. In questo scenario, Next.js con App Directory è il primo framework a integrare direttamente tutte le nuove feature full-stack di React. Pur essendo piacevole da usare quando funziona, appare ancora molto immaturo: le API sono instabili, la DX è carente e il framework rende difficile aggirarne i limiti.

Detto questo, siamo ottimisti sul futuro di React e ci sono piaciute molte delle scelte architetturali generali. È fantastico che React supporti sempre di più le architetture full-stack, migliorando al contempo il supporto alle SPA.

Continueremo a investire ed esplorare quest’area, e vi aggiorneremo!

Gabriele Petronella
Gabriele Petronella Managing Partner & CTO

Gabriele is co-founder of Buildo, where he works as co-CTO. He is passionate about front-end and back-end architectures, and has long-term experience with React, TypeScript and Scala.

Vincenzo Guerrisi
Vincenzo Guerrisi AI Productivity Engineer and Tech Lead

Vincenzo joined Buildo in 2016 as a Fullstack Engineer, with a strong focus on Frontend technologies. He pays close attention to process improvement and team dynamics, working every day to foster effective collaboration and bring out the best in each team member.

NEWSLETTER

Ehi, c'è anche una Newslettero!
Sì, proprio con la o.

Ogni mese scegliamo articoli utili ma anche idee fresche e stimolanti. Proprio quello che vorremmo trovare noi in una newsletter!