Documentazione · src/react/state
Lo stato
Nove file. Uno store con chiavi indirizzabili, slice dichiarate come dato, e reducer che il pacchetto esporta ma non installa.
Una chiave, un indirizzo
Lo stato è piatto: una mappa da chiave a valore. La chiave però può essere scopata per nodo — nodo-42:open — e lo scoping è quello che permette a tre accordion nella stessa pagina di avere ognuno il suo open senza collidere, e a un binding di puntarne uno preciso.
Le forme sono quattro: global (chiave piatta, condivisa da tutti), self (isolata sul nodo corrente), #<id> (un nodo preciso), owner (il contenitore, trovato invece che nominato). Il valore scopato resta comunque piatto nella mappa, il che tiene lo store semplice e le chiavi confrontabili per stringa.
Le prime tre sono indirizzi ASSOLUTI: chi li scrive deve sapere l’id dell’altro nodo, e dentro una lista quell’id va riscritto per ogni figlio. owner risale l’albero e prende il primo contenitore che DICHIARA quel nome — non il primo che ha uno stato qualsiasi, o un contenitore intermedio che espone altro se lo porterebbe via.
lo store: valori, dichiarazioni, commit, sottoscrizione per chiave
Come si osserva
const { open } = useSliceState(['nodo-42:open'])Se non dichiari le chiavi
useUIState((s) => s) // si sottoscrive a TUTTO lo store:
// ogni commit, ovunque, ridisegna questo nodolo scoping delle chiavi e la valutazione delle espressioni di binding
Come si chiama
scopeKey('self', 'open', 'nodo-42') // 'nodo-42:open'
scopeKey('#hero', 'open', 'nodo-42') // 'hero:open'
scopeKey('global', 'open', 'nodo-42') // 'open'
scopeKey('owner', 'index', 'nodo-42', catena) // 'carosello-7:index'L'indirizzo relativo
// la catena arriva dal contesto, dal piu vicino al piu lontano
[{ id: 'slide1', names: ['active'] }, { id: 'carosello-7', names: ['index'] }]
// `owner` + `index` scavalca slide1: espone `active`, non `index`Cosa resta vero
store.get() // { 'nodo-42:open': true, 'hero:open': false, open: true }
// la mappa non guadagna livelli: e la chiave a portare l'indirizzoCi si sveglia solo per le proprie chiavi
Un nodo reattivo dichiara quali chiavi guarda e si sottoscrive a quelle. Un commit su una chiave che non guarda non lo risveglia; un nodo che guarda zero chiavi — quello che ha solo un’animazione — non si sottoscrive affatto e non si risveglia mai.
L’ultimo caso è il più importante e il più facile da perdere: un nodo che si sottoscrivesse a tutto lo store verrebbe risvegliato da qualunque commit, in qualunque punto della pagina. La dichiarazione delle chiavi è ciò che lo impedisce.
Collegare le chiavi però non era una riga: useSyncExternalStore chiama lo snapshot durante il render e di nuovo dopo, per vedere se è cambiato. Un selettore che costruisce un oggetto nuovo a ogni chiamata non supera mai quel confronto e va in loop. La memoizzazione sta dentro l’hook, dove chi chiama non la può sbagliare.
l’hook che un componente interattivo usa per registrare e guidare la propria slice
Chiamata reale, dal wrapper del Collapsible
const [open, setOpen] = useSliceControl<boolean>({
storeManaged,
nodeId,
name: 'open',
defaultValue: !!defaultOpen,
broadcastEvent: 'ui:openCollapsible',
})Cosa scrive nello store
setOpen(true) // commit sulla chiave <nodeId>:open
// chi non guarda quella chiave non si svegliaLa forma delle slice è un dato
Quale stato espone un tipo di blocco è dichiarato in una mappa: nome, tipo, da quale campo prende il valore iniziale, quale evento lo cambia. È dato, non una scelta chiusa dentro il componente: il wrapper dell’accordion chiama l’hook con name: 'active', e quel nome sta scritto dove chiunque può leggerlo.
Averla come dato serve a due cose: l’admin può MOSTRARE quale stato un blocco espone, e lo store conserva la dichiarazione accanto al valore. Un undefined non distingue «chiave sbagliata» da «chiave vuota»; una dichiarazione sì.
La personalizzazione dei valori di default non è qui: qui c’è la forma e l’interazione. La customizzazione arriverà con lo spostamento della definizione nello Studio.
la mappa blocco → slice esposte, con tipo e campo di provenienza
La dichiarazione
export const BLOCK_SLICES: Record<string, SliceShape[]> = {
accordion: [{ name: 'active', type: 'string', from: 'defaultValue', event: 'ui:openAccordion' }],
collapsible: [{ name: 'open', type: 'boolean', from: 'defaultOpen', event: 'ui:openCollapsible' }],
tabs: [{ name: 'active', type: 'string', from: 'defaultValue', event: 'ui:selectTab' }],
swiper: [{ name: 'index', type: 'number', from: 'initialSlide' }],
}Come si legge
slicesOf('collapsible').map(describeSlice)
// ['open: boolean (da `defaultOpen`)']l’evento con cui l’hook scrive la slice di un nodo
I reducer li mette il progetto
UIStateProvider riceve i reducer attivi e non ne ha di default: il motore è vuoto. Il pacchetto li esporta ma non li installa — li installa il file app-state.ts che init scaffolda nel progetto, dove si vedono, si estendono e si possono togliere.
La scelta ha un costo dichiarato: Reducer, Wiring, Effect e Mutation sono contratto pubblico, perché è contro quei tipi che il consumer scrive la propria logica di dominio. Cambiare la loro forma è un breaking change, e questo va tenuto presente prima di cambiarla.
Una mutazione porta sempre una key, anche quando porta un path. Il path scrive su una foglia — sections.hero.title — e chi lo produce valorizza comunque key con la RADICE del percorso: è quella che lo store confronta e su cui sveglia chi ascolta. Il campo fa parte della forma pubblica, quindi si può contare su di esso.
il provider: reducer attivi, store, wiring
Il montaggio, dal progetto
<UIStateProvider
reducers={{ ...defaultReducers, ACTIVATE_SECTION }}
effects={stateEffects}
persist="url"
>
{children}
</UIStateProvider>Il canale che non passi
dispatch({ type: 'ACTIVATE_SECTION', ... })
// nessun reducer risponde: nessun commit, nessun errorela porta dell’area
Gli effetti di pagina
Alcune conseguenze hanno bisogno del router: navigare, cambiare la query string. Le loro implementazioni non possono uscire da un componente, perché useRouter è un hook — quindi vengono registrate sul kernel dentro un useEffect, a runtime.
Al momento della CONFIGURAZIONE però un browser non c’è, quindi le implementazioni non esistono ancora: il plugin non avrebbe niente da leggere e l’admin non potrebbe proporle in un elenco. Da qui la separazione fra i NOMI, dichiarati staticamente, e le implementazioni, registrate quando un browser c’è.
i nomi degli effetti di pagina, dichiarati staticamente
I nomi, leggibili a configurazione
export const PAGE_EFFECT_NAMES = ['refresh', 'reload', 'navigate'] as const
le implementazioni, registrate sul kernel a runtime
Le implementazioni, vive solo a runtime
useEffect(() => {
kernel.registerEffect('navigate', ({ payload }) => router.push(payload.href))
}, [kernel, router])lo stato dichiarato a livello di PAGINA, non di blocco
Continua
Altro in «Il render · src/react»
Il renderTutte le diciassette pagineindice