Errori e fix

React Router: navigare da codice senza rompere il router

Il modo corretto dipende da dove ti trovi: componente, loader o modulo esterno. `useNavigate` in un componente, `redirect` in un loader, un'istanza del router fuori da React.

In sintesi

  • Dentro un componente usa `useNavigate` e chiamalo in un evento o in un effetto, mai durante il render.
  • In un loader o in un'action usa `redirect`, che è dati e non un effetto collaterale.
  • Fuori da React serve l'istanza creata con `createBrowserRouter`: `router.navigate()` è disponibile anche in un interceptor.

Risposta breve: dentro un componente usa useNavigate() e chiamalo in un evento o in un effetto; in un loader usa redirect(); fuori da React usa l'istanza del router creata con createBrowserRouter. Mai navigate() durante il render.

Il meccanismo: la navigazione è uno stato, non una chiamata

Il router tiene una location e un insieme di sottoscrittori. Chiamare navigate non cambia la pagina: propone una nuova location, il router la confronta con quella corrente, aggiorna la cronologia e notifica i componenti che dipendono da quel contesto. Il render arriva dopo. Per questo una chiamata durante il render è un errore: stai chiedendo al router di notificare un componente mentre quel componente si sta ancora calcolando. React lo segnala come aggiornamento di un altro componente durante il render, e in Strict Mode il doppio render raddoppia la richiesta.

Un passo diagnostico: guarda la location, non lo schermo

Quando «non naviga», la prima domanda è se la location è cambiata. Il frammento qui sotto si sottoscrive al router e stampa ogni transizione: se la path cambia e lo schermo no, il problema è nel rendering della rotta; se la path non cambia, la chiamata non è mai partita o è stata annullata. Ricordati di annullare la sottoscrizione, altrimenti in sviluppo ne accumuli una per ogni hot reload.

// la navigazione è uno stato: guarda cosa cambia davvero
const unsubscribe = router.subscribe((state) => {
  console.log(state.navigation.state, state.location.pathname, state.location.key);
});
// chiama unsubscribe() quando smonti il modulo

Dentro un componente

import { useNavigate } from "react-router";

export function SaveButton() {
  const navigate = useNavigate();
  async function onSave() {
    await save();
    navigate("/dashboard", { replace: true });
  }
  return Salva;
}

replace: true sostituisce la voce nella cronologia: dopo un salvataggio il tasto Indietro non deve riportare al form compilato. È una scelta di prodotto, non un dettaglio tecnico.

Nei loader e nelle action

import { redirect } from "react-router";

export async function loader({ request }: { request: Request }) {
  const user = await getUser(request);
  if (!user) throw redirect("/login");
  return { user };
}

redirect è un valore di ritorno, non un effetto: interrompe il flusso e il router gestisce la transizione. È la forma corretta quando la decisione dipende da dati caricati prima del render.

Fuori da React

// router.ts
export const router = createBrowserRouter(routes);

// http.ts
import { router } from "./router";
http.interceptors.response.use(undefined, (error) => {
  if (error.response?.status === 401) router.navigate("/login");
  return Promise.reject(error);
});

Gli hook non esistono fuori da un componente: useNavigate in un modulo di rete lancia un errore. L'istanza del router è l'unico punto stabile.

Quando la correzione non funziona

Se la navigazione avviene ma la pagina non cambia, il problema è il componente che resta montato: due rotte che renderizzano lo stesso elemento con la stessa key. Se invece ottieni un loop, stai chiamando navigate in un effetto senza dipendenze stabili: la nuova location cambia le dipendenze e l'effetto riparte. In quel caso la navigazione appartiene al gestore dell'evento, non all'effetto.

Quando la correzione comune peggiora le cose

La prima correzione che si incontra è replace: true, utile dopo un salvataggio e dannosa se diventa un'abitudine. Sostituire la voce di cronologia significa che il tasto Indietro salta la schermata precedente: chi voleva tornare al form finisce due passi più indietro, e il passo intermedio non esiste più per nessuno. Anche la misurazione ne risente, perché una pagina che non entra nella cronologia non produce un pageview separato e le funnel sembrano più corte di quanto siano. replace è una decisione di prodotto da prendere caso per caso, non un modo per togliere il form compilato dalla cronologia.

Un limite con un nome: nessuna istanza nel router dichiarativo

Il limite ha un nome tecnico: router dichiarativo. Con <BrowserRouter> la cronologia vive dentro il componente, quindi un interceptor HTTP o un client generato non hanno nulla da chiamare, e l'istanza citata sopra non esiste. La soluzione non è un trucco con la History API: porta l'evento nel componente, con uno stato condiviso che l'interceptor aggiorna e un effetto che lo legge. Chi passa a React Router 7 in modalità framework trova l'istanza già pronta e non incontra il problema.

Come prevenirlo

  • Regola unica: la navigazione è una conseguenza di un'azione, non un calcolo del render.
  • Preferisci i loader per le decisioni che dipendono dai dati.
  • Nei test, verifica la cronologia: dopo il salvataggio Indietro non deve tornare al form.

La cronologia fa parte del comportamento, quindi va testata come il comportamento: dopo il salvataggio, Indietro non deve mostrare il form; dopo un 401, la rotta protetta non deve restare raggiungibile con Indietro. Due asserzioni coprono i due bug che questa pagina descrive.

Dove continuare

Per costruire le schermate: dashboard con shell, componenti e blocchi con preview. Le guide: struttura App Router e copy-paste o headless; il confronto: alternative a MUI misurate.

Domande frequenti

Perché `navigate()` durante il render causa un loop?

La navigazione cambia la location, che rirenderizza il componente, che naviga di nuovo. Va chiamata in un gestore di evento o dentro `useEffect` con dipendenze stabili.

Come navigo dopo una chiamata API?

Nel componente, attendi la promise e poi chiama `navigate`. Se la chiamata è in un layer condiviso, fai restituire il risultato e naviga nel componente.

`redirect` e `navigate` sono equivalenti?

No. `redirect` viene lanciato in un loader o in un'action e interrompe il flusso; `navigate` è un comando imperativo dal client.

Funziona con React Router 7 in modalità framework?

Sì, con la stessa distinzione: `useNavigate` nei componenti, `redirect` nei loader. In modalità framework i loader sono la strada preferita.

Come navigo da un interceptor axios?

Esporta l'istanza del router creata con `createBrowserRouter` e chiama `router.navigate('/login')`. Non usare hook fuori da un componente.