Italiano

Sfrutta la potenza dell'App Router di Next.js con la nostra guida approfondita al routing basato su file. Impara a strutturare la tua applicazione, creare route dinamiche, gestire layout e altro ancora.

App Router di Next.js: una guida completa al routing basato su file

L'App Router di Next.js, introdotto in Next.js 13 e diventato lo standard nelle versioni successive, rivoluziona il modo in cui strutturiamo e navighiamo nelle applicazioni. Introduce un sistema di routing potente e intuitivo basato su file che semplifica lo sviluppo, migliora le prestazioni e ottimizza l'esperienza complessiva dello sviluppatore. Questa guida completa approfondirà il routing basato su file dell'App Router, fornendoti le conoscenze e le competenze per creare applicazioni Next.js robuste e scalabili.

Cos'è il routing basato su file?

Il routing basato su file è un sistema in cui la struttura delle route della tua applicazione è determinata direttamente dall'organizzazione dei tuoi file e delle tue directory. Nell'App Router di Next.js, si definiscono le route creando file all'interno della directory `app`. Ogni cartella rappresenta un segmento di route e file speciali all'interno di quelle cartelle definiscono come quel segmento di route sarà gestito. Questo approccio offre diversi vantaggi:

Iniziare con l'App Router

Per utilizzare l'App Router, è necessario creare un nuovo progetto Next.js o migrare un progetto esistente. Assicurati di utilizzare la versione 13 o successiva di Next.js.

Creazione di un nuovo progetto:

Puoi creare un nuovo progetto Next.js con l'App Router utilizzando il seguente comando:

npx create-next-app@latest my-app --example with-app

Migrazione di un progetto esistente:

Per migrare un progetto esistente, devi spostare le tue pagine dalla directory `pages` alla directory `app`. Potrebbe essere necessario adattare di conseguenza la logica di routing. Next.js fornisce una guida alla migrazione per aiutarti in questo processo.

Concetti fondamentali del routing basato su file

L'App Router introduce diversi file e convenzioni speciali che definiscono come vengono gestite le tue route:

1. La directory `app`

La directory `app` è la radice delle route della tua applicazione. Tutti i file e le cartelle all'interno di questa directory verranno utilizzati per generare le route. Qualsiasi cosa al di fuori della directory `app` (come la directory `pages` se stai migrando) sarà ignorata dall'App Router.

2. Il file `page.js`

Il file `page.js` (o `page.jsx`, `page.ts`, `page.tsx`) è la parte più fondamentale dell'App Router. Definisce il componente UI che verrà renderizzato per un segmento di route specifico. È un file richiesto per qualsiasi segmento di route che vuoi rendere direttamente accessibile.

Esempio:

Se hai una struttura di file come questa:

app/
  about/
    page.js

Il componente esportato da `app/about/page.js` verrà renderizzato quando un utente naviga su `/about`.

// app/about/page.js
import React from 'react';

export default function AboutPage() {
  return (
    <div>
      <h1>Chi siamo</h1>
      <p>Scopri di più sulla nostra azienda.</p>
    </div>
  );
}

3. Il file `layout.js`

Il file `layout.js` (o `layout.jsx`, `layout.ts`, `layout.tsx`) definisce un'interfaccia utente condivisa tra più pagine all'interno di un segmento di route. I layout sono utili per creare intestazioni, piè di pagina, barre laterali e altri elementi coerenti che dovrebbero essere presenti su più pagine.

Esempio:

Supponiamo di voler aggiungere un'intestazione sia alla pagina `/about` che a un'ipotetica pagina `/about/team`. Puoi creare un file `layout.js` nella directory `app/about`:

// app/about/layout.js
import React from 'react';

export default function AboutLayout({ children }) {
  return (
    <div>
      <header>
        <h1>Sulla nostra azienda</h1>
      </header>
      <main>{children}</main>
    </div>
  );
}

La prop `children` sarà sostituita con l'UI renderizzata dal file `page.js` nella stessa directory o in qualsiasi directory nidificata.

4. Il file `template.js`

Il file `template.js` è simile a `layout.js`, ma crea una nuova istanza del componente per ogni route figlia. Ciò è utile per scenari in cui si desidera mantenere lo stato del componente o impedire nuovi rendering durante la navigazione tra le route figlie. A differenza dei layout, i template verranno ri-renderizzati alla navigazione. L'uso dei template è ottimo per animare elementi durante la navigazione.

Esempio:

// app/template.js
'use client'

import { useState } from 'react'

export default function Template({ children }) {
  const [count, setCount] = useState(0)

  return (
    <main>
      <p>Template: {count}</p>
      <button onClick={() => setCount(count + 1)}>Aggiorna Template</button>
      {children}
    </main>
  )
}

5. Il file `loading.js`

Il file `loading.js` (o `loading.jsx`, `loading.ts`, `loading.tsx`) consente di creare un'interfaccia utente di caricamento che viene visualizzata mentre un segmento di route si sta caricando. Questo è utile per fornire una migliore esperienza utente durante il recupero di dati o l'esecuzione di altre operazioni asincrone.

Esempio:

// app/about/loading.js
import React from 'react';

export default function Loading() {
  return <p>Caricamento informazioni "chi siamo"...</p>;
}

Quando un utente naviga su `/about`, il componente `Loading` verrà visualizzato fino a quando il componente `page.js` non sarà completamente renderizzato.

6. Il file `error.js`

Il file `error.js` (o `error.jsx`, `error.ts`, `error.tsx`) consente di creare un'interfaccia utente di errore personalizzata che viene visualizzata quando si verifica un errore all'interno di un segmento di route. Questo è utile per fornire un messaggio di errore più user-friendly e impedire che l'intera applicazione si blocchi.

Esempio:

// app/about/error.js
'use client'

import React from 'react';

export default function Error({ error, reset }) {
  return (
    <div>
      <h2>Si è verificato un errore!</h2>
      <p>{error.message}</p>
      <button onClick={() => reset()}>Riprova</button>
    </div>
  );
}

Se si verifica un errore durante il rendering della pagina `/about`, verrà visualizzato il componente `Error`. La prop `error` contiene informazioni sull'errore e la funzione `reset` consente all'utente di tentare di ricaricare la pagina.

7. Gruppi di Route

I Gruppi di Route `(nomeGruppo)` consentono di organizzare le route senza influire sulla struttura dell'URL. Si creano racchiudendo il nome di una cartella tra parentesi. Questo è particolarmente utile per organizzare layout e componenti condivisi.

Esempio:

app/
  (marketing)/
    about/
      page.js
    contact/
      page.js
  (shop)/
    products/
      page.js

In questo esempio, le pagine `about` e `contact` sono raggruppate nel gruppo `marketing`, e la pagina `products` è nel gruppo `shop`. Gli URL rimangono rispettivamente `/about`, `/contact` e `/products`.

8. Route Dinamiche

Le route dinamiche consentono di creare route con segmenti variabili. Questo è utile per visualizzare contenuti basati su dati recuperati da un database o da un'API. I segmenti di route dinamici sono definiti racchiudendo il nome del segmento tra parentesi quadre (es. `[id]`)

Esempio:

Supponiamo di voler creare una route per visualizzare singoli post di un blog in base al loro ID. Puoi creare una struttura di file come questa:

app/
  blog/
    [id]/
      page.js

Il segmento `[id]` è un segmento dinamico. Il componente esportato da `app/blog/[id]/page.js` verrà renderizzato quando un utente naviga verso un URL come `/blog/123` o `/blog/456`. Il valore del parametro `id` sarà disponibile nella prop `params` del componente.

// app/blog/[id]/page.js
import React from 'react';

export default async function BlogPost({ params }) {
  const { id } = params;

  // Recupera i dati per il post del blog con l'ID fornito
  const post = await fetchBlogPost(id);

  if (!post) {
    return <p>Post del blog non trovato.</p>;
  }

  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </div>
  );
}

async function fetchBlogPost(id) {
  // Simula il recupero dei dati da un database o API
  return new Promise((resolve) => {
    setTimeout(() => {
      const posts = {
        '123': { title: 'Il mio primo post sul blog', content: 'Questo è il contenuto del mio primo post sul blog.' },
        '456': { title: 'Un altro post sul blog', content: 'Questo è un altro contenuto entusiasmante.' },
      };
      resolve(posts[id] || null);
    }, 500);
  });
}

È anche possibile utilizzare più segmenti dinamici in una route. Ad esempio, potresti avere una route come `/blog/[category]/[id]`.

9. Segmenti Catch-all

I segmenti catch-all consentono di creare route che corrispondono a un numero qualsiasi di segmenti. Questo è utile per scenari come la creazione di un CMS in cui la struttura dell'URL è determinata dall'utente. I segmenti catch-all sono definiti aggiungendo tre punti prima del nome del segmento (es. `[...slug]`)

Esempio:

app/
  docs/
    [...slug]/
      page.js

Il segmento `[...slug]` corrisponderà a qualsiasi numero di segmenti dopo `/docs`. Ad esempio, corrisponderà a `/docs/getting-started`, `/docs/api/users` e `/docs/advanced/configuration`. Il valore del parametro `slug` sarà un array contenente i segmenti corrispondenti.

// app/docs/[...slug]/page.js
import React from 'react';

export default function DocsPage({ params }) {
  const { slug } = params;

  return (
    <div>
      <h1>Documentazione</h1>
      <p>Slug: {slug ? slug.join('/') : 'Nessuno slug'}</p>
    </div>
  );
}

I segmenti catch-all opzionali possono essere creati aggiungendo il nome del segmento tra doppie parentesi quadre `[[...slug]]`. Ciò rende il segmento di route opzionale. Esempio:

app/
  blog/
    [[...slug]]/
      page.js

Questa configurazione renderizzerà il componente page.js sia in `/blog` che in `/blog/any/number/of/segments`.

10. Route Parallele

Le Route Parallele consentono di renderizzare simultaneamente una o più pagine nello stesso layout. Questo è particolarmente utile per layout complessi, come le dashboard, dove diverse sezioni della pagina possono essere caricate in modo indipendente. Le Route Parallele sono definite usando il simbolo `@` seguito dal nome di uno slot (es. `@sidebar`, `@main`).

Esempio:

app/
  @sidebar/
    page.js  // Contenuto per la barra laterale
  @main/
    page.js  // Contenuto per la sezione principale
  default.js // Richiesto: Definisce il layout predefinito per le route parallele

Il file `default.js` è richiesto quando si utilizzano le route parallele. Definisce come i diversi slot vengono combinati per creare il layout finale.

// app/default.js
export default function RootLayout({ children: { sidebar, main } }) {
  return (
    <div style={{ display: 'flex' }}>
      <aside style={{ width: '200px', backgroundColor: '#f0f0f0' }}>
        {sidebar}
      </aside>
      <main style={{ flex: 1, padding: '20px' }}>
        {main}
      </main>
    </div>
  );
}

11. Route Intercettanti

Le Route Intercettanti consentono di caricare una route da una parte diversa della tua applicazione all'interno del layout corrente. Questo è utile per creare modali, gallerie di immagini e altri elementi dell'interfaccia utente che dovrebbero apparire sopra il contenuto della pagina esistente. Le Route Intercettanti sono definite utilizzando la sintassi `(..)`, che indica di quanti livelli risalire nell'albero delle directory per trovare la route intercettata.

Esempio:

app/
  (.)photos/
    [id]/
      page.js  // La route intercettata
  feed/
    page.js  // La pagina dove viene visualizzato il modale della foto

In questo esempio, quando un utente clicca su una foto nella pagina `/feed`, la route `app/(.)photos/[id]/page.js` viene intercettata e visualizzata come un modale sopra la pagina `/feed`. La sintassi `(.)` dice a Next.js di cercare un livello più in su (nella directory `app`) per trovare la route `photos/[id]`.

Recupero Dati con l'App Router

L'App Router fornisce supporto integrato per il recupero dei dati utilizzando Server Components e Client Components. I Server Components sono renderizzati sul server, mentre i Client Components sono renderizzati sul client. Ciò consente di scegliere l'approccio migliore per ogni componente in base ai suoi requisiti.

Server Components

I Server Components sono l'impostazione predefinita nell'App Router. Ti consentono di recuperare i dati direttamente nei tuoi componenti senza la necessità di route API separate. Ciò può migliorare le prestazioni e semplificare il codice.

Esempio:

// app/products/page.js
import React from 'react';

export default async function ProductsPage() {
  const products = await fetchProducts();

  return (
    <div>
      <h1>Prodotti</h1>
      <ul>
        {products.map((product) => (
          <li key={product.id}>{product.name}</li>
        ))}
      </ul>
    </div>
  );
}

async function fetchProducts() {
  // Simula il recupero dei dati da un database o API
  return new Promise((resolve) => {
    setTimeout(() => {
      const products = [
        { id: 1, name: 'Prodotto A' },
        { id: 2, name: 'Prodotto B' },
        { id: 3, name: 'Prodotto C' },
      ];
      resolve(products);
    }, 500);
  });
}

In questo esempio, la funzione `fetchProducts` viene chiamata direttamente all'interno del componente `ProductsPage`. Il componente viene renderizzato sul server e i dati vengono recuperati prima che l'HTML venga inviato al client.

Client Components

I Client Components vengono renderizzati sul client e consentono di utilizzare funzionalità lato client come listener di eventi, stato e API del browser. Per utilizzare un Client Component, è necessario aggiungere la direttiva `'use client'` all'inizio del file.

Esempio:

// app/counter/page.js
'use client'

import React, { useState } from 'react';

export default function CounterPage() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>Contatore</h1>
      <p>Conteggio: {count}</p>
      <button onClick={() => setCount(count + 1)}>Incrementa</button>
    </div>
  );
}

In questo esempio, il componente `CounterPage` è un Client Component perché utilizza l'hook `useState`. La direttiva `'use client'` dice a Next.js di renderizzare questo componente sul client.

Tecniche di Routing Avanzate

L'App Router offre diverse tecniche di routing avanzate che possono essere utilizzate per creare applicazioni complesse e sofisticate.

1. Route Handlers

I Route Handlers consentono di creare endpoint API all'interno della directory `app`. Ciò elimina la necessità di una directory `pages/api` separata. I Route Handlers sono definiti in file chiamati `route.js` (o `route.ts`) ed esportano funzioni che gestiscono diversi metodi HTTP (es. `GET`, `POST`, `PUT`, `DELETE`).

Esempio:

// app/api/users/route.js
import { NextResponse } from 'next/server'

export async function GET(request) {
  // Simula il recupero degli utenti da un database
  const users = [
    { id: 1, name: 'John Doe' },
    { id: 2, name: 'Jane Doe' },
  ];

  return NextResponse.json(users);
}

export async function POST(request) {
  const body = await request.json()
  console.log('Dati ricevuti:', body)
  return NextResponse.json({ message: 'Utente creato' }, { status: 201 })
}

Questo esempio definisce un route handler in `/api/users` che gestisce sia le richieste `GET` che `POST`. La funzione `GET` restituisce un elenco di utenti e la funzione `POST` crea un nuovo utente.

2. Gruppi di Route con Layout Multipli

È possibile combinare gruppi di route con layout per creare layout diversi per diverse sezioni della propria applicazione. Questo è utile per scenari in cui si desidera avere un'intestazione o una barra laterale diversa per diverse parti del sito.

Esempio:

app/
  (marketing)/
    layout.js  // Layout marketing
    about/
      page.js
    contact/
      page.js
  (admin)/
    layout.js  // Layout admin
    dashboard/
      page.js

In questo esempio, le pagine `about` e `contact` utilizzeranno il layout `marketing`, mentre la pagina `dashboard` utilizzerà il layout `admin`.

3. Middleware

Il Middleware consente di eseguire codice prima che una richiesta venga gestita dalla tua applicazione. Questo è utile per attività come l'autenticazione, l'autorizzazione, il logging e il reindirizzamento degli utenti in base alla loro posizione o dispositivo.

Il Middleware è definito in un file chiamato `middleware.js` (o `middleware.ts`) alla radice del tuo progetto.

Esempio:

// middleware.js
import { NextResponse } from 'next/server'

export function middleware(request) {
  // Controlla se l'utente è autenticato
  const isAuthenticated = false; // Sostituisci con la tua logica di autenticazione

  if (!isAuthenticated && request.nextUrl.pathname.startsWith('/admin')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();
}

// Vedi "Percorsi corrispondenti" di seguito per saperne di più
export const config = {
  matcher: '/admin/:path*',
}

Questo esempio definisce un middleware che controlla se l'utente è autenticato prima di consentirgli di accedere a qualsiasi route sotto `/admin`. Se l'utente non è autenticato, viene reindirizzato alla pagina `/login`.

Migliori Pratiche per il Routing Basato su File

Per sfruttare al meglio il sistema di routing basato su file dell'App Router, considera le seguenti migliori pratiche:

Esempi di Internazionalizzazione con l'App Router di Next.js

L'App Router di Next.js semplifica l'internazionalizzazione (i18n) attraverso il routing basato su file. Ecco come puoi implementare efficacemente l'i18n:

1. Routing con Sotto-percorsi

Organizza le tue route in base alla locale utilizzando i sotto-percorsi. Per esempio:

app/
  [locale]/
    page.tsx         // Pagina principale per la locale
    about/
      page.tsx     // Pagina "Chi siamo" per la locale
// app/[locale]/page.tsx
import { getTranslations } from './dictionaries';

export default async function HomePage({ params: { locale } }) {
  const t = await getTranslations(locale);
  return (<h1>{t.home.title}</h1>);
}

// dictionaries.js
const dictionaries = {
  en: () => import('./dictionaries/en.json').then((module) => module.default),
  es: () => import('./dictionaries/es.json').then((module) => module.default),
};

export const getTranslations = async (locale) => {
  try {
    return dictionaries[locale]() ?? dictionaries.en();
  } catch (error) {
    console.error(`Impossibile caricare le traduzioni per la locale ${locale}`, error);
    return dictionaries.en();
  }
};

In questa configurazione, il segmento di route dinamico `[locale]` gestisce diverse locali (es. `/en`, `/es`). Le traduzioni vengono caricate dinamicamente in base alla locale.

2. Domain Routing

Per un approccio più avanzato, è possibile utilizzare domini o sottodomini diversi per ogni locale. Questo spesso comporta una configurazione aggiuntiva con il tuo provider di hosting.

3. Middleware per il Rilevamento della Locale

Usa il middleware per rilevare automaticamente la locale preferita dell'utente e reindirizzarlo di conseguenza.

// middleware.js
import { NextResponse } from 'next/server';
import { match } from '@formatjs/intl-localematcher';
import Negotiator from 'negotiator';

let locales = ['en', 'es', 'fr'];

function getLocale(request) {
  const negotiatorHeaders = {};
  request.headers.forEach((value, key) => (negotiatorHeaders[key] = value));
  let languages = new Negotiator({ headers: negotiatorHeaders }).languages();

  try {
      return match(languages, locales, 'en'); // Usa "en" come locale predefinita
  } catch (error) {
      console.error("Errore nella corrispondenza della locale:", error);
      return 'en'; // Ritorna all'inglese se la corrispondenza fallisce
  }
}

export function middleware(request) {
  const pathname = request.nextUrl.pathname;
  const pathnameIsMissingLocale = locales.every(
    (locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
  );

  if (pathnameIsMissingLocale) {
    const locale = getLocale(request);

    return NextResponse.redirect(
      new URL(
        `/${locale}${pathname.startsWith('/') ? '' : '/'}${pathname}`,
        request.url
      )
    );
  }
}

export const config = {
  matcher: [
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
};

Questo middleware controlla se il percorso richiesto ha un prefisso di locale. In caso contrario, rileva la locale preferita dell'utente utilizzando l'header `Accept-Language` e lo reindirizza al percorso specifico della locale appropriato. Librerie come `@formatjs/intl-localematcher` e `negotiator` vengono utilizzate per gestire la negoziazione della locale.

L'App Router di Next.js e l'Accessibilità Globale

La creazione di applicazioni web accessibili a livello globale richiede un'attenta considerazione dei principi di accessibilità (a11y). L'App Router di Next.js fornisce una solida base per la creazione di esperienze accessibili, ma è essenziale implementare le migliori pratiche per garantire che la tua applicazione sia utilizzabile da tutti, indipendentemente dalle loro abilità.

Considerazioni chiave sull'Accessibilità

  1. HTML Semantico: Usa elementi HTML semantici (es. `<article>`, `<nav>`, `<aside>`, `<main>`) per strutturare i tuoi contenuti. Questo fornisce significato alle tecnologie assistive e aiuta gli utenti a navigare più facilmente nel tuo sito.
  2. Attributi ARIA: Usa gli attributi ARIA (Accessible Rich Internet Applications) per migliorare l'accessibilità dei componenti e widget personalizzati. Gli attributi ARIA forniscono informazioni aggiuntive sul ruolo, lo stato e le proprietà degli elementi alle tecnologie assistive.
  3. Navigazione da Tastiera: Assicurati che tutti gli elementi interattivi siano accessibili tramite tastiera. Gli utenti dovrebbero essere in grado di navigare attraverso la tua applicazione utilizzando il tasto `Tab` e interagire con gli elementi usando i tasti `Invio` o `Spazio`.
  4. Contrasto dei Colori: Usa un contrasto di colore sufficiente tra testo e sfondo per garantire la leggibilità per gli utenti con disabilità visive. Le Linee Guida per l'Accessibilità dei Contenuti Web (WCAG) raccomandano un rapporto di contrasto di almeno 4.5:1 per il testo normale e 3:1 per il testo grande.
  5. Testo Alternativo per le Immagini: Fornisci un testo alternativo descrittivo per tutte le immagini. Il testo alternativo fornisce un'alternativa testuale per le immagini che può essere letta dagli screen reader.
  6. Etichette dei Moduli: Associa le etichette dei moduli ai campi di input corrispondenti utilizzando l'elemento `<label>`. Questo rende chiaro agli utenti quali informazioni sono richieste in ogni campo.
  7. Test con Screen Reader: Testa la tua applicazione con uno screen reader per assicurarti che sia accessibile agli utenti con disabilità visive. Screen reader popolari includono NVDA, JAWS e VoiceOver.

Implementare l'Accessibilità nell'App Router di Next.js

  1. Usa il Componente Link di Next.js: Usa il componente `<Link>` per la navigazione. Fornisce funzionalità di accessibilità integrate, come il prefetching e la gestione del focus.
  2. Gestione del Focus: Quando si naviga tra le pagine o si aprono modali, assicurarsi che il focus sia gestito correttamente. Il focus dovrebbe essere impostato sull'elemento più logico della nuova pagina o del modale.
  3. Componenti Personalizzati Accessibili: Quando crei componenti personalizzati, assicurati che siano accessibili seguendo i principi sopra descritti. Usa HTML semantico, attributi ARIA e navigazione da tastiera per rendere i tuoi componenti utilizzabili da tutti.
  4. Linting e Test: Usa strumenti di linting come ESLint con plugin per l'accessibilità per identificare potenziali problemi di accessibilità nel tuo codice. Inoltre, usa strumenti di test automatizzati per testare la tua applicazione per violazioni di accessibilità.

Conclusione

Il sistema di routing basato su file dell'App Router di Next.js offre un modo potente e intuitivo per strutturare e navigare le tue applicazioni. Comprendendo i concetti fondamentali e le migliori pratiche delineate in questa guida, puoi creare applicazioni Next.js robuste, scalabili e manutenibili. Sperimenta con le diverse funzionalità dell'App Router e scopri come può semplificare il tuo flusso di lavoro di sviluppo e migliorare l'esperienza utente.