MudBlazor: i pattern che rompono il rendering (RenderMode, cascading parameters, drawer)

MudBlazor: i pattern che rompono il rendering (RenderMode, cascading parameters, drawer)

I problemi più frequenti tra MudBlazor e i render mode di Blazor Web App non sono bug di MudBlazor in sé, ma conseguenze di come Blazor Web App gestisce i confini tra rendering statico lato server e circuito interattivo — un concetto che semplicemente non esisteva prima dell'introduzione dei render mode. Quattro pattern in particolare causano la maggior parte dei problemi osservati in progetti reali: @rendermode applicato ai Layout, cascading parameters che non attraversano i confini di render mode, l'ordine di <Routes /> e <MudProviders />, e la navigazione enhanced che serve listener registrati in due punti diversi.

@rendermode non va applicato direttamente ai componenti Layout

Un errore facile da fare per chi struttura un progetto Blazor Web App pensando ancora in termini di Blazor Server "classico": applicare @rendermode InteractiveServer direttamente su un componente Layout (MainLayout.razor o simili).

@* Sbagliato — causa errore di serializzazione RenderFragment *@
@rendermode InteractiveServer

<MudLayout>
    ...
</MudLayout>

Il motivo tecnico: un Layout riceve il proprio contenuto tramite un RenderFragment (il @Body), e i RenderFragment non sono serializzabili tra i confini di render mode diversi — è esattamente il meccanismo che permette il rendering ibrido di Blazor Web App, ma che si rompe se si prova a forzare un render mode specifico sul contenitore che ospita quel fragment. Il pattern corretto è impostare il render mode a livello di componente/pagina figlia, o globalmente in App.razor sul componente radice (<Routes @rendermode="InteractiveServer" />), non sul Layout stesso.

Cascading parameters MudBlazor non attraversano i confini di render mode

MudDrawer e MudLayout comunicano tra loro tramite cascading parameters — funziona perfettamente finché entrambi vivono nello stesso render mode. Il problema emerge in scenari cross-rendermode (es. una pagina con @rendermode diverso da quello del Layout che la ospita, o un mix di pagine statiche e interattive nello stesso progetto): il cascading parameter semplicemente non arriva, e il drawer smette di rispondere ai toggle programmatici senza generare un errore esplicito — il sintomo è "il bottone hamburger non fa nulla", non un'eccezione in console.

La soluzione più affidabile trovata in pratica non è cercare di forzare la comunicazione Blazor tra i due, ma uscire dal circuito per quella specifica interazione: gestire l'apertura/chiusura del drawer con CSS puro (una classe che ne controlla la visibilità) e un toggle in JavaScript vanilla, invece che tramite lo stato interno di MudDrawer/MudLayout.

// Toggle drawer in JS vanilla, indipendente dal circuito Blazor e dai suoi render mode
document.getElementById('nav-toggle')?.addEventListener('click', () => {
    document.getElementById('nav-drawer')?.classList.toggle('nav-drawer--open');
});

Non è "meno elegante" per pigrizia: è deliberatamente fuori dal circuito Blazor perché quel confine è esattamente il punto che si rompe in scenari cross-rendermode — un problema di piattaforma, non qualcosa che si risolve scrivendo il binding in modo diverso.

L'ordine di e in App.razor conta

Un dettaglio facile da sottovalutare: l'ordine con cui <Routes /> e <MudProviders /> compaiono in App.razor non è indifferente. Un ordine sbagliato causa una race condition sul MudPopoverProvider (il componente che gestisce popup, menu, dialog) che si manifesta in modo particolarmente subdolo: funziona al primo caricamento, ma crasha il circuito su un hard reload (F5) di una pagina già interattiva.

Il motivo è che MudProviders deve essere pronto a ricevere popover/dialog richiesti dai componenti dentro <Routes /> — se l'ordine di dichiarazione fa sì che Routes provi a montare componenti che dipendono da provider non ancora inizializzati, il primo render "vince" per timing favorevole ma un hard reload (che riparte da zero il ciclo di inizializzazione) espone la race condition. La correzione è puramente nell'ordine di dichiarazione dei due componenti in App.razor — nessuna configurazione aggiuntiva richiesta, ma va verificato esplicitamente con un hard reload durante lo sviluppo, non solo con la normale navigazione interna che non lo fa emergere.

Navigazione enhanced: due punti di registrazione, non uno

Blazor Web App introduce la "enhanced navigation" — un meccanismo che intercetta i link interni ed evita un reload completo della pagina, aggiornando solo il contenuto necessario. Per qualunque script JS che deve reagire al caricamento della pagina (inizializzazioni di componenti custom, come nel caso dell'editor Markdown/Quill visto in altri articoli di questa serie), un solo listener su DOMContentLoaded non è sufficiente: quell'evento scatta solo al primo caricamento reale della pagina, non alle navigazioni successive gestite dall'enhanced navigation.

function initCustomWidgets() {
    // logica di inizializzazione
}

document.addEventListener('DOMContentLoaded', initCustomWidgets);
document.addEventListener('enhancedload', initCustomWidgets);

Senza il secondo listener, uno script che funziona perfettamente al primo caricamento smette di funzionare dopo la prima navigazione interna — un sintomo che spesso viene scambiato per un bug del componente stesso, quando in realtà il componente non viene proprio più inizializzato.

Domande frequenti

Questi problemi esistono anche in Blazor Server "puro" (non Blazor Web App con render mode misti)? Il problema del cascading parameter cross-rendermode no, perché in un'app Blazor Server pura non ci sono confini di render mode diversi da attraversare. Gli altri tre (rendermode sui Layout, ordine Routes/MudProviders, enhanced navigation) dipendono dal modello Blazor Web App introdotto più di recente — un progetto Blazor Server "classico" precedente a quel modello non li incontra nella stessa forma.

Come faccio a capire se un bug è dovuto a un confine di render mode invece che a un errore normale nel mio codice? Un indizio tipico: il comportamento funziona in un contesto (es. prima navigazione, un solo render mode nella pagina) e smette di funzionare in un altro (hard reload, pagina con render mode misti) senza che il codice del componente sia cambiato — quella incoerenza è il segnale più affidabile di un problema di confine, non di logica applicativa.

Vale la pena evitare del tutto il mix di render mode diversi nella stessa app per semplicità? Dipende dal progetto. Il mix ha vantaggi reali (pagine statiche pubbliche più leggere, pagine autenticate pienamente interattive), ma richiede di conoscere questi limiti in anticipo. Se il progetto è piccolo e non ha bisogno di quella differenziazione, un render mode unico per tutta l'app evita categoricamente questa classe di problemi.