11 minuti di lettura
Un dipendente apre la feature ferie e vede il proprio residuo. Nello stesso momento, la shell dell’app deve poter mostrare quello stesso dato altrove — un riepilogo, un badge, un valore usato da una WebView.
Il dato è lo stesso, ma i proprietari sono diversi. Il package deve poter recuperare e presentare i contatori; la shell deve poterli conservare nello stato globale. Se il package importa direttamente Redux, il confine architetturale si rompe in un punto solo, ma con effetti su tutto il resto.
La soluzione adottata è un callback: onContatoriFetched.
Perché i contatori sono un caso particolare
Una lista di richieste può restare confinata nella feature. I contatori, invece, hanno spesso valore trasversale: appaiono nella home ferie, vengono letti da una card di riepilogo, possono alimentare una dashboard, comparire in una WebView, o determinare badge e notifiche. Per questo vengono spesso messi in Redux — ma il fatto che un dato sia globale non significa che ogni package debba conoscere lo store globale.
Il requisito corretto, in sintesi, è questo: la feature deve notificare l’host quando ha ricevuto un nuovo risultato; l’host decide come conservarlo, e chi può leggerlo.
Il problema dell’accesso diretto a Redux
Immaginiamo una screen del package che importi direttamente useDispatch e l’action della slice:
import { useDispatch } from 'react-redux';
import { setContatori } from '../../../redux/slice/contatoriSlice';
La screen a questo punto dipende dalla posizione della slice nella shell, dal nome dell’action, dalla libreria Redux, dalla struttura dello store, e dalle convenzioni specifiche dell’app host. Il package non è più blind: non può essere montato con un semplice mock, e non può essere spostato in un’altra applicazione senza portarsi dietro l’intera struttura Redux.
Il problema non è Redux in sé — è appropriato per lo stato globale della shell. Il problema è il punto in cui viene importato.
Separare lettura, notifica e persistenza
Tre azioni diverse vengono spesso confuse tra loro: la lettura, cioè recuperare i contatori dal backend; la notifica, cioè comunicare alla shell che il risultato è arrivato; e la persistenza, cioè salvare il risultato nello store globale. Nel modello corretto, il flusso è:
Package
↓ onGetContatoriFeriePermessi()
App contract
↓ API client
Backend
↓ result
Package
↓ onContatoriFetched(result)
Wired layer
↓ dispatch(setContatori(result))
Redux shell
Il package può usare il risultato per il proprio rendering senza attendere che Redux completi il dispatch. La shell, invece, aggiorna in parallelo tutti gli altri consumatori.
La slice dei contatori
La slice presente nell’app mantiene un oggetto aggregato:
const contatoriSlice = createSlice({
name: 'contatori',
initialState: { value: {} },
reducers: {
setContatori: (state, action) => {
state.value = action.payload;
},
resetContatori: state => {
state.value = {};
},
updateContatori: (state, action) => {
state.value = { ...state.value, ...action.payload };
},
},
});
setContatori sostituisce l’oggetto completo, mentre updateContatori applica un merge parziale. La scelta dipende dalla forma della risposta, e deve essere documentata: se il backend restituisce sempre il risultato completo, la sostituzione è più sicura; se restituisce sezioni indipendenti, un aggiornamento parziale può essere utile, ma deve evitare di conservare dati vecchi senza indicarlo esplicitamente.
Il callback onContatoriFetched
Nel wiring del navigator:
const contracts = {
...appLeavesContracts,
onContatoriFetched: result => dispatch(setContatori(result)),
onConfigLoaded: result => dispatch(setLeaveScreenConfig(result)),
theme,
};
Il nome della callback descrive un evento, non un’implementazione: il package non sa se l’host farà un dispatch, aggiornerà un altro store, o inoltrerà il dato a un servizio esterno. Una callback di questo tipo dovrebbe essere opzionale se la feature può funzionare senza sincronizzazione globale, documentata nel contratto, chiamata solo dopo una risposta valida, invocata con un formato stabile, ed esclusa quando la richiesta fallisce. È importante non chiamarla con dati parziali non dichiarati: altrimenti Redux può sovrascrivere un valore valido con un oggetto incompleto.
Il flusso completo
Il caricamento può essere descritto così:
async function loadCounters() {
setLoading(true);
try {
const result = await onGetContatoriFeriePermessi();
setLocalCounters(result);
onContatoriFetched?.(result);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
}
La feature mantiene il proprio stato locale per mostrare loading, errore e dati aggiornati. La callback informa l’host senza trasferire alla shell la gestione della UI interna. Una sincronizzazione riuscita dovrebbe aggiornare la card della feature, il contatore Redux, l’eventuale riepilogo globale, e il timestamp di aggiornamento, se previsto.
In caso di errore, il dato precedente non dovrebbe essere cancellato automaticamente. L’app può mostrare un errore di refresh e mantenere l’ultimo saldo noto, indicandone la data:
Ultimo saldo valido: 12 ore
Aggiornamento non riuscito alle 10:42
[Riprova]
È decisamente più utile di una schermata vuota che nasconde un dato ancora valido.
Dati obsoleti e refresh concorrenti
Due richieste possono partire quasi contemporaneamente: apertura della schermata, pull-to-refresh, ritorno dal form. Se la prima risposta arriva dopo la seconda, potrebbe sovrascrivere il dato più recente. Le soluzioni includono un identificativo progressivo della richiesta, AbortController quando supportato, il confronto del timestamp di risposta, l’esclusione di una risposta che non appartiene all’ultima richiesta effettuata, o la serializzazione dei refresh. Un modello semplice:
const requestId = ++latestRequestId;
const result = await fetchCounters();
if (requestId !== latestRequestId) return;
setLocalCounters(result);
onContatoriFetched?.(result);
La soluzione reale deve essere coerente con l’architettura del client HTTP scelto.
Testare il confine
Il package può essere testato con callback mock:
const contracts = {
onGetContatoriFeriePermessi: jest
.fn()
.mockResolvedValue({ ferie: 12, permessi: 8 }),
onContatoriFetched: jest.fn(),
};
Il test della feature verifica che la richiesta venga effettuata, che il dato locale venga aggiornato, che onContatoriFetched riceva il risultato, e che in caso di errore la callback non venga chiamata. Il test della shell è diverso:
const dispatch = jest.fn();
const wire = result => dispatch(setContatori(result));
wire({ ferie: 12 });
expect(dispatch).toHaveBeenCalled();
Due test separati riflettono due responsabilità separate.
Errori comuni
Importare useDispatch nel package rompe l’isolamento, e lega la feature allo store host. Inviare dati parziali senza dichiararlo permette alla shell di sovrascrivere un saldo completo con un oggetto incompleto. Cancellare il dato in caso di refresh fallito fa perdere all’utente l’ultimo valore utile, senza che capisca se il problema riguarda il saldo o la connessione. Aggiornare solo la card locale lascia gli altri consumatori con un dato diverso — la sincronizzazione deve sempre passare dal contratto. Chiamare la callback prima della validazione rischia di trasformare un risultato vuoto o non valido nello stato globale. Confondere contatore e payroll, infine, è un errore concettuale: l’app deve indicare la fonte del dato, e se il payroll è autorevole, il package non dovrebbe presentare un calcolo locale come saldo definitivo.
Domande frequenti
Il package può leggere Redux?
In un modello blind è preferibile di no. Riceve dati e callback dal Context, e lascia alla shell la gestione dello store.
Dove va gestito il loading?
La feature gestisce il proprio loading. La shell può mantenere uno stato globale solo se serve ad altri flussi.
Cosa succede se onContatoriFetched non è fornita?
La callback può essere opzionale se la feature funziona comunque. Le dipendenze necessarie al rendering, invece, devono essere validate.
È meglio sostituire o fare merge dei contatori?
Dipende dal contratto della risposta. Il comportamento deve essere esplicito e testato.
Come mostro l’ultimo aggiornamento?
Con un timestamp proveniente dal backend o dal momento di ricezione, chiarendo sempre quale dei due si sta mostrando.
Conclusione
Il callback onContatoriFetched è un piccolo confine con un effetto importante. Il package recupera e presenta il dato; la shell decide come sincronizzare Redux e gli altri consumatori.
Separare lettura, notifica e persistenza riduce l’accoppiamento, migliora i test, e rende più affidabile la gestione degli errori e dei refresh concorrenti.




