Search code examples
react-nativereact-navigation-v5

What is the best way to implement a loading screen with react navigation (I want to load data into context before navigation is displayed


I don't know if I'm asking this question right but here goes. I use the Context API for storing global state. When the app loads, I'm displaying a Splash Screen (I do this natively and I'm not building a managed app / Expo). In the background I want to load some data into the global context object (in this example it's UserProfileContext). When this is complete I will display the main navigation. I think the code will make it clear what I'm trying to do.

The problem is that I don't have access to the global context until I display the navigation routes because I use the Context objects to wrap the navigation component. How can I accomplish what I'm trying to do?

If there is a better way to load some data before choosing my route I am willing to change the structure of the navigation and/or app.

Here is my code for the navigation:

const Stack = createStackNavigator()
const Drawer = createDrawerNavigator()

function CheckinStack() {
   return (
      <Stack.Navigator headerMode={'none'}>
         <Stack.Screen
            name={'Search Locations'}
            component={SearchLocationsScreen}
         />

         <Stack.Screen
            name={'Check In Form'}
            component={CheckInFormScreen}
         />

         <Stack.Screen
            name={'Checked In'}
            component={CheckedInScreen}
         />

         <Stack.Screen
            name={'Business Details'}
            component={BusinessDetailsScreen}
         />
      </Stack.Navigator>
   )
}

function MainDrawer() {
    const {updateUserProfile} = useContext(UserProfileContext);
   const [isLoading, setIsLoading] = useState(true)

   const load = async () => {
      try {
            const profile = await retrieveUserProfile()
            profile && updateUserProfile(profile)

            setIsLoading(false)
      } catch (e) {
            
        }
   }

    if(isLoading){
        return <LoadingScreen setIsLoading={setIsLoading}/>
    }

   return (
      <Drawer.Navigator
         drawerStyle={{
            width: Dimensions.get('window').width > 600 ? '50%' : '70%',
            maxWidth: 400,
         }}
         drawerContent={(props) => <CustomDrawerContent {...props} dataLoaded />}>
         <Drawer.Screen name={'Search Locations'} component={CheckinStack} />
         <Drawer.Screen name={'About'} component={AboutScreen} />
         <Drawer.Screen name={'Favorites'} component={FavoritesScreen} />
         <Drawer.Screen name={'Profile'} component={ProfileScreen} />
         <Drawer.Screen name={'Report Issues'} component={ReportIssuesScreen} />
      </Drawer.Navigator>
   )
}

const NavContainer = () => {
   return (
      <NavigationContainer>
         <UserLocationProvider>
            <BusinessLocationsProvider>
               <UserProfileProvider>
                  <CheckInProvider>
                     <FavoritesProvider>
                        <MainDrawer />
                     </FavoritesProvider>
                  </CheckInProvider>
               </UserProfileProvider>
            </BusinessLocationsProvider>
         </UserLocationProvider>
      </NavigationContainer>
   )
}

Solution

  • Well, I don't know if this is the best way, but it's what I came up with.

    This is my Top level Navigator function:

    // Main Navigation -------------------
    const NavDrawer = ({route}) => {
       const [isLoading, setIsLoading] = useState(true)
    
       if (isLoading) {
          return <LoadingScreen setIsLoading={setIsLoading} />
       }
    
       return (
          <Drawer.Navigator
             initialRouteName="Search Locations"
             drawerStyle={styles.drawerStyle}
             backBehavior="firstRoute"
             drawerType="slide"
             drawerContent={(props) => <DrawerContent {...props} />}>
             <Drawer.Screen name="Search Locations" component={CheckIn} />
             <Drawer.Screen name="About" component={About} />
             <Drawer.Screen name='Favorites' component={Favorites} />
             <Drawer.Screen name='Profile' component={Profile} />
             <Drawer.Screen name='Report Issues' component={Issues} />
          </Drawer.Navigator>
       )
    }
    

    I implemented a LoadingScreen like this:

    const LoadingScreen = ({setIsLoading}) => {
        const { updateUserProfile } = useContext(UserProfileContext)
       const { loadFavorites } = useContext(FavoriteLocationsContext)
       const { checkInUser } = useContext(CheckInContext)
    
        const loadAppData = async () => {
          try {
             const profile = await retrieveUserProfile()
    
             if (profile) {
                updateUserProfile(profile)
             }
             const favorites = await retrieveFavorites()
    
             if (favorites) {
                loadFavorites(favorites)
             }
             const checkinData = await retrieveCheckinData()
    
             if (checkinData && checkinData.checkedIn) {
                checkInUser(checkinData)
             }
          } catch (e) {
             throw e
          }
       }
    
        useEffect(() => {
          loadAppData()
             .then(() => {
                setIsLoading(false)
             })
             .catch((e) => {
                console.log('LoadingScreen: ', e.message)
                setIsLoading(false)
             })
       }, [])
    
       return null
    
    }
    
    export default LoadingScreen
    

    I think the code is self-explanatory. I hope this helps someone and I will change the accepted answer if someone has a better suggestion.