10 minuti di lettura
Una feature ferie e permessi raramente vive in una sola schermata. Nel progetto myGenius il flusso comprende la home, la lista delle richieste, il dettaglio, il form, i contatori, le approvazioni e l’archivio.
All’inizio può sembrare naturale registrare ogni schermata direttamente nell’AppStack: si importa un componente, si aggiunge una rotta, si passa alla successiva. Con il tempo, però, la shell inizia a conoscere dettagli che non le appartengono — i nomi delle rotte interne, i parametri del form, gli hook usati dalle approvazioni — e ogni modifica alla feature finisce per richiedere interventi in più punti dell’app.
La scelta adottata è stata diversa: racchiudere l’intero flusso in un package locale con un solo entry point pubblico, LeavesFeatureNavigator.
Il problema di una feature distribuita nella shell
Una shell applicativa dovrebbe occuparsi di avvio, autenticazione, provider globali e navigazione principale. La feature ferie dovrebbe invece conoscere il proprio flusso interno:
AppStack
↓
LeavesFeature
↓
Home → Lista → Dettaglio
↓ ↓
Form Contatori
↓ ↓
Approvazioni → Archivio
Quando tutte queste rotte vengono definite nello stack principale, la separazione si rompe. La shell finisce per contenere l’import di ogni screen, i nomi delle rotte interne, i parametri specifici del form, le callback per approvazioni e archivio, le dipendenze API della feature, e la logica per sincronizzare i contatori. La dipendenza non è soltanto estetica: se si cambia il flusso interno, o si aggiunge una schermata, bisogna modificare l’app principale.
Il navigator interno introduce invece un confine netto: la shell monta la feature, la feature governa ciò che avviene al suo interno.
Le sette schermate del flusso
Il package registra sette schermate correlate. LeavesRequestScreen è la home della feature, e mostra eventi, contatori, richieste imminenti e punti di accesso al form o alle liste. HolidayRequestListScreen rappresenta la lista delle richieste inviate, e può condurre al dettaglio. HolidayRequestDetailScreen mostra una singola richiesta con dati, stato, note e azioni disponibili. LRMContatoriDetailScreen raccoglie il dettaglio dei contatori ferie e permessi. LeavesRequestFormScreen orchestra selezione del tipo, form ferie, form permessi, note e invio. PendingApprovalsScreen è il punto di lavoro del manager: carica richieste pendenti, filtra, approva o rifiuta. LeavesArchiveScreen, infine, permette di interrogare lo storico con filtri per tipologia, reparto, stato e periodo.
Queste schermate condividono tema, contratti, componenti e navigazione. Tenerle insieme non significa trasformarle in un unico file: significa dare loro un proprietario architetturale comune.
Perché non creare sette WiredScreen
Un wrapper WiredXxxScreen per ogni schermata può funzionare quando le screen sono completamente indipendenti tra loro. In questo caso, però, produrrebbe una ripetizione significativa: ogni wrapper dovrebbe ricevere gli stessi elementi.
AppStack, e i test possono montare l’intera feature con contratti mock.Non è una regola assoluta: una schermata raggiunta da un deep link globale può richiedere una strategia diversa. Per un flusso coeso, però, un entry point unico riduce sensibilmente l’accoppiamento.LeavesContext come confine delle dipendenze
Il Context è volutamente semplice:
import { createContext, useContext } from 'react';
export const LeavesContext = createContext({});
export const useLeavesContext = () => useContext(LeavesContext);
Il suo valore contiene callback e dati condivisi dalla feature. Un hook può chiedere solo ciò che gli serve:
const {
onSendLeaveRequest,
onGetTypeSelectorOptions,
theme,
} = useLeavesContext();
Il package non importa apiClient, slice Redux o provider di alert della shell: riceve comportamenti dall’esterno.
Passare props dal navigator a ogni schermata e poi a ogni componente produrrebbe prop drilling. Il Context mantiene invece il contratto vicino alla feature, riducendo la quantità di dati che attraversano le rotte. Non deve però diventare uno store globale: deve contenere le dipendenze della feature, non qualsiasi dato dell’app.
Il wiring nella composition layer
La shell crea una versione wired del navigator:
export function WiredLeavesNavigator(props) {
const { showAlert } = useAlert();
const { showError } = useErrorModal();
const dispatch = useDispatch();
const theme = useTheme();
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 {...props} contracts={contracts} />;
}
Questo è il punto in cui l’app decide quale backend usare, come mostrare gli errori, dove salvare i contatori, quale tema passare, e quali provider globali collegare. Il package resta cieco rispetto a tutte queste decisioni.
Vantaggi e compromessi
Il navigator interno riduce il numero di import nella shell, rende il package testabile, e mantiene insieme codice che appartiene allo stesso dominio. Consente anche di pubblicare in futuro un package con un contratto chiaro: chi lo usa deve fornire le dipendenze, non conoscere la struttura interna.
I compromessi esistono comunque: il package deve documentare bene le rotte e il contratto, un errore nel Context può essere meno visibile di un prop mancante, e le schermate raggiunte da link globali richiedono una strategia di integrazione esplicita. Il navigator interno, va detto, non risolve automaticamente i problemi di stato — evita soltanto di diffonderli nella shell.
Errori comuni
Creare un secondo NavigationContainer è un errore da evitare: il package dovrebbe registrare uno Stack annidato, mentre il container principale resta normalmente responsabilità dell’app. Importare Redux nel package rende quest’ultimo dipendente dalla struttura dello store host. Mettere ogni cosa nel Context lo trasforma in qualcosa di difficile da documentare e testare — raggruppare le callback per area rende il contratto molto più leggibile. Passare l’intero oggetto di configurazione attraverso le rotte è un’altra scorciatoia rischiosa: meglio recuperare i dati tramite hook e callback, passando solo parametri specifici della navigazione. Confondere entry point con export di tutte le screen, infine, vanifica il confine costruito: se la shell importa ogni sub-screen, il confine è già stato superato.
Domande frequenti
Il navigator interno crea un secondo container? No. Crea uno Stack annidato dentro il NavigationContainer dell’app.
Posso usare Redux dentro il package? È tecnicamente possibile, ma in un modello blind è preferibile ricevere dati e callback dalla shell.
Perché usare Context invece di passare props? Per evitare prop drilling tra navigator, schermate e componenti, mantenendo le dipendenze nel perimetro della feature.
Una feature con due sole schermate ha bisogno di un navigator interno? Non necessariamente. Il pattern è utile quando le schermate condividono flusso e dipendenze; non va applicato per principio a ogni componente.
Conclusione
Sette schermate non richiedono sette punti di integrazione. Un navigator interno con Context offre un confine chiaro: la feature possiede navigazione e flusso, mentre la shell collega backend, stato globale, tema e servizi comuni.
Il risultato non è soltanto un codice più ordinato. È una responsabilità meglio distribuita: ogni livello conosce ciò che deve conoscere, e ignora il resto.




