Gestione dello stato in Vue 3 per pannelli admin: Pinia vs stato locale
Gestione dello stato in Vue 3 per pannelli amministrativi: scegli tra Pinia, provide/inject e stato locale con esempi reali come filtri, bozze e schede.

Cosa rende complesso lo stato nei pannelli admin
I pannelli amministrativi sembrano pesanti di stato perché mettono molti elementi in movimento nella stessa schermata. Una tabella non è solo dati: include ordinamento, filtri, paginazione, righe selezionate e il contesto “cosa è successo?” su cui gli utenti fanno affidamento. Aggiungi form lunghi, permessi basati sui ruoli e azioni che cambiano ciò che l'interfaccia deve permettere, e le piccole decisioni sullo stato iniziano a contare.
La sfida non è memorizzare valori. È mantenere il comportamento prevedibile quando diversi componenti hanno bisogno della stessa verità. Se un filtro mostra “Attivi”, la tabella, l'URL e l'azione di esportazione dovrebbero essere d'accordo. Se un utente modifica un record e naviga via, l'app non dovrebbe perdere silenziosamente il suo lavoro. Se apre due schede, una non dovrebbe sovrascrivere l'altra.
In Vue 3 di solito finisci per scegliere tra tre posti dove tenere lo stato:
- Stato locale del componente: posseduto da un singolo componente e sicuro da resettare quando viene smontato.
provide/inject: stato condiviso con scope di pagina o area funzionale, senza dover passare props a catena.- Pinia: stato condiviso che deve sopravvivere alla navigazione, essere riusato tra route e rimanere facile da debuggare.
Un modo utile per pensarci: per ogni pezzo di stato, decidi dove dovrebbe vivere così resta corretto, non sorprende l'utente e non si trasforma in spaghetti.
Gli esempi qui sotto si concentrano su tre problemi comuni negli admin: filtri e tabelle (cosa deve persistere o resettarsi), bozze e modifiche non salvate (form di cui gli utenti si possono fidare), e modifica multi-scheda (evitare collisioni di stato).
Un modo semplice per classificare lo stato prima di scegliere uno strumento
I dibattiti sullo stato si semplificano quando smetti di discutere degli strumenti e prima nomini il tipo di stato che hai. Stati diversi si comportano diversamente, e mescolarli è ciò che crea bug strani.
Una suddivisione pratica:
- Stato UI: toggle, dialog aperti, righe selezionate, tab attive, ordine di ordinamento.
- Stato server: risposte API, flag di loading, errori, ultima volta di refresh.
- Stato del form: valori dei campi, errori di validazione, flag dirty, bozze non salvate.
- Stato cross-screen: tutto ciò che più route devono leggere o modificare (workspace corrente, permessi condivisi).
Poi definisci lo scope. Chiediti dove lo stato è usato oggi, non dove potrebbe essere usato un giorno. Se conta solo dentro un componente tabella, lo stato locale di solito va bene. Se due componenti fratelli nella stessa pagina ne hanno bisogno, il vero problema è la condivisione a livello di pagina. Se più route ne hanno bisogno, sei in territorio di stato condiviso dell'app.
Successivamente valuta la durata. Alcuni stati devono resettarsi quando chiudi un drawer. Altri devono sopravvivere alla navigazione (filtri mentre entri in un record e torni indietro). Alcuni devono sopravvivere a un refresh (una bozza lunga che l'utente riapre dopo). Trattare tutti e tre allo stesso modo è come finire con filtri che si resettano misteriosamente o bozze che scompaiono.
Infine, controlla la concorrenza. I pannelli admin incontrano casi limite in fretta: un utente apre lo stesso record in due schede, un refresh in background aggiorna una riga mentre un form è dirty, o due editor gareggiano per salvare.
Esempio: una schermata “Utenti” con filtri, una tabella e un drawer di modifica. I filtri sono stato UI con durata di pagina. Le righe sono stato server. I campi del drawer sono stato form. Se lo stesso utente è modificato in due tab, serve una decisione esplicita sulla concorrenza: bloccare, unire o avvisare.
Una volta che sai etichettare lo stato per tipo, scope, lifetime e concorrenza, la scelta dello strumento (stato locale, provide/inject o Pinia) diventa spesso molto più chiara.
Come scegliere: un processo decisionale che regge
Le buone scelte di stato iniziano con un'abitudine: descrivi lo stato in parole semplici prima di scegliere uno strumento. I pannelli admin mescolano tabelle, filtri, grandi form e navigazione tra record, quindi anche lo stato “piccolo” può diventare fonte di bug.
Un processo decisionale in 5 passi
-
Chi ha bisogno dello stato?
- Un componente: tienilo locale.
- Diversi componenti sotto una stessa pagina: considera
provide/inject. - Più route: considera Pinia.
I filtri sono un buon esempio. Se influenzano solo una tabella che li possiede, lo stato locale va bene. Se i filtri stanno in una toolbar e pilotano una tabella sotto, la condivisione a livello di pagina (spesso
provide/inject) mantiene le cose pulite. -
Quanto deve durare?
- Se può sparire quando il componente si smonta, lo stato locale è ideale.
- Se deve sopravvivere a un cambio di route, Pinia è spesso la scelta giusta.
- Se deve sopravvivere a un reload, serve anche persistenza (storage), indipendentemente da dove vive.
Questo è particolarmente importante per le bozze. Le modifiche non salvate richiedono fiducia: le persone si aspettano che una bozza sia ancora lì se escono e poi tornano.
-
Deve essere condiviso tra schede del browser o isolato per scheda?
La modifica multi-scheda è dove i bug si nascondono. Se ogni scheda deve avere la propria bozza, evita un singleton globale. Preferisci stato indicizzato per ID record, o mantienilo con scope di pagina così una scheda non può sovrascriverne un'altra.
-
Scegli l'opzione più semplice che funzioni.
Parti dal locale. Sali di livello solo quando senti un vero dolore: prop drilling, logica duplicata o reset difficili da riprodurre.
-
Conferma le tue esigenze di debugging.
Se vuoi una vista chiara e ispezionabile dei cambiamenti tra schermate, Pinia con azioni centralizzate e stato è spesso un grande risparmio di tempo. Se lo stato è breve e ovvio, lo stato locale è più facile da leggere.
Stato locale del componente: quando basta
Lo stato locale è l'impostazione di default quando i dati contano solo per un componente in una pagina. È facile saltare questa opzione e sovradimensionare uno store che poi passerai mesi a mantenere.
Un caso chiaro è una singola tabella con i propri filtri. Se i filtri influenzano solo quella tabella (per esempio, la lista Utenti) e nient'altro dipende da loro, tienili come ref dentro il componente tabella. Lo stesso vale per piccoli stati UI come “il modal è aperto?”, “quale riga è in modifica?” e “quali elementi sono selezionati ora?”.
Cerca di non salvare ciò che puoi calcolare. Il badge “Filtri attivi (3)” dovrebbe essere calcolato dai valori dei filtri correnti. Etichette di ordinamento, sommari formattati e flag “si può salvare” sono meglio come computed perché rimangono sincronizzati automaticamente.
Le regole di reset contano più dello strumento che usi. Decidi cosa si pulisce al cambio di route (di solito tutto) e cosa rimane quando l'utente cambia vista dentro la stessa pagina (potresti mantenere i filtri ma cancellare selezioni temporanee per evitare azioni bulk sorprendenti).
Lo stato locale è di solito sufficiente quando:
- Lo stato interessa un widget (un form, una tabella, un modal).
- Nessun'altra schermata deve leggerlo o modificarlo.
- Puoi tenerlo entro 1–2 componenti senza passare props su più livelli.
- Riesci a descrivere il comportamento di reset in una sola frase.
Il limite principale è la profondità. Quando inizi a far passare lo stesso stato attraverso diversi componenti annidati, lo stato locale diventa prop drilling, ed è il segnale per passare a provide/inject o a uno store.
provide/inject: condividere stato all'interno di una pagina o area funzionale
provide/inject sta tra lo stato locale e uno store completo. Un parent “fornisce” valori a tutto ciò che è sotto di esso, e i componenti annidati li “iniettano” senza prop drilling. Nei pannelli admin è perfetto quando lo stato appartiene a una singola schermata o area funzionale, non all'intera app.
Un pattern comune è un page shell che possiede lo stato mentre i componenti più piccoli lo consumano: una barra di filtri, una tabella, una toolbar per azioni bulk, un drawer di dettagli e un banner “modifiche non salvate”. Lo shell può fornire una piccola superficie reattiva come un oggetto filters, un oggetto draftStatus (dirty, saving, error) e un paio di flag in sola lettura (per esempio, isReadOnly basato sui permessi).
Cosa fornire (tenerlo piccolo)
Se fornisci tutto, hai praticamente ricreato uno store con meno struttura. Fornisci solo ciò di cui diversi figli hanno veramente bisogno. I filtri sono un esempio classico: quando tabella, chips, azione di esportazione e paginazione devono restare sincronizzati, è meglio condividere una sola fonte di verità invece di destreggiarsi tra props ed eventi.
Chiarezza e insidie
Il rischio più grande sono le dipendenze nascoste: un figlio “funziona” perché qualcosa sopra ha fornito i dati, e dopo diventa difficile capire da dove arrivano gli aggiornamenti.
Per mantenerlo leggibile e testabile, dai alle injection nomi chiari (spesso con costanti o Symbol). Preferisci anche fornire azioni, non solo oggetti mutabili. Una piccola API come setFilter, markDirty e resetDraft rende esplicita la proprietà e le modifiche consentite.
Pinia: stato condiviso e aggiornamenti prevedibili tra schermate
Pinia brilla quando lo stesso stato deve rimanere consistente tra route e componenti. In un pannello admin questo spesso significa l'utente corrente, cosa può fare, quale organizzazione/workspace è selezionata e le impostazioni a livello di app. Diventa tedioso se ogni schermata lo reimplementa.
Uno store aiuta perché ti dà un unico posto dove leggere e aggiornare lo stato condiviso. Invece di passare props su più livelli, importi lo store dove ne hai bisogno. Quando passi da una lista a una pagina dettaglio, il resto dell'interfaccia può comunque reagire allo stesso workspace selezionato, ai permessi e alle impostazioni.
Perché Pinia è più facile da mantenere
Pinia spinge verso una struttura semplice: state per i valori grezzi, getters per i valori derivati e actions per gli aggiornamenti. Negli UI admin questa struttura previene i “fix rapidi” che si trasformano in mutazioni sparse.
Se canEditUsers dipende dal ruolo corrente più una feature flag, metti la regola in un getter. Se cambiare org richiede svuotare selezioni cache e ricaricare la navigazione, metti quella sequenza in un'azione. Finirai con meno watcher misteriosi e meno momenti “perché è cambiato questo?”.
Pinia funziona bene anche con Vue DevTools. Quando c'è un bug, è molto più semplice ispezionare lo stato dello store e vedere quale azione è stata eseguita, piuttosto che inseguire cambiamenti in oggetti reattivi sparsi creati all'interno di componenti a caso.
Evita lo store-dump
Uno store globale sembra ordinato all'inizio, poi diventa un cassetto dei rifiuti. Buoni candidati per Pinia sono preoccupazioni veramente condivise come identità utente, permessi, workspace selezionato, feature flag e dati di riferimento usati in molte schermate.
Preoccupazioni solo di pagina (come input temporanei di un form) dovrebbero restare locali a meno che più route abbiano davvero bisogno di loro.
Esempio 1: filtri e tabelle senza trasformare tutto in uno store
Immagina una pagina Ordini: una tabella, filtri (stato, intervallo di date, cliente), paginazione e un pannello laterale che mostra l'anteprima dell'ordine selezionato. Questo si complica in fretta perché è facile mettere ogni filtro e impostazione della tabella in uno store globale.
Un modo semplice per scegliere è decidere cosa deve essere ricordato e dove:
- Memoria solo (locale o provide/inject): si resetta quando lasci la pagina. Ottimo per stato usa-e-getta.
- Query params: condivisibili e sopravvivono al reload. Buono per filtri e paginazione che gli utenti copiano.
- Pinia: sopravvive alla navigazione. Buono per “tornare alla lista esattamente com'era”.
Da lì, l'implementazione segue spesso questa linea:
Se nessuno si aspetta che le impostazioni sopravvivano alla navigazione, tieni filters, sort, page e pageSize dentro il componente pagina Orders e lascia che quella pagina inneschi la fetch. Se toolbar, tabella e pannello di anteprima hanno bisogno dello stesso modello e il passaggio di props diventa rumoroso, sposta il modello della lista nello shell di pagina e condividilo via provide/inject. Se vuoi che la lista sia sticky tra route (apri un ordine, vai via, torna e trovi gli stessi filtri e selezioni), Pinia è la scelta migliore.
Una regola pratica: parti locale, passa a provide/inject quando diversi figli hanno bisogno dello stesso modello, e usa Pinia solo quando hai davvero bisogno di persistenza cross-route.
Esempio 2: bozze e modifiche non salvate (form di cui ci si può fidare)
Immagina un agente di supporto che modifica un record cliente: dettagli di contatto, informazioni di fatturazione e note interne. Viene interrotto, cambia schermata e poi torna. Se il form dimentica il suo lavoro o salva dati incompleti, la fiducia se ne va.
Per le bozze, separa tre cose: l'ultimo record salvato, le modifiche gestite dall'utente e lo stato UI-only come errori di validazione.
Stato locale: modifiche gestite con regole di dirty chiare
Se la schermata di modifica è autocontenuta, lo stato locale del componente è spesso il più sicuro. Tieni una copia draft del record, traccia isDirty (o una mappa dirty per campo) e conserva gli errori vicino ai controlli del form.
Un flusso semplice: carica il record, clona in una bozza, modifica la bozza e invia la richiesta di salvataggio solo quando l'utente clicca Salva. Annulla scarta la bozza e ricarica.
provide/inject: una bozza condivisa tra sezioni annidate
I form admin sono spesso divisi in tab o pannelli (Profilo, Indirizzi, Permessi). Con provide/inject puoi mantenere un unico modello bozza ed esporre una piccola API come updateField(), resetDraft() e validateSection(). Ogni sezione legge e scrive la stessa bozza senza passare props su cinque livelli.
Quando Pinia aiuta con le bozze
Pinia diventa utile quando le bozze devono sopravvivere alla navigazione o essere visibili al di fuori della pagina di modifica. Un pattern comune è draftsById[customerId], così ogni record ha la propria bozza. Questo aiuta anche quando gli utenti possono aprire più schermate di modifica contemporaneamente.
I bug delle bozze spesso provengono da errori prevedibili: creare una bozza prima che il record sia caricato, sovrascrivere una bozza dirty al refetch, dimenticare di pulire gli errori al cancel, o usare una singola chiave condivisa che fa sovrascrivere le bozze. Se stabilisci regole chiare (quando creare, sovrascrivere, scartare, persistire e sostituire dopo il salvataggio), la maggior parte di questi scompare.
Se costruisci schermate admin con AppMaster (appmaster.io), la separazione “bozza vs record salvato” vale ancora: tieni la bozza sul client e tratta il backend come fonte di verità solo dopo un Save riuscito.
Esempio 3: modifica multi-scheda senza collisioni di stato
La modifica multi-scheda è dove i pannelli admin spesso si rompono. Un utente apre Cliente A, poi Cliente B, torna indietro e si aspetta che ogni scheda ricordi le proprie modifiche non salvate.
La soluzione è modellare ogni scheda come il proprio bundle di stato, non come una singola bozza condivisa. Ogni scheda ha bisogno almeno di una chiave unica (spesso basata sull'ID del record), i dati della bozza, lo stato (clean, dirty, saving) e gli errori di campo.
Se le schede vivono dentro una schermata, un approccio locale funziona bene. Tieni la lista di schede e le bozze possedute dal componente pagina che rende le schede. Ogni pannello editor legge e scrive solo il proprio bundle. Quando una scheda si chiude, cancella quel bundle e il gioco è fatto. Questo mantiene le cose isolate e facili da ragionare.
Qualunque sia la collocazione dello stato, la forma è simile:
- Una lista di oggetti tab (ognuno con
customerId,draft,statusederrors) - Un
activeTabKey - Azioni come
openTab(id),updateDraft(key, patch),saveTab(key)ecloseTab(key)
Pinia diventa la scelta migliore quando le schede devono sopravvivere alla navigazione (vai a Ordini e torna) o quando più schermate devono poter aprire e mettere a fuoco schede. In tal caso, un piccolo store “tab manager” mantiene il comportamento coerente nell'app.
La collisione principale da evitare è una singola variabile globale come currentDraft. Funziona fino alla seconda scheda aperta, poi le modifiche si sovrascrivono, gli errori di validazione compaiono nel posto sbagliato e Salva aggiorna il record sbagliato. Quando ogni scheda ha il suo bundle, le collisioni praticamente scompaiono per progettazione.
Errori comuni che causano bug e codice disordinato
La maggior parte dei bug nei pannelli admin non sono “bug di Vue”. Sono bug di stato: i dati vivono nel posto sbagliato, due parti dello schermo non sono d'accordo, o lo stato vecchio rimane attaccato.
Ecco i pattern che compaiono più spesso:
Mettere tutto in Pinia di default rende l'ownership poco chiara. Uno store globale sembra organizzato all'inizio, ma presto ogni pagina legge e scrive gli stessi oggetti, e il cleanup diventa un gioco di indovinelli.
Usare provide/inject senza un contratto chiaro crea dipendenze nascoste. Se un figlio inietta filters ma non esiste comprensione condivisa di chi lo fornisce e quali azioni possono cambiarlo, arriveranno aggiornamenti a sorpresa quando un altro figlio inizi a mutare lo stesso oggetto.
Mescolare stato server e stato UI nello stesso store causa sovrascritture accidentali. I record fetchati si comportano diversamente da “drawer aperto?”, “tab corrente” o “campi dirty”. Quando convivono, il refetch può schiacciare la UI, o le modifiche UI possono mutare i dati cached.
Saltare il cleanup del ciclo di vita lascia lo stato a perdere. Filtri di una vista possono influenzarne un'altra, e bozze possono rimanere dopo aver lasciato la pagina. La prossima volta che qualcuno apre un record diverso vede selezioni vecchie e pensa che l'app sia rotta.
Keyare male le bozze è un killer silenzioso di fiducia. Se salvi le bozze sotto una chiave come draft:editUser, modificare Utente A poi Utente B sovrascrive la stessa bozza.
Una regola semplice previene la maggior parte di questo: tieni lo stato il più vicino possibile a dove viene usato, e solleva solo quando due parti indipendenti hanno davvero bisogno di condividerlo. Quando lo condividi, definisci proprietà (chi può cambiarlo) e identità (come è indicizzato).
Una checklist rapida prima di scegliere locale, provide/inject o Pinia
La domanda più utile è: chi possiede questo stato? Se non riesci a dirlo in una frase, lo stato probabilmente fa troppo e andrebbe diviso.
Usa questi controlli come filtro rapido:
- Riesci a nominare il proprietario (un componente, una pagina o l'intera app)?
- Deve sopravvivere a cambi di route o a un reload? Se sì, pianifica la persistenza invece di sperare che il browser lo mantenga.
- Verranno mai modificati due record contemporaneamente? Se sì, keya lo stato per ID record.
- Lo stato è usato solo da componenti sotto uno shell di pagina? Se sì,
provide/injectspesso è adatto. - Hai bisogno di ispezionare i cambiamenti e capire chi ha cambiato cosa? Se sì, Pinia è spesso il posto più pulito per quella fetta.
Abbinamento degli strumenti, in termini semplici:
Se lo stato nasce e muore dentro un componente (come un flag dropdown aperto/chiuso), tienilo locale. Se diversi componenti nella stessa schermata hanno bisogno di contesto condiviso (barra filtri + tabella + riassunto), provide/inject lo condivide senza renderlo globale. Se lo stato deve essere condiviso tra schermate, sopravvivere alla navigazione o richiede aggiornamenti prevedibili e debug-abili, usa Pinia e keya le entrate per ID record quando ci sono bozze.
Se costruisci un UI admin Vue 3 (anche generato con strumenti come AppMaster), questa checklist ti aiuta a evitare di mettere tutto in uno store troppo presto.
Passi successivi: far evolvere lo stato senza creare disordine
Il modo più sicuro per migliorare la gestione dello stato nei pannelli admin è farlo crescere con passi piccoli e noiosi. Parti dallo stato locale per tutto ciò che rimane dentro una pagina. Quando vedi vero riuso (logica copiata, un terzo componente che ha bisogno dello stesso stato), sali di livello. Solo allora considera uno store condiviso.
Un percorso che funziona per la maggior parte dei team:
- Mantieni lo stato solo-pagina locale per primo (filtri, sort, paginazione, pannelli aperti/chiusi).
- Usa
provide/injectquando diversi componenti nella stessa pagina hanno bisogno di contesto condiviso. - Aggiungi uno store Pinia alla volta per esigenze cross-screen (draft manager, tab manager, workspace corrente).
- Scrivi regole di reset e rispettale (cosa si resetta alla navigazione, al logout, Clear filters, Discard changes).
Le regole di reset sembrano piccole, ma prevengono la maggior parte dei momenti “perché è cambiato?”. Decidi, per esempio, cosa succede a una bozza quando qualcuno apre un record diverso e poi ritorna: ripristina, avvisa o resetta. Poi rendi quel comportamento coerente.
Se introduci uno store, mantienilo feature-shaped. Uno store bozze dovrebbe gestire creare, ripristinare e cancellare bozze, ma non dovrebbe possedere filtri di tabella o flag di layout UI.
Se vuoi prototipare rapidamente un pannello admin, AppMaster (appmaster.io) può generare una web app Vue3 più backend e logica di business, e puoi comunque rifinire il codice generato dove ti serve una gestione dello stato personalizzata. Un passo pratico è costruire una schermata end-to-end (per esempio, un form di modifica con recupero bozza) e vedere cosa deve davvero stare in Pinia rispetto a cosa può restare locale.
FAQ
Usa lo stato locale quando i dati interessano solo un componente e possono essere resettati quando quel componente viene smontato. Esempi tipici: apertura/chiusura di un dialog, righe selezionate in una singola tabella e una sezione di un form che non viene riutilizzata altrove.
provide/inject è utile quando diversi componenti nella stessa pagina hanno bisogno di una singola fonte di verità e il passaggio delle props diventa ingombrante. Fornisci solo ciò che serve e mantieni l'interfaccia chiara per non creare dipendenze nascoste.
Usa Pinia quando lo stato deve essere condiviso tra route, sopravvivere alla navigazione o essere facilmente ispezionabile e debug-abile in un punto centrale. Esempi comuni: workspace corrente, permessi, feature flag e manager cross-screen come bozze o schede.
Inizia nominando il tipo di stato (UI, server, form, cross-screen), poi decidi scope (un componente, una pagina, più route), lifetime (resettare allo smontaggio, sopravvivere alla navigazione, sopravvivere al reload) e concorrenza (editor singolo o multi-scheda). Di solito la scelta dello strumento segue da queste quattro etichette.
Se gli utenti devono condividere o ripristinare la vista, metti filtri e paginazione nei parametri di query così sopravvivono al reload e possono essere copiati. Se gli utenti si aspettano principalmente di “tornare alla lista com'era” tra le route, valuta di memorizzare il modello della lista in Pinia; altrimenti mantienilo scoped alla pagina.
Separa l'ultimo record salvato dalle modifiche in corso dell'utente e scrivi sul backend solo quando l'utente clicca Salva. Tieni una regola chiara di dirty, e decidi cosa succede alla navigazione (avviso, autosave, o conservare una bozza recuperabile) per evitare perdite di lavoro.
Assegna a ogni editor aperto il proprio bundle di stato identificato dall'ID del record (e talvolta da una chiave tab), invece di usare un unico currentDraft globale. Così le modifiche e gli errori di validazione restano isolati e non si sovrascrivono a vicenda.
Se l'intero flusso di modifica è confinato a una route, un setup provide/inject posseduto dalla pagina può funzionare. Se invece le bozze devono sopravvivere alla navigazione o essere accessibili fuori dalla pagina di modifica, Pinia con una struttura tipo draftsById[recordId] è solitamente più semplice e prevedibile.
Non memorizzare ciò che puoi calcolare. Deriva badge, riassunti e flag tipo “può salvare” dallo stato corrente usando computed, così non si rischia che vadano fuori sync.
Mettere tutto in Pinia a priori, mescolare risposte server con toggles UI e non pulire lo stato al cambio di vista sono le cause principali di comportamento strano. Un altro errore comune è usare chiavi scadenti per le bozze, come un singolo draft riutilizzato per record diversi.


