React Router: Routing Client-Side

17 gennaio 2026
10 min di lettura

Introduzione

React Router permette di aggiungere routing client-side alle applicazioni React, mantenendo il comportamento di single-page application ma supportando URL diversi per sezioni diverse. Questo articolo esplora come funziona React Router, come configurare le route, gestire la navigazione e utilizzare loaders e actions per il data fetching e la submission.

Routing Client-Side

Il routing client-side permette di cambiare il contenuto visualizzato in base all’URL senza ricaricare la pagina. React Router monitora l’URL corrente e renderizza il componente appropriato quando l’URL cambia.

A differenza del routing tradizionale (multi-page), dove ogni path richiede una nuova richiesta HTTP, il routing client-side gestisce tutto nel browser: un’unica pagina HTML viene caricata inizialmente, poi JavaScript gestisce i cambiamenti di URL e il rendering dei componenti.

Configurazione Base

Per utilizzare React Router, si installa il package react-router-dom e si configura il router nell’applicazione.

Creazione del Router

Il router si crea con createBrowserRouter, passando un array di oggetti di configurazione delle route:

import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import HomePage from './pages/Home';
import ProductsPage from './pages/Products';
const router = createBrowserRouter([
{
path: '/',
element: <HomePage />
},
{
path: '/products',
element: <ProductsPage />
}
]);
function App() {
return <RouterProvider router={router} />;
}

Ogni oggetto route ha:

  • path: il percorso URL per cui la route è attiva
  • element: il componente JSX da renderizzare quando la route è attiva

RouterProvider

RouterProvider avvolge l’applicazione e fornisce il router a tutti i componenti figli. Solo i componenti dentro RouterProvider possono utilizzare le funzionalità di React Router.

Definizione route con JSX

Alternativamente, si possono definire le route usando JSX:

import { createRoutesFromElements, Route } from 'react-router-dom';
const routeDefinitions = createRoutesFromElements(
<Route path="/" element={<HomePage />} />
<Route path="/products" element={<ProductsPage />} />
);
const router = createBrowserRouter(routeDefinitions);

Entrambi gli approcci sono validi; la scelta è una questione di preferenza.

Per navigare tra le route, si usa il componente Link invece degli anchor HTML standard. Link previene il comportamento di default del browser (richiesta HTTP) e gestisce la navigazione lato client.

import { Link } from 'react-router-dom';
function Navigation() {
return (
<nav>
<Link to="/">Home</Link>
<Link to="/products">Products</Link>
</nav>
);
}

Link renderizza un anchor HTML ma intercetta i click, previene la richiesta HTTP e aggiorna l’URL e il contenuto via JavaScript.

NavLink estende Link permettendo di evidenziare il link attivo:

import { NavLink } from 'react-router-dom';
function Navigation() {
return (
<nav>
<NavLink
to="/"
className={({ isActive }) => isActive ? 'active' : undefined}
end
>
Home
</NavLink>
<NavLink
to="/products"
className={({ isActive }) => isActive ? 'active' : undefined}
>
Products
</NavLink>
</nav>
);
}

La prop className accetta una funzione che riceve un oggetto con isActive. La prop end fa sì che il link sia attivo solo se l’URL termina esattamente con quel path.

Per navigare programmaticamente (da codice, non da click), si usa l’hook useNavigate:

import { useNavigate } from 'react-router-dom';
function MyComponent() {
const navigate = useNavigate();
const handleClick = () => {
navigate('/products');
};
return <button onClick={handleClick}>Go to Products</button>;
}

Layout Routes

Le layout routes permettono di avvolgere più route con un componente comune (es. navigazione, footer):

const router = createBrowserRouter([
{
path: '/',
element: <RootLayout />,
children: [
{
index: true,
element: <HomePage />
},
{
path: 'products',
element: <ProductsPage />
}
]
}
]);

Il componente layout deve renderizzare <Outlet /> per indicare dove renderizzare le route figlie:

import { Outlet } from 'react-router-dom';
function RootLayout() {
return (
<>
<MainNavigation />
<main>
<Outlet />
</main>
</>
);
}

Le route con index: true sono route di default che si attivano quando il path del parent è attivo.

Route Dinamiche

Le route dinamiche permettono di catturare valori dall’URL usando parametri:

const router = createBrowserRouter([
{
path: '/products',
element: <ProductsPage />
},
{
path: '/products/:productId',
element: <ProductDetailPage />
}
]);

Il : indica un segmento dinamico. Per accedere al valore nel componente:

import { useParams } from 'react-router-dom';
function ProductDetailPage() {
const params = useParams();
const productId = params.productId;
return <div>Product ID: {productId}</div>;
}

useParams restituisce un oggetto con tutti i parametri dinamici della route corrente.

Link con parametri dinamici

Per creare link a route dinamiche:

function ProductsList({ products }) {
return (
<ul>
{products.map(product => (
<li key={product.id}>
<Link to={`/products/${product.id}`}>
{product.title}
</Link>
</li>
))}
</ul>
);
}

Path Relativi e Assoluti

I path possono essere assoluti (iniziano con /) o relativi (non iniziano con /).

I path assoluti sono sempre risolti dalla root del dominio. I path relativi sono risolti rispetto alla route parent o al path corrente, a seconda del contesto.

// Path assoluto: sempre /products
<Link to="/products">Products</Link>
// Path relativo: aggiunto al path corrente
<Link to="products">Products</Link>

La prop relative su Link controlla se il path è relativo alla route definition (route) o al path corrente nell’URL (path).

Gestione Errori

Per gestire errori (route non trovate o errori nei loaders), si aggiunge errorElement alla route:

const router = createBrowserRouter([
{
path: '/',
element: <RootLayout />,
errorElement: <ErrorPage />,
children: [
// ... altre route
]
}
]);

Gli errori “bubbling up”: se una route figlia genera un errore e non ha errorElement, l’errore risale alla route parent più vicina che ha errorElement.

Per accedere ai dettagli dell’errore:

import { useRouteError } from 'react-router-dom';
function ErrorPage() {
const error = useRouteError();
return (
<div>
<h1>An error occurred!</h1>
<p>{error.message}</p>
</div>
);
}

Loaders: Data Fetching

I loaders sono funzioni eseguite da React Router prima di renderizzare una route. Permettono di fetchare dati prima che il componente venga montato.

Definizione di un Loader

pages/Events.js
export async function loader() {
const response = await fetch('http://localhost:8080/events');
if (!response.ok) {
throw new Response(
JSON.stringify({ message: 'Could not fetch events.' }),
{ status: 500 }
);
}
const resData = await response.json();
return resData.events;
}

Il loader viene registrato nella route:

import { eventsLoader } from './pages/Events';
const router = createBrowserRouter([
{
path: '/events',
element: <EventsPage />,
loader: eventsLoader
}
]);

Accesso ai Dati del Loader

Per accedere ai dati restituiti dal loader:

import { useLoaderData } from 'react-router-dom';
function EventsPage() {
const events = useLoaderData();
return (
<ul>
{events.map(event => (
<li key={event.id}>{event.title}</li>
))}
</ul>
);
}

useLoaderData può essere usato nel componente della route o in qualsiasi componente figlio, ma non in route parent.

Parametri nei Loaders

I loaders ricevono un oggetto con request e params:

export async function loader({ params }) {
const response = await fetch(
`http://localhost:8080/events/${params.eventId}`
);
return response;
}
Loaders condivisi

Per condividere un loader tra più route, si usa una route parent senza elemento:

const router = createBrowserRouter([
{
path: '/events/:eventId',
loader: eventLoader,
children: [
{
index: true,
element: <EventDetailPage />
},
{
path: 'edit',
element: <EditEventPage />
}
]
}
]);

Per accedere ai dati da una route parent, si usa useRouteLoaderData con un id:

// Route con id
{
id: 'event-detail',
path: '/events/:eventId',
loader: eventLoader
}
// Nel componente
import { useRouteLoaderData } from 'react-router-dom';
function EditEventPage() {
const event = useRouteLoaderData('event-detail');
// ...
}

Actions: Form Submission

Le actions sono funzioni eseguite quando si invia un form. Permettono di gestire la submission senza gestire manualmente lo state di loading/error.

Definizione di un’Action

pages/NewEvent.js
export async function action({ request }) {
const data = await request.formData();
const eventData = {
title: data.get('title'),
image: data.get('image'),
date: data.get('date'),
description: data.get('description')
};
const response = await fetch('http://localhost:8080/events', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(eventData)
});
if (!response.ok) {
throw new Response(
JSON.stringify({ message: 'Could not save event.' }),
{ status: 500 }
);
}
return redirect('/events');
}

L’action viene registrata nella route:

import { newEventAction } from './pages/NewEvent';
const router = createBrowserRouter([
{
path: '/events/new',
element: <NewEventPage />,
action: newEventAction
}
]);

Utilizzo del Form Component

Per triggerare l’action, si usa il componente Form di React Router:

import { Form } from 'react-router-dom';
function NewEventPage() {
return (
<Form method="post">
<input type="text" name="title" required />
<input type="url" name="image" required />
<input type="date" name="date" required />
<textarea name="description" required />
<button>Save</button>
</Form>
);
}

Form previene il submit di default del browser e invia i dati all’action della route corrente. Gli input devono avere l’attributo name per essere estratti con formData.get().

Redirect dopo Submission

Per reindirizzare dopo una submission riuscita:

import { redirect } from 'react-router-dom';
export async function action({ request }) {
// ... logica di submission
return redirect('/events');
}

Submission Programmatica

Per triggerare un’action programmaticamente (non da form):

import { useSubmit } from 'react-router-dom';
function DeleteButton({ eventId }) {
const submit = useSubmit();
const handleDelete = () => {
if (confirm('Are you sure?')) {
submit(null, { method: 'delete' });
}
};
return <button onClick={handleDelete}>Delete</button>;
}

Il primo argomento di submit sono i dati (o null), il secondo sono le opzioni (method, action, ecc.).

Validazione e Error Handling

Per gestire errori di validazione senza mostrare la pagina di errore:

export async function action({ request }) {
const data = await request.formData();
const response = await fetch('http://localhost:8080/events', {
method: 'POST',
body: JSON.stringify(Object.fromEntries(data))
});
if (response.status === 422) {
// Ritorna la response invece di throw
return response;
}
if (!response.ok) {
throw new Response(
JSON.stringify({ message: 'Could not save event.' }),
{ status: 500 }
);
}
return redirect('/events');
}

Nel componente, si accede ai dati dell’action con useActionData:

import { useActionData } from 'react-router-dom';
function EventForm() {
const data = useActionData();
return (
<Form method="post">
{data && data.errors && (
<ul>
{Object.values(data.errors).map(err => (
<li key={err}>{err}</li>
))}
</ul>
)}
{/* ... campi form */}
</Form>
);
}

useFetcher: Azioni Senza Navigazione

useFetcher permette di triggerare loaders o actions senza navigare a una nuova route:

import { useFetcher } from 'react-router-dom';
function NewsletterSignup() {
const fetcher = useFetcher();
return (
<fetcher.Form action="/newsletter" method="post">
<input type="email" name="email" />
<button disabled={fetcher.state === 'submitting'}>
{fetcher.state === 'submitting' ? 'Submitting...' : 'Sign Up'}
</button>
</fetcher.Form>
);
}

fetcher.Form non causa una navigazione. fetcher.state può essere idle, loading o submitting. fetcher.data contiene i dati restituiti dall’action o loader.

Defer: Data Loading Differita

defer permette di renderizzare una pagina prima che tutti i dati siano caricati, mostrando alcuni contenuti immediatamente e altri quando i dati arrivano.

import { defer } from 'react-router-dom';
async function loadEvents() {
const response = await fetch('http://localhost:8080/events');
const resData = await response.json();
return resData.events;
}
export async function loader() {
return defer({
events: loadEvents()
});
}

Nel componente, si usa Await per gestire i dati differiti:

import { useLoaderData, Await } from 'react-router-dom';
import { Suspense } from 'react';
function EventsPage() {
const { events } = useLoaderData();
return (
<Suspense fallback={<p>Loading...</p>}>
<Await resolve={events}>
{(loadedEvents) => (
<ul>
{loadedEvents.map(event => (
<li key={event.id}>{event.title}</li>
))}
</ul>
)}
</Await>
</Suspense>
);
}

Await riceve una promise e renderizza il contenuto quando la promise si risolve. Suspense mostra il fallback mentre si attende.

Controllo Granulare

Si può decidere quali dati attendere prima della navigazione e quali differire:

export async function loader({ params }) {
return defer({
event: await loadEvent(params.eventId), // Atteso prima della navigazione
events: loadEvents() // Differito, caricato dopo
});
}

Stati di Navigazione

Per mostrare feedback durante le transizioni, si usa useNavigation:

import { useNavigation } from 'react-router-dom';
function RootLayout() {
const navigation = useNavigation();
return (
<>
{navigation.state === 'loading' && <p>Loading...</p>}
<Outlet />
</>
);
}

navigation.state può essere idle, loading o submitting. Per gli stati di submission di un form specifico, si usa useNavigation nel componente del form:

function EventForm() {
const navigation = useNavigation();
const isSubmitting = navigation.state === 'submitting';
return (
<Form method="post">
<button disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Save'}
</button>
</Form>
);
}

React Router gestisce il routing client-side monitorando l’URL e renderizzando i componenti appropriati. Le route si definiscono con createBrowserRouter e si attivano con RouterProvider.

Link e NavLink gestiscono la navigazione senza ricaricare la pagina. Le route dinamiche catturano valori dall’URL con :paramName e si accedono con useParams.

I loaders fetchano dati prima del rendering della route. I dati sono accessibili con useLoaderData. Le actions gestiscono la submission dei form, estraendo i dati con request.formData().

useFetcher permette di triggerare loaders/actions senza navigare. defer permette di renderizzare contenuti parziali mentre alcuni dati sono ancora in caricamento.

Gli errori si gestiscono con errorElement e useRouteError. Gli stati di navigazione si monitorano con useNavigation per mostrare feedback all’utente.

Continua la lettura

Leggi il prossimo capitolo: "Autenticazione in React"

Continua a leggere