12 minuti DI LETTURA
Quando una feature React Native contiene più schermate correlate, il problema non è soltanto decidere quale componente renderizzare. Bisogna anche stabilire chi possiede la navigazione, chi effettua le chiamate API, e come arrivano a ogni schermata tema, alert e callback.
Se ogni screen importa direttamente i servizi dell’app, il package smette di essere riutilizzabile. Se invece tutte le dipendenze vengono passate come props attraverso ogni livello, il codice finisce per soffrire di prop drilling. Il pattern usato da feature-leaves combina due strumenti: uno Stack interno creato con createNativeStackNavigator, e un LeavesContext che distribuisce le dipendenze della feature.
In questo articolo vediamo come funziona il pattern, quali contratti richiede e quali errori evitare.
Il confine tra shell e feature
La shell dell’app conosce provider, autenticazione, store, tema globale e stack principale. La feature ferie conosce invece richieste, contatori, archivio, approvazioni e form. Il confine desiderato è:
App shell
├── apiClient
├── Redux
├── AlertModalProvider
└── WiredLeavesNavigator
↓ contracts
feature-leaves
├── screens
├── hooks
├── components
└── internal navigator
Il package può chiamare onGetLeaves, ma non deve sapere se dietro c’è fetch, un client custom o un mock. Può chiamare onShowError, ma non deve importare ErrorModalProvider. Questa è dependency injection: le dipendenze vengono fornite dall’esterno, e consumate attraverso un contratto.
Struttura del package
La struttura rilevante è:
feature-leaves/
└── src/
├── components/
├── context/LeavesContext.js
├── contracts/ILeavesContracts.js
├── hooks/
├── navigation/LeavesFeatureNavigator.js
└── screens/
Le schermate importano componenti e hook relativi. Gli hook leggono dal Context soltanto le callback necessarie. Il file di navigazione non contiene logica di business. Questa separazione permette di leggere il flusso dall’alto verso il basso: il navigator registra le rotte, il Context fornisce le dipendenze, le screen compongono la UI, gli hook coordinano stato e azioni, e la shell implementa il contratto.
Definire LeavesContext
Il Context è minimale:
import { createContext, useContext } from 'react';
export const LeavesContext = createContext({});
export const useLeavesContext = () => useContext(LeavesContext);
Il valore di default vuoto evita un’importazione globale obbligatoria. In sviluppo, però, è utile rilevare presto una dipendenza mancante. Un helper può controllare le callback essenziali:
export function assertLeavesContracts(contracts) {
const required = ['onGetLeaves', 'onSendLeaveRequest'];
const missing = required.filter(key => typeof contracts?.[key] !== 'function');
if (missing.length > 0) {
throw new Error(`Missing Leaves contracts: ${missing.join(', ')}`);
}
}
Il controllo va usato con criterio: alcune callback possono essere opzionali in una determinata configurazione o in un test.
Sono candidati corretti per il Context: callback API, callback per alert ed errori, tema o token di design, callback di sincronizzazione verso la shell, configurazioni specifiche della feature. Non lo sono, invece: token JWT, l’intero Redux store, oggetti globali non usati dalla feature, o riferimenti a componenti della shell senza contratto.
Registrare le sette schermate
La registrazione completa è esplicita:
<Stack.Screen name="LeavesRequestScreen" component={LeavesRequestScreen} />
<Stack.Screen name="HolidayRequestListScreen" component={LeavesRequestListScreen} />
<Stack.Screen name="HolidayRequestDetailScreen" component={LeavesRequestDetailScreen} />
<Stack.Screen name="LRMContatoriDetailScreen" component={LRMContatoriDetailScreen} />
<Stack.Screen name="LeavesRequestFormScreen" component={LeavesRequestFormScreen} />
<Stack.Screen name="PendingApprovalsScreen" component={PendingApprovalsScreen} />
<Stack.Screen name="LeavesArchiveScreen" component={LeavesArchiveScreen} />
I nomi sono parte dell’API interna di navigazione, e vanno usati con coerenza:
navigation.navigate('LeavesRequestFormScreen', {
autoOpenSelector: true,
configData,
});
Un refactor dei nomi deve essere accompagnato dalla ricerca di tutti gli utilizzi, e da test di navigazione dedicati: una stringa sbagliata produce un errore soltanto a runtime.
Consumare il Context nelle schermate
Un hook come useLeavesArchive può leggere le callback senza importare il backend:
export function useLeavesArchive() {
const { onGetLeaves, onUpdateLeaveStatus } = useLeavesContext();
// stato, filtri, paginazione e azioni
}
La schermata conserva un ruolo presentazionale e di composizione:
export default function LeavesArchiveScreen() {
const { theme } = useLeavesContext();
const data = useLeavesArchiveScreen();
return <ArchiveList theme={theme} {...data} />;
}
Il componente non deve decidere quale URL chiamare, e non deve nemmeno sapere come Redux aggiorna lo stato globale. LeavesRequestFormScreen segue la stessa logica: riceve parametri di navigazione, chiama l’hook useLeavesRequestForm, e renderizza FerieForm o PermessiForm in base alla causale. Il form può usare onSendLeaveRequest dal Context, mentre la shell decide quale implementazione fornire.
Definire e validare il contratto
Il Context è un meccanismo; il contratto è la documentazione delle dipendenze. È utile raggruppare le callback per area:
Config e utente
- onGetLeaveScreenConfig
- onGetUserInfos
Richieste
- onSearchLeaveRequests
- onSendLeaveRequest
- onDeleteLeaveRequest
Archivio e approvazioni
- onGetLeaves
- onUpdateLeaveStatus
Contatori e form
- onGetContatoriFeriePermessi
- onGetTypeSelectorOptions
- onGetMooreaObj
Il contratto dovrebbe precisare parametri, risultato, errori e callback opzionali:
onUpdateLeaveStatus: async (id, status) => {
// status: valore semantico convertito nel formato richiesto dal backend
}
È meglio che il package lavori con intenti leggibili, e che la conversione verso numeri o URL avvenga direttamente nel contratto app.
Collegare la shell con il wiring
La shell implementa i contratti HTTP in appLeavesContracts, e aggiunge dipendenze locali:
const contracts = {
...appLeavesContracts,
onShowAlert: (title, message) => showAlert(title, message),
onShowError: error => showError(error),
onContatoriFetched: result => dispatch(setContatori(result)),
onConfigLoaded: result => dispatch(setLeaveScreenConfig(result)),
theme,
};
return <LeavesFeatureNavigator contracts={contracts} />;
Questa composizione evita che il package debba conoscere endpoint come /get_leaves o /crea_giustificativo, la struttura delle slice, i provider modali, o la configurazione del tema dell’app. Il punto di wiring è anche il posto corretto per logging applicativo, metriche e adattamento dei dati, senza inserire queste responsabilità nelle screen.
Errori comuni
Importare il client HTTP nel package crea accoppiamento, e rende difficile sostituire il backend. Creare un Context globale per tutta l’app fa perdere il beneficio del confine: il Context deve restare limitato alla feature, e avere un contratto leggibile. Nascondere dipendenze obbligatorie è rischioso: se una screen richiede una callback, il contratto deve dichiararlo — un errore silenzioso in onSendLeaveRequest?.() può trasformarsi in una richiesta che semplicemente non parte. Usare parametri di rotta come stato globale confonde due ruoli diversi: le rotte devono trasportare intenti e dati minimi, non sostituire un repository o uno store. Dimenticare il container principale, infine, è un errore strutturale: il navigator interno va sempre annidato nel container dell’app, senza crearne uno nuovo senza una ragione specifica.
Domande frequenti
Context e dependency injection sono la stessa cosa?
No. Context è il meccanismo React; dependency injection è il principio per cui le dipendenze vengono fornite dall’esterno.
Il package può usare un tema condiviso?
Sì. Il tema può arrivare dal Context o da un design system comune, senza importare configurazioni private della shell.
Posso esportare anche le singole schermate?
È possibile, ma l’entry point principale dovrebbe restare il navigator. Esportare screen singole va fatto solo quando esiste una necessità architetturale documentata.
Come gestisco i deep link?
La shell deve definire il deep link globale, e tradurlo nella rotta interna corretta, senza esporre direttamente ogni dettaglio del package.
Conclusione
LeavesFeatureNavigator e LeavesContext risolvono due problemi diversi ma collegati tra loro. Il navigator governa il flusso delle sette schermate; il Context distribuisce contratti e valori condivisi senza prop drilling.
La shell resta responsabile del wiring, mentre il package resta cieco rispetto a HTTP, Redux e provider globali. È questo confine, più che il singolo componente, a rendere la feature riutilizzabile e testabile.




