12 minuti di lettura
Il file che decide se l’app può partire
In un’app React Native, il file App.tsx può sembrare a prima vista un componente di passaggio: importa il navigator, monta i provider e restituisce l’applicazione. In realtà, quando contiene il bootstrap, decide una cosa molto più importante di quanto sembri: quale parte dell’app l’utente può vedere prima che siano pronti rete, versione e sessione.
Nel progetto myGenius l’avvio deve coordinare quattro situazioni diverse tra loro: la splash screen nativa mentre vengono eseguiti i controlli iniziali, l’assenza di connessione Internet, la versione installata non più supportata, e infine l’ingresso nell’app normale, con provider e navigator montati.
La difficoltà non sta nel numero di righe di codice — sta nell’ordine in cui queste cose accadono. Se la splash scompare troppo presto, l’utente vede una schermata incompleta. Se la verifica versione non viene completata prima del resto, un client non supportato può comunque raggiungere il backend. Se la schermata offline viene valutata prima del forced update, l’utente non riceve l’istruzione più importante di tutte, proprio quando servirebbe di più.
Per questo il bootstrap va letto come una piccola macchina a stati, non come una serie casuale di if messi uno dopo l’altro.
La sequenza generale
Il flusso desiderato è, in sintesi, questo:
avvio nativo
↓
controlli iniziali
├── versione non supportata → forced update
├── rete assente → schermata offline
├── controlli in corso → splash
└── tutto pronto → AppProviders + AppNavigator
Nel codice attuale, l’ordine di rendering è leggermente diverso rispetto a quello che si potrebbe descrivere in una specifica astratta: il forced update ha priorità assoluta su tutto il resto, poi viene valutata la splash, poi l’offline, e infine l’app normale.
Questa priorità è corretta proprio per il caso in cui il server dica che la versione deve essere bloccata. Anche se la rete cade subito dopo il controllo, l’informazione mustBlock già acquisita deve restare più importante dell’errore di connettività sopraggiunto dopo.
Gli import di App.tsx
Il file inizia importando gli strumenti React Native e i servizi che servono a costruire lo stato iniziale:
import React, { useState, useEffect } from 'react';
import { SafeAreaView, ActivityIndicator, Text, Image } from 'react-native';
import NetInfo from '@react-native-community/netinfo';
import checkConnectionOnStart from './src/services/networkService';
import VersionModal from './src/components/modals/VersionModal';
import AppNavigator from './src/navigators/useAppNavigator';
import { globalStyles } from '@geniuswebsrl/mobile-ui';
import { AppProviders } from './src/app/AppProviders';
import "./global.css"
Ogni import qui ha una responsabilità ben distinta dalle altre. React fornisce stato ed effetti, il modulo React Native fornisce la shell visuale dello splash e della schermata offline, NetInfo osserva i cambi di connettività per tutta la vita dell’app, checkConnectionOnStart orchestra il primo controllo all’avvio, VersionModal impedisce l’accesso quando la versione è troppo vecchia, AppNavigator rappresenta il contenuto applicativo vero e proprio, e AppProviders monta i context e gli store necessari all’app quando è finalmente pronta a partire.
global.css è un dettaglio puramente di stile, ma nel bootstrap conviene ricordare un principio più generale: è importante non far dipendere i controlli di rete o versione da componenti che vengono montati soltanto nell’ultima fase del flusso.
I quattro stati locali
const App = () => {
const [isFirstScreen, setIsFirstScreen] = useState(true);
const [isOffline, setIsOffline] = useState(false);
const [mustBlock, setMustBlock] = useState(false);
const [updateUrl, setUpdateUrl] = useState(null);
isFirstScreen parte da true e rappresenta la fase iniziale dell’app. Finché resta attivo, App.tsx mostra lo spinner invece del navigator vero e proprio. Il nome richiama la “prima schermata”, ma il suo significato reale è più preciso: “bootstrap ancora in corso”.
isOffline indica che la connessione non è considerata utilizzabile in quel momento. Viene valorizzato dal controllo iniziale, e successivamente aggiornato dal listener NetInfo che resta attivo per tutta la sessione.
mustBlock è probabilmente il flag più importante dal punto di vista del controllo accessi applicativo. Quando vale true, l’app non monta affatto la navigazione: restituisce soltanto VersionModal, punto e basta.
updateUrl, infine, contiene l’URL dello store ricevuto dal servizio di controllo versione. È tenuto separato dal booleano perché il componente visuale deve sapere esattamente dove accompagnare l’utente, non solo che deve farlo.
Questi quattro stati sono sufficienti per l’implementazione corrente, ma non esprimono davvero tutte le transizioni possibili nel sistema. In particolare, non distinguono “controllo non ancora eseguito”, “errore nel controllo versione” e “server non raggiungibile” — tre situazioni concettualmente diverse che oggi vengono un po’ appiattite. Per una prima shell funzionano comunque bene; per un bootstrap più complesso sarebbe più leggibile un unico stato enumerato, o un vero e proprio oggetto di bootstrap.
Il controllo iniziale con checkConnectionOnStart
Il primo useEffect parte una sola volta, all’avvio:
useEffect(() => {
const init = async () => {
await checkConnectionOnStart(
setIsOffline,
setMustBlock,
setUpdateUrl,
setIsFirstScreen
);
};
init();
}, []);
La dipendenza vuota è del tutto intenzionale: il controllo di bootstrap non deve ripetersi a ogni render dell’app. La funzione init esiste principalmente per poter usare await dentro l’effetto, senza dover rendere useEffect stesso una funzione asincrona — cosa che React, come noto, non supporta direttamente.
In un codice più recente si potrebbe scrivere l’effetto con una funzione nominata e un flag di cancellazione, soprattutto se il componente può essere smontato prima che arrivi la risposta. Il principio, però, non cambia: il componente passa dei setter a un servizio esterno, che a sua volta aggiorna lo stato React dall’esterno del componente stesso.
La firma con quattro setter è semplice da introdurre in fretta, ma crea un accoppiamento piuttosto stretto tra le parti. networkService.js deve conoscere l’ordine esatto degli argomenti, e può modificare direttamente lo stato React senza passare da un’interfaccia più esplicita. Un contratto più pulito restituirebbe invece un risultato:
const bootstrap = await loadBootstrap();
setState(bootstrap);
Per questo articolo analizziamo comunque il codice esistente così com’è, ma questo resta il primo punto da considerare in un eventuale refactor futuro.
networkService.js: il primo controllo di rete
Il servizio contiene una funzione piuttosto piccola:
export const checkInternetConnection = async () => {
const state = await NetInfo.fetch();
return state.isConnected;
};
NetInfo.fetch() legge lo stato una volta sola, in un istante preciso. La funzione restituisce soltanto isConnected, e quindi in questa fase non distingue esplicitamente tra rete attiva e Internet non raggiungibile — una semplificazione che vale la pena tenere a mente.
Nel contesto della shell iniziale può essere una semplificazione accettabile, ma va ricordato il suo limite: un telefono collegato a un access point senza uscita Internet può risultare tecnicamente connesso, e subito dopo fallire comunque la chiamata all’API. Il listener di App.tsx, come vedremo, usa invece anche isInternetReachable, creando così due definizioni leggermente diverse di “online” all’interno dello stesso file.
Il controllo iniziale completo è questo:
export default async function checkConnectionOnStart(
setIsOffline,
setMustBlock,
setUpdateUrl,
setIsFirstScreen
) {
const connected = await checkInternetConnection();
setIsOffline(!connected);
const version = await checkVersions();
if (version.mustBlock) {
setMustBlock(true);
setUpdateUrl(version.updateUrl);
}
setIsFirstScreen(false);
SplashScreen.hide();
}
Il servizio esegue, in sequenza, quattro operazioni: legge la rete, aggiorna lo stato offline di conseguenza, verifica la versione minima accettata dal backend, e infine conclude il bootstrap disattivando lo stato iniziale e nascondendo la splash nativa.
Questa sequenza ha una conseguenza interessante da notare: anche quando connected risulta falso, checkVersions() viene comunque chiamata lo stesso. Se il controllo remoto fallisce, checkVersions() cattura l’errore internamente e restituisce mustBlock: false. L’app può quindi uscire dalla splash e mostrare la schermata offline senza restare bloccata a metà.
Questa scelta evita di lasciare l’utente davanti a uno spinner infinito, il che è un bene. D’altra parte, quando il server versione non è raggiungibile, l’app non può sapere con certezza se esiste un blocco obbligatorio in vigore. La policy va quindi decisa consapevolmente: fail-open, come nel codice attuale, significa privilegiare la disponibilità dell’app; fail-closed significherebbe invece privilegiare il controllo, con il rischio di bloccare utenti che hanno semplicemente una rete temporaneamente problematica.
Perché la splash viene nascosta alla fine
react-native-splash-screen mantiene visibile la schermata nativa fino alla chiamata esplicita:
SplashScreen.hide();
Nel servizio, questa chiamata avviene dopo il controllo di rete, il controllo versione e l’aggiornamento degli stati. È la posizione corretta, se vogliamo evitare di mostrare per un istante un contenuto non ancora classificato correttamente.
App.tsx ha anche una splash React Native di riserva, che entra in gioco un momento dopo:
if (isFirstScreen) {
return (
<SafeAreaView style={globalStyles.splashContainer}>
<ActivityIndicator
size="large"
style={{ transform: [{ scale: 3 }] }}
color="#cc66ff"
/>
</SafeAreaView>
);
}
La distinzione tra le due splash è utile da tenere a mente: la splash nativa copre il periodo prima che React Native abbia montato la gerarchia di componenti, mentre la splash React Native copre il periodo in cui il componente è già attivo, ma il bootstrap non è ancora finito del tutto.
Il colore e lo stile possono ovviamente essere raffinati in seguito, ma la regola più importante resta temporale: lo spinner non deve mai essere rimosso soltanto perché è arrivato il primo evento NetInfo. Deve restare visibile fino a quando il controllo iniziale ha effettivamente prodotto una decisione definitiva.
Il listener NetInfo per tutta la vita dell’app
Il secondo effetto osserva i cambi di connessione nel tempo:
useEffect(() => {
const unsubscribe = NetInfo.addEventListener((state) => {
const connected =
state.isConnected &&
state.isInternetReachable !== false;
setIsOffline(!connected);
if (connected && !mustBlock) {
setIsFirstScreen(false);
}
});
return () => unsubscribe();
}, [mustBlock]);
La libreria restituisce una funzione di unsubscribe, e chiamarla nel cleanup è essenziale: evita listener duplicati se l’effetto viene rieseguito, e impedisce aggiornamenti indesiderati su un componente non più montato.
La formula usata qui considera online una rete con isConnected truthy e con isInternetReachable diverso da false. Quindi un valore null per la raggiungibilità non blocca subito l’app — è una decisione ragionevole nella fase di transizione, perché in quel momento lo stato può essere ancora semplicemente sconosciuto al sistema.
Il listener aggiorna isOffline in entrambe le direzioni, sia quando la rete cade sia quando torna. Quando la rete torna disponibile, porta anche isFirstScreen a false, ma soltanto se non è attivo il forced update in quel momento.
L’effetto dipende esplicitamente da mustBlock, perché la callback interna deve conoscere il valore aggiornato di quel flag. Quando il flag cambia, React esegue il cleanup del vecchio listener e ne registra uno nuovo, con la chiusura corretta sui valori più recenti. Questa soluzione funziona, ma ogni cambio di mustBlock ricrea comunque la sottoscrizione da zero. Un’alternativa sarebbe usare un useRef per mantenere il valore corrente e installare il listener una sola volta per tutta la vita del componente — non è una correzione obbligatoria, ma una scelta da valutare se il bootstrap dovesse crescere o guadagnare altre dipendenze.
C’è poi un possibile effetto di gara da tenere presente: il listener può ricevere un evento con connessione disponibile mentre checkConnectionOnStart() sta ancora verificando la versione in background. In quel momento mustBlock può risultare ancora false, e setIsFirstScreen(false) può eseguire prima della vera conclusione del bootstrap. Il risultato può essere una breve apertura anticipata dell’app, prima che tutto sia davvero pronto. Per rendere l’ordine deterministico, conviene separare isFirstScreen dalla semplice connettività, e consentire al listener di chiudere la splash soltanto quando il controllo iniziale è realmente terminato.
Un modello più esplicito potrebbe essere:
const [bootstrapDone, setBootstrapDone] = useState(false);
// checkConnectionOnStart conclude con:
setBootstrapDone(true);
// Il listener aggiorna offline, ma non chiude la splash.
if (bootstrapDone && connected && !mustBlock) {
// eventuale aggiornamento della UI
}
Questa osservazione non cambia il comportamento desiderato dell’app — serve solo a rendere chiaro che “rete presente” e “bootstrap terminato” restano due fatti concettualmente diversi, anche se spesso arrivano quasi insieme.
La priorità del forced update
Il rendering, come detto, inizia proprio da qui:
if (mustBlock) {
return <VersionModal visible={true} updateUrl={updateUrl} />;
}
Questa condizione viene valutata prima della splash e prima dell’offline. È una priorità corretta: se il backend ha dichiarato che la versione non è più supportata, l’utente deve ricevere subito il percorso di aggiornamento, invece di entrare nell’app o restare fermo davanti alla schermata di rete senza sapere perché.
Il nome mustBlock rende esplicito il comportamento fin dal nome stesso: non è un banner informativo, e non è un suggerimento facoltativo. È un vero e proprio gate applicativo, che va rispettato senza eccezioni.
Un forced update dovrebbe però essere usato soltanto quando la versione precedente non può più operare in sicurezza o in compatibilità con il backend. Per aggiornamenti non critici, è preferibile un messaggio non bloccante che lasci comunque l’app utilizzabile. Android, del resto, documenta una distinzione simile tra aggiornamenti flessibili e immediati: il flusso immediato è adatto quando l’aggiornamento è critico per la funzione principale dell’app, mentre quello flessibile lascia usare l’app anche durante il download in background.
Su iOS e Android è comunque lo store a gestire l’installazione vera e propria. L’app può soltanto controllare una policy remota e aprire la pagina dello store, ma non dovrebbe mai promettere un’installazione silenziosa che la piattaforma, semplicemente, non concede a nessuno.
VersionModal: il blocco visuale
Il componente riceve soltanto due proprietà, in tutto:
const VersionModal = ({ visible, updateUrl }) => {
Sceglie il badge da mostrare in base alla piattaforma:
const badgeSource = Platform.OS === 'ios'
? appStoreBadge
: googlePlayStoreBadge;
e poi mostra una modale volutamente non dismissable:
<Modal
visible={visible}
dismissable={false}
contentContainerStyle={globalStyles.modal}
style={globalStyles.overlay}
>
<Text style={globalStyles.title}>Aggiornamento obbligatorio</Text>
<Text style={globalStyles.text}>
La tua versione dell'app non è più supportata.{"n"}
Per continuare, aggiorna l'app.
</Text>
{/* badge dello store */}
</Modal>
dismissable={false} impedisce di chiudere la modale con un tap esterno, ed è una scelta coerente: il contenuto non propone infatti nessun tasto “Continua comunque”, proprio in linea con il significato pieno di mustBlock.
Il link verso lo store viene aperto così:
<Pressable onPress={() => Linking.openURL(updateUrl)}>
<Image source={badgeSource} />
</Pressable>
Qui c’è un controllo che non andrebbe trascurato: se updateUrl risulta null, Linking.openURL() può fallire silenziosamente. Il contratto dovrebbe garantire che un blocco abbia sempre un URL valido associato, oppure la UI dovrebbe disabilitare il pulsante mostrando un messaggio alternativo chiaro. Un forced update senza destinazione, in fondo, è un vicolo cieco per l’utente — gli si dice di aggiornare, ma non gli si dà modo di farlo.
Vale anche la pena notare che PaperProvider viene montato dentro VersionModal stesso. Se in futuro la modale viene usata insieme a provider globali già esistenti, conviene verificare che non si creino due alberi di tema o di portal sovrapposti tra loro. Non è un problema specifico della rete in sé, ma il bootstrap è proprio il punto in cui questo genere di dettagli tende a diventare visibile.
Il controllo remoto della versione
src/utils/checkVersions.js confronta la versione locale con una configurazione remota che differisce tra Android e iOS:
const VERSION_CHECK_URL_ANDROID =
'https://my-genius.it/wp-json/genius/v1/app_version_android';
const VERSION_CHECK_URL_IOS =
'https://my-genius.it/wp-json/genius/v1/app_version_ios';
La piattaforma decide quale endpoint chiamare:
if (Platform.OS === 'android') {
response = await fetch(VERSION_CHECK_URL_ANDROID, { method: 'GET' });
} else if (Platform.OS === 'ios') {
response = await fetch(VERSION_CHECK_URL_IOS, { method: 'GET' });
}
Questa separazione è utile perché gli store e i numeri di versione possono differire tra le due piattaforme. Il backend restituisce una risposta con minimumVersion, latestVersion e updateUrl; il codice legge soprattutto minimumVersion per decidere se applicare il blocco.
La versione installata sul dispositivo viene recuperata con react-native-device-info:
const getAppVersion = () => {
const currentVersionStr = DeviceInfo.getVersion();
return parseVersion(currentVersionStr);
};
Il parser trasforma una stringa come 1.2.3 in tre numeri distinti:
const parseVersion = (versionString) => {
const [version = 0, build = 0, patch = 0] =
versionString.split('.').map(Number);
return { version, build, patch };
};
Il confronto attuale utilizza soltanto version e build:
if (current.version < minimum.version) {
mustBlock = true;
} else if (
current.version === minimum.version &&
current.build < minimum.build
) {
mustBlock = true;
}
Il terzo segmento, patch, viene sì parsato, ma non viene mai confrontato nella logica attuale. Se il progetto considera la patch rilevante ai fini del blocco, questa è una scelta da correggere o quantomeno da documentare esplicitamente. Non è un dettaglio puramente tecnico da trascurare: un numero di versione deve avere una semantica condivisa e coerente tra store, backend e app, altrimenti il confronto rischia di dire cose diverse a persone diverse.
La funzione, per gli errori, usa un fallback volutamente permissivo:
catch (error) {
console.warn('Errore controllo versione:', error);
return {
mustBlock: false,
showBanner: true,
updateUrl: null,
};
}
showBanner viene restituito dalla funzione, ma App.tsx non lo utilizza da nessuna parte. Di fatto, in caso di errore l’app non mostra alcun banner e non blocca l’accesso — è, anche qui, una scelta fail-open.
Può essere una scelta corretta se il servizio di versione è considerato accessorio, e il backend mantiene comunque compatibilità con le versioni precedenti. È invece rischiosa se il controllo serve a impedire accessi incompatibili, o a chiudere una vulnerabilità lato client. La policy deve restare esplicita e voluta, non l’effetto involontario di un semplice catch scritto in fretta.
C’è anche una fragilità nel parsing della risposta, che vale la pena segnalare:
const data = await response.json();
const versionInfo = JSON.parse(data);
Questo funziona soltanto se l’endpoint restituisce una stringa contenente JSON serializzato al suo interno. Se restituisce invece direttamente un oggetto JSON, il secondo JSON.parse() fallisce senza troppi complimenti. Il contratto della risposta deve essere definito con chiarezza, e verificato con test su entrambe le piattaforme prima di fidarsene ciecamente.
L’ordine finale del rendering
Dopo tutti gli effetti descritti finora, App.tsx decide finalmente cosa visualizzare, seguendo un ordine preciso.
Per prima cosa, il forced update:
if (mustBlock) {
return <VersionModal visible={true} updateUrl={updateUrl} />;
}
Nessun provider o navigator viene montato in questo caso. L’utente può soltanto aprire lo store, e nient’altro.
Poi la splash React Native:
if (isFirstScreen) {
return <SafeAreaView>{/* spinner */}</SafeAreaView>;
}
Questa fase protegge l’avvio mentre i controlli sono ancora effettivamente in corso, senza mostrare nulla di prematuro.
Segue l’offline:
if (isOffline) {
return (
<SafeAreaView style={globalStyles.errorContainer}>
<Text style={[globalStyles.errorText, globalStyles.antonRegular]}>
CONNESSIONE ASSENTE
</Text>
<Image
source={require('./src/assets/no-connection.png')}
style={globalStyles.noConnectionImage}
/>
</SafeAreaView>
);
}
L’app non monta il contenuto applicativo se non considera disponibile la rete in quel momento. È un blocco chiaro, ed è adatto a una WebView che semplicemente non può lavorare senza connessione.
E infine, solo a questo punto, l’app normale:
return (
<AppProviders>
<AppNavigator />
</AppProviders>
);
Soltanto qui vengono finalmente montati provider e navigazione. Questa separazione protegge l’applicazione principale da una parte consistente dei problemi di bootstrap: il navigator non deve mai sapere se la rete è stata verificata, o se la versione è stata bloccata — a lui arriva soltanto uno stato già pulito e affidabile.
Una macchina a stati più esplicita
I quattro booleani descritti finora funzionano, ma permettono combinazioni non sempre intuitive da leggere a colpo d’occhio. Per esempio, isFirstScreen e isOffline possono essere entrambi true allo stesso tempo; il rendering, in quel caso, sceglie comunque la splash, anche se sappiamo già che la rete manca. Non è necessariamente sbagliato come comportamento, ma il suo significato dipende dall’ordine degli if scritti nel file, invece che da un modello esplicito e leggibile a colpo d’occhio.
Un approccio alternativo potrebbe usare uno stato unico, tipizzato in modo più rigoroso:
type BootstrapState =
| { status: 'checking' }
| { status: 'offline' }
| { status: 'mustUpdate'; updateUrl: string }
| { status: 'ready' };
Il rendering, con questo modello, diventerebbe molto più leggibile a prima vista:
switch (bootstrap.status) {
case 'mustUpdate':
return <VersionModal updateUrl={bootstrap.updateUrl} visible />;
case 'checking':
return <SplashView />;
case 'offline':
return <OfflineView />;
default:
return <AppProviders><AppNavigator /></AppProviders>;
}
Non è necessario riscrivere subito il file per questo motivo. È però un modello utile da tenere a mente quando in futuro si aggiungeranno autenticazione iniziale, migrazioni locali, configurazione remota, manutenzione del servizio, o una vera modalità offline di sola lettura — tutte cose che, con quattro booleani slegati, diventerebbero rapidamente difficili da ragionare.
Cosa testare nel bootstrap
Il test più utile non è verificare soltanto che App.tsx esegua il render senza errori. Bisogna verificare davvero le transizioni: rete disponibile e versione supportata, che deve portare da splash ad app; rete assente e controllo versione fallito, che deve portare da splash a offline; rete disponibile ma versione sotto il minimo richiesto, che deve portare al forced update; updateUrl valido, che deve aprire correttamente lo store; updateUrl nullo, che deve essere gestito da un pulsante senza causare crash; rete persa dopo l’avvio, che deve mostrare la schermata offline; rete ripristinata, che deve riportare alla shell solo quando è davvero appropriato farlo; un evento NetInfo ripetuto, che non deve creare listener duplicati; l’app smontata durante il controllo, che non deve produrre aggiornamenti di stato non validi; una risposta versione già arrivata come oggetto JSON, che va parsata correttamente; una risposta versione con formato errato, che deve avere un fallback coerente; una versione con patch diversa, il cui comportamento va documentato esplicitamente; mustBlock attivo con rete ripristinata, dove la modale deve restare comunque prioritaria; e infine un server lento, dove la splash non deve restare infinita e deve esistere un timeout ben definito.
È importante provare tutto questo anche su dispositivi reali, non solo in emulazione, e verificare le transizioni tra background e foreground. Un’app spesso viene aperta, minimizzata proprio durante il caricamento, e riaperta in una condizione diversa da quella iniziale — è lì che i bug di bootstrap emergono davvero.
Miglioramenti possibili senza cambiare il prodotto
Il comportamento attuale può essere reso più robusto con alcuni interventi mirati, senza stravolgere nulla dell’esperienza esistente: usare la stessa definizione di connettività sia nel controllo iniziale sia nel listener, distinguere chiaramente il bootstrap terminato dalla semplice rete disponibile, verificare response.ok prima di eseguire il parsing della versione, gestire un payload JSON già arrivato come oggetto senza chiamare JSON.parse() inutilmente, decidere consapevolmente se confrontare anche il numero di patch, validare updateUrl prima di passarlo a Linking.openURL(), aggiungere un timeout esplicito al controllo remoto, rendere esplicita la policy fail-open o fail-closed invece di lasciarla implicita in un catch, usare AppState per rivalutare la rete quando l’app torna attiva, ed estrarre SplashView, OfflineView e la logica di bootstrap in file separati se il codice dovesse continuare a crescere.
Questi miglioramenti non trasformano automaticamente l’app in un client offline completo — quello richiederebbe un lavoro ben più ampio. Rendono soltanto più prevedibili gli stati iniziali e gli errori, che è già un passo avanti non da poco.
Conclusione
App.tsx è il punto in cui un’app React Native decide se mostrare un contenuto, attendere, bloccare, oppure spiegare un problema all’utente. Nel progetto myGenius, la sequenza attuale combina NetInfo, checkConnectionOnStart, react-native-splash-screen, checkVersions e VersionModal per costruire un gate iniziale semplice e, tutto sommato, leggibile.
Il valore dell’implementazione non sta nel numero di controlli effettuati, ma nella loro priorità reciproca: il forced update prevale sull’accesso, la splash copre l’intero bootstrap, l’offline impedisce di mostrare un contenuto che richiede rete, e soltanto alla fine di tutto vengono montati provider e navigator.
La parte più delicata, in definitiva, è rendere esplicite le decisioni che oggi restano implicite: che cosa significa davvero essere online, cosa succede se il server versione non risponde, quanto deve durare la splash, e quando il ritorno della rete consente davvero di entrare nell’app. Quando queste risposte sono documentate con chiarezza, App.tsx smette di essere un componente monolitico e diventa finalmente quello che dovrebbe essere fin dall’inizio: il coordinatore iniziale dell’intera applicazione.
Domande frequenti
Perché non montare subito AppNavigator e mostrare un loader sopra?
È possibile farlo, in teoria, ma il navigator potrebbe iniziare a eseguire effetti, richieste o auto-login prima che la shell sappia se la versione è supportata o se la rete è disponibile. Tenere il contenuto non montato rende il confine tra le due fasi molto più chiaro e sicuro.
Il forced update deve bloccare sempre l’app?
No. Deve bloccare soltanto quando la versione precedente non è più compatibile o sicura da usare. Per aggiornamenti non critici è preferibile un banner informativo o un flusso più flessibile che non impedisca l’uso dell’app.
NetInfo sostituisce una chiamata di health check?
No, e non dovrebbe mai essere trattato come tale. NetInfo osserva lo stato della rete del dispositivo, punto. Un health check applicativo verifica invece che il servizio necessario sia davvero raggiungibile e risponda correttamente — sono due controlli diversi, con scopi diversi.
Cosa succede se il controllo versione fallisce?
Nel codice attuale il fallback è fail-open: mustBlock diventa false e l’app può proseguire, mentre la schermata offline viene comunque gestita separatamente. È una policy che va confermata consapevolmente in base al rischio reale del progetto, non lasciata all’implicito.
Perché usare sia splash nativa sia splash React Native?
La splash nativa copre il tempo prima del mount della UI React Native, quando ancora nulla di JavaScript è in esecuzione. La vista React Native copre invece il periodo successivo, mentre i controlli asincroni sono ancora in corso ma il componente è già attivo.




