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.