# How to build a settings page that survives growth

A settings page survives growth when sections are data, not copied JSX. Here is the structure, per-section saving and the limit of ready blocks.

> Page: https://ui.eneawork.it/blog/come-costruire-una-pagina-impostazioni
> Markdown: https://ui.eneawork.it/blog/come-costruire-una-pagina-impostazioni.md

*2026-11-01*

Una pagina impostazioni che regge la crescita è un elenco di sezioni dichiarate come dati, non una sequenza di componenti scritti a mano. Aggiungere un'opzione deve costare un oggetto, non una copia di JSX.

Il punto in cui si rompe è prevedibile: la terza sezione. Arriva il primo salvataggio per sezione, poi il primo campo che dipende da un altro, poi il primo permesso negato.

## Una sezione, un salvataggio

## Cosa c'è nel catalogo

Il blocco settings di 1st-pouf mostra sezioni a tab con react-hook-form; openlmis-settings-list è la variante a righe con badge e valori in sola lettura. Sfoglia la categoria settings. Sorgenti: github.com/moji2002/1st-pouf e github.com/SolDevelo/soldevelo-registry, licenza dichiarata a monte «see source».

Il 1 novembre 2026 abbiamo letto da /api/stats: 111.985 componenti, 1.556 blocchi, 12.808 icone e 1.472 librerie. Lo stesso giorno docs/keyword-data.json, raccolto il 31 ottobre 2026 con la Stack Exchange API, riporta 995 voti sulla domanda «How to set focus on an input field after rendering?»: il focus dopo un'azione è ancora il dettaglio che le pagine impostazioni sbagliano. Con lo stesso metodo, un comando eseguito quel giorno su data/blocks.json (snapshot del 31 ottobre 2026) conta un solo blocco con la categoria settings: node -e "const b=require('./data/blocks.json').blocks;console.log(b.filter((x)=>(x.categories||[]).includes('settings')).length)" stampa 1.

La categoria stretta non è una lacuna del catalogo: dice che «impostazioni» non è un tipo di pagina. Il blocco di 1st-pouf non dichiara categorie e si trova per titolo, non per filtro. Cerca per nome, apri il codice e leggi come tratta i campi.

## Dati, non colonne

npx shadcn@latest add tabs switch label

import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; type Setting = { id: string; label: string; description: string; defaultOn: boolean; }; export function SettingsSection({ title, settings }: { title: string; settings: Setting[] }) { return ( {title} {settings.map((setting) => ( {setting.label} {setting.description} ))} ); }

## L'esempio concreto: la sezione è un oggetto

Tre file bastano. Un file di configurazione dichiara le sezioni, una route dinamica legge lo slug e il componente disegna le righe. Aggiungere «avvisi di sicurezza» costa una voce nell'array e una riga nella tabella dei valori; nessuna pagina viene copiata.

export type Field = { id: string; label: string; help: string; kind: "switch" | "select"; options?: string[]; dependsOn?: { field: string; equals: boolean }; permission: "member" | "admin"; }; export const sections = { notifications: { title: "Notifiche", route: "/settings/notifications", fields: [ { id: "digest", label: "Riepilogo settimanale", help: "Una email il lunedì.", kind: "switch", permission: "member" }, { id: "mentions", label: "Menzioni", help: "Solo quando qualcuno ti nomina.", kind: "switch", dependsOn: { field: "digest", equals: true }, permission: "member" }, { id: "channel", label: "Canale", help: "Dove arriva il riepilogo.", kind: "select", options: ["email", "slack"], permission: "admin" }, ], }, } satisfies Record;

Il tipo Field è il contratto. Chi aggiunge un campo non tocca il JSX: sceglie kind, scrive help e dichiara permission. La dipendenza dependsOn spegne «Menzioni» finché il riepilogo è spento, e la ragione sta nel dato, non in un ramo nascosto.

## Quando la pagina cresce

Con lo schema, una nuova impostazione è una voce nell'array e un campo nel database. Permessi, audit log e valori ereditati restano fuori dal componente: li decide il server. Se un campo dipende da un altro, la dipendenza sta nello schema, non in un if dentro il JSX.

Limite: entrambi i blocchi dichiarano «see source» come licenza, e settings di 1st-pouf porta quattordici registryDependencies. Non è un componente da copiare in cinque minuti: leggilo come riferimento di struttura e verifica la licenza prima di spedire.

## La regola di decisione

Regola: un campo che richiede un permesso diverso dagli altri, o che dipende da un altro campo, prende una sezione con la sua route. Tutto il resto resta una riga nella lista. La soglia evita i due estremi: dodici tab in una pagina sola e una pagina per un singolo interruttore.

## L'errore da evitare

L'errore da evitare è fidarsi di defaultChecked. Lo switch mostra il nuovo valore prima della risposta, e se il salvataggio fallisce nessuno lo riporta indietro: la pagina dichiara «attivo» un valore che il database non ha mai ricevuto. Tieni il valore nello stato, disegna lo switch da lì e ripristina lo stato precedente quando la risposta è un errore.

Per scegliere il blocco giusto leggi Migliori settings blocks 2026 e la checklist in 12 punti per componenti React accessibili. Se devi decidere se partire da un tema pronto, il confronto Temi shadcn gratis o premium mette i costi sul tavolo.

## FAQ

### Una pagina sola o una route per sezione?

Una route per sezione, perché il link diretto è la funzione principale di una pagina impostazioni. Una pagina con dodici tab non si può linkare né ricaricare senza perdere il punto.

### Autosave o pulsante Salva?

Pulsante Salva per sezione, salvo impostazioni banali come un toggle. L'autosave invisibile rende impossibile capire cosa è stato scritto e quando.

### Come gestisco i campi disabilitati?

Con una spiegazione accanto al campo e un motivo leggibile, non solo un attributo disabled. Chi non capisce perché è bloccato apre un ticket.

### Dove metto i permessi?

Sul server. Il client può nascondere un controllo, ma la decisione su chi può cambiare un valore va presa e verificata lato server.

### Posso usare i blocchi settings del catalogo?

Puoi leggerli e adattarli, ma la licenza è dichiarata «see source». Controlla il repository indicato prima di includerli in un prodotto.
