React native trying to use navigation before mounted - react-native

I get the following error:
ERROR The 'navigation' object hasn't been initialized yet.
This might happen if you don't have a navigator mounted,
or if the navigator hasn't finished mounting.
See https://reactnavigation.org/docs/navigating-without-navigation-prop#handling-initialization
for more details.
I initially had the following:
const Stack = createNativeStackNavigator();
export default function App() {
const navigation = useNavigation();
...
useEffect(() => {
const subscriber = auth.onAuthStateChanged((_user) => {
if (!_user) {
navigation.navigate('Login');
...
}
else {
navigation.navigate('CoreTabs', { screen: 'Home' })
}
});
return subscriber;
},[]);
I adjusted my code as follows (following instructions in URL provided with error.)
const Stack = createNativeStackNavigator();
export default function App() {
// const navigation = useNavigation(); // removed
const navigationRef = useNavigationContainerRef(); // added
...
useEffect(() => {
const subscriber = auth.onAuthStateChanged((_user) => {
if (!_user) {
// navigation.navigate('Login'); // removed
if (navigationRef.isReady) navigationRef.navigate('Login'); // added
}
else {
...
navigation.navigate('CoreTabs', { screen: 'Home' }) // removed
if (navigationRef.isReady) navigationRef.navigate('CoreTabs', { screen: 'Home' }) // added
}
});
return subscriber;
},[]);
However I continue to receive the error. Here's how my NavigationContainer is setup.
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Group>
{ user && <Stack.Screen name="CoreTabs" component={CoreTabs} /> }
<Stack.Screen name="Login" component={LoginScreen} }}/>
<Stack.Screen name="Registration" component={RegistrationScreen}/>
<Stack.Screen name="Reset Password" component={ResetPasswordScreen}/>
</Stack.Group>
<Stack.Group screenOptions={{ presentation: 'modal' }}>
<Stack.Screen name="Notifications" component={NotificationsScreen} />
<Stack.Screen name="Edit Profile" component={ProfileEditScreen} />
</Stack.Group>
</Stack.Navigator>
</NavigationContainer>
);

I don't think you can use the useNavigationContainerRef hook outside the NavigationContainer.
You can get the navigationContainerRef like this.
const navigationRef = useRef()
return <NavigationContainer ref={navigationRef}>...</NavigationContainer>

Related

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>
)
}
Maintain a isLoading state inside the context. And inside your Context, conditionally render some Loading component or {props.children} depending on isLoading state. Initialize isLoading as true after request completes, set it to false. You will have to make the request inside the context however.
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.

How to hide some pages from Drawer Navigation but still be able to navigate to them - React Native?

I want to hide some pages from the Drawer
I want to hide some pages from the Drawer (for example hide the SignUpPage and SuccessPage), how can I do it ?
i also tried to make an anonymous function in the DrawerLabel [ ()=> null ] but it is still not a good solution because even tho it shows me an empty label, yet when i click on it , it navigates me to the page that i wanted to hide.
Please help
and thanks for all the helpers :)
import { createDrawerNavigator } from '#react-navigation/drawer';
const Drawer = createDrawerNavigator();
function DrawerNavigator() {
return (
<Drawer.Navigator initialRouteName="WelcomePage">
//...all the pages
<Drawer.Screen
name="HomePage"
component={HomePage}
options={{ drawerLabel: 'Home Page' }}
/>
<Drawer.Screen
name="SignUpPage"
component={SignUpPage}
options={{ drawerLabel: 'SignUp Page' }}
/>
<Drawer.Screen
name="SuccessPage"
component={SuccessPage}
options={{ drawerLabel: 'SuccessPage' }}
/>
</Drawer.Navigator>
);
}
const Stack = createStackNavigator();
export default function App() {
return (
< NavigationContainer >
<DrawerNavigator>
<Stack.Navigator initialRouterName="WelcomePage">
<Stack.Screen name="WelcomePage" component={WelcomePage} />
>
<Stack.Screen name="SuccessPage" component={SuccessPage} />
<Stack.Screen name="HomePage" component={HomePage} />
</Stack.Navigator>
</DrawerNavigator>
</NavigationContainer >
);
}
You have differents options
I guess you want to hide that options when your user is signed or not. With v5 you can do the code below. The another option is the same but playing with custom content that is a bit complex also I give you the docs if you want the complex solution https://reactnavigation.org/docs/drawer-navigator.
DrawerNavigator
const Drawer = createDrawerNavigator();
function DrawerNavigator() {
return (
<Drawer.Navigator initialRouteName="WelcomePage">
//...all the pages
<Drawer.Screen
name="HomePage"
component={HomePage}
options={{ drawerLabel: 'Home Page' }}
/>
</Drawer.Navigator>
);
}
AuthNavigator
const Stack = createStackNavigator<AuthParamList>();
export const AuthNavigator = () => {
return (
<Stack.Navigator headerMode='none'>
<Stack.Screen name='SignUpPage' component={SignUpPage}></Stack.Screen>
<Stack.Screen name='SuccessPage' component={SuccessPage}></Stack.Screen>
</Stack.Navigator>
);
};
IsAuthScreen, I use firebase + redux so here you need to put your login logic
const IsAuth: React.FC<RoutesProps> = (props) => {
const { eva, ...rest } = props;
const dispatch = useDispatch();
const onAuthStateChanged = (currentUser: any) => {
console.log("onAuthStateChanged -> currentUser", currentUser)
if (!currentUser) {
dispatch(new authActions.DidTryLogin());
} else {
if (!currentUser.emailVerified) {
dispatch(new authActions.DidTryLogin());
} else {
dispatch(new authActions.SigninSuccess(currentUser));
dispatch(new settingsActions.GetProfile(currentUser.uid));
}
}
};
useEffect(() => {
const subscriber = firebase.auth().onAuthStateChanged(onAuthStateChanged);
return () => {
subscriber();
}; // unsubscribe on unmount
}, [dispatch]);
return (<View >
<LoadingIndicator size='large' /> // Here put a loading component
</View>);
};
App component, I use redux for check if my user is logged so here you need to put your own logic
const isAuth = useSelector(selectAuthUser);
const didTryAutoLogin = useSelector(selectAuthDidTryLogin);
return (
<NavigationContainer>
{isAuth && <DrawerNavigator />}
{!isAuth && didTryAutoLogin && && <AuthNavigator />}
{!isAuth && !didTryAutoLogin && <IsAuthScreen />}
</NavigationContainer>);
So when you logout you don't need to navigate to SignInScreen (this will be a problem if you think about it because if you want to do that you can back to protected screens with the back button or gesture). You only need to update the state and the correct navigator will be in place and you can put the default screen to show. You can achieve this with redux or react context.

How to join 2 stack navigators in React Native

I have 2 stack navigators that handles 2 diferrent slices of my app, one will be the authentication and the other will be the app itself. I would like to put these 2 stack navigators in different files and one file to join both. Is there a way to do this?
MainNavigation code
import React from 'react'
import {NavigationContainer} from '#react-navigation/native'
import AuthNav from './AuthNav'
import AppNav from './AppNav'
const MainNav = () => {
return (
<NavigationContainer>
<AuthNav />
<AppNav />
</NavigationContainer>
)
}
export default MainNav
AppNavigation code
const Stack = createStackNavigator<AppNavParams>()
const AppNav = () => {
return (
<Stack.Navigator>
<Stack.Screen name="Register">
{props => <RegisterScreen {...props} />}
</Stack.Screen>
</Stack.Navigator>
)
}
AuthNavigation Code
const Stack = createStackNavigator<AppNavParams>()
const AuthNav = () => {
return (
<Stack.Navigator>
<Stack.Screen name="Login">
{props => <RegisterScreen {...props} />}
</Stack.Screen>
</Stack.Navigator>
)
}
edit1: removed image and inserted code
You just need to verify if your authenticated token exist or not, in your MainNav
And create a stack navigator around both AppNav and AuthNav
You can do something like this
import React, { useState, useEffect, useRef } from 'react'
import { NavigationContainer, useLinking } from '#react-navigation/native'
import { createStackNavigator } from '#react-navigation/stack'
import AppNav from './AppNav'
import AuthNav from './AuthNav'
const AppStack = createStackNavigator()
const MainNav() => {
const someFn = () => {
// write your logic here. Either retrieve from redux store or from local storage
// return true or false
}
const isLoggedIn = someFn()
return (
<NavigationContainer>
<AppStack.Navigator>
{isLoggedIn ? (
<AppStack.Screen name='App' component={AppNav} />
) : (
<AppStack.Screen name='Auth' component={AuthNav} />
)}
</AppStack.Navigator>
</NavigationContainer>
)
}
You need to create an auth stack and user stack and render based on sign state.
Create your auth stack.
const Stack = createStackNavigator();
export default function AuthStack() {
return (
<Stack.Navigator>
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen name="Register" component={RegisterScreen} />
</Stack.Navigator>
);
}
Create your user stack.
const Stack = createStackNavigator();
export default function UserStack() {
return (
<Stack.Navigator>
<Stack.Screen name="User" component={UserScreen} />
<Stack.Screen name="Settings" component={SettingsScreen} />
</Stack.Navigator>
);
}
Render auth or user based on sign state.
export default function Account({ navigation, route }) {
const [sign, setSign] = useState(false);
useEffect(() => {
// some logic to check user sign
}, []);
if (sign) {
return <UserStack />;
} else {
return <AuthStack />;
}
}
Wrap with navigation container
<NavigationContainer>
<Account/>
</NavigationContainer>

Why after login my react-navigation routing does not work properly?

I have navigation container(created in react-navigation)
const AppStack = createStackNavigator();
const AppStackScreen = () => (
<AppStack.Navigator>
<AppStack.Screen name="Tabbed" component={TabsScreenNavigationScreen} />
</AppStack.Navigator>
);
class AppNavigationContainer extends Component {
constructor(props) {
super(props);
this.state = {
appLoading : true,
}
}
user = {};
componentDidMount() {
let _this = this;
this.getUser()
.then(() => {
this.setState({appLoading: !_this.state.appLoading})
})
}
getUser = async () => {
return await AsyncStoreService.getUserFromStore()
.then((user) => {
this.user = user;
});
}
render() {
const user = this.user;
const {
appLoading
} = this.state;
return (
<NavigationContainer>
{appLoading ?
<SplashScreen/>
:
<>
{user ?
<AppStackScreen/>
:
<AuthStackNavigationScreen/>
}
</>
}
</NavigationContainer>
)
}
}
export default AppNavigationContainer;
How can you see I have separated modules for app and login. login router:
const AuthStack = createStackNavigator();
const AuthStackNavigationScreen = () => (
<AuthStack.Navigator>
<AuthStack.Screen
name="ChooseRole"
component={SelectRoleScreen}
options={{title: false}}
/>
<AuthStack.Screen
name="AuthStart"
component={MainScreen}
options={{title: false}}
/>
<AuthStack.Screen
name="SignIn"
component={LoginScreen }
options={{title: 'Sign In'}}
/>
<AuthStack.Screen
name="CreateAccount"
component={RegisterScreen}
options={{title: 'Create Account'}}
/>
</AuthStack.Navigator>
);
export default AuthStackNavigationScreen;
Tabbed router for app:
const GalleryStack = createStackNavigator();
const SearchStack = createStackNavigator();
const MessagesStack = createStackNavigator();
const MenuStack = createStackNavigator();
const Tabs = createBottomTabNavigator();
const GalleryStackScreen = () => (
<GalleryStack.Navigator>
<GalleryStack.Screen name="Gallery" component={GalleryScreen} />
<GalleryStack.Screen name="GalleryItem" component={GalleryItemScreen} />
</GalleryStack.Navigator>
);
const SearchStackScreen = () => (
<SearchStack.Navigator>
<SearchStack.Screen name="Search" component={SearchScreen} />
<SearchStack.Screen name="SearchResults" component={SearchResultsScreen} />
</SearchStack.Navigator>
);
const MessagesStackScreen = () => (
<MessagesStack.Navigator>
<MessagesStack.Screen name="ConversationList" component={ConversationListScreen} />
<MessagesStack.Screen name="Conversation" component={ConversationScreen} />
</MessagesStack.Navigator>
);
const MenuStackScreen = () => (
<MenuStack.Navigator>
<MenuStack.Screen name="Menu" component={MenuScreen} />
<MenuStack.Screen name="About" component={AboutScreen} />
</MenuStack.Navigator>
);
const TabsScreenNavigationScreen = () => (
<Tabs.Navigator>
<Tabs.Screen name="Gallery" component={GalleryStackScreen} />
<Tabs.Screen name="Search" component={SearchStackScreen} />
<Tabs.Screen name="Messages" component={MessagesStackScreen} />
<Tabs.Screen name="Menu" component={MenuStackScreen} />
</Tabs.Navigator>
);
export default TabsScreenNavigationScreen;
So on login screen name="SignIn" I login, perform navigation.navigate('Tabbed') after succesfully login and get message:
The action 'NAVIGATE' with payload {"name":"Tabbed"} was not handled by any navigator.
Do you have a screen named 'Tabbed'?
He doesnt 'see' my tab navigation. Why it happens so(I have such screen name and put it to render), and how can I fix this?
According to the stack you have you will either have the appstack or the authstack at a given moment
<>
{user ?
<AppStackScreen/>
:
<AuthStackNavigationScreen/>
}
</>
So you cant navigate to tabbed from signin screen which does not exist.
The way you can handle this is update the user object maybe using a callback function or use context instead of state which will trigger a render of the AppNavigationContainer and the tabbed stack will automatically rendered instead of the auth stack. You wont need a navigate you should do the same for logout to where you will set the user to null.
You can refer more on Auth flows

switch navigator in react-navigation v5

There is a good example of switch navigator in V4 documentation of react-navigation:
https://snack.expo.io/#react-navigation/auth-flow-v3
I didn't understand how can I change this into a proper way for V5. here is the link:
https://reactnavigation.org/docs/en/upgrading-from-4.x.html#switch-navigator
Any assistance you can provide would be greatly appreciated.
Well, if you want to implement a "switch" like feature with V5, you need to opt into using the new <Stack.Navigator /> definition. Basically, the <Stack.Navigator /> can have children which are <Stack.Screen /> and anytime you explicitly switch between them (or set them to null), it will animate between them. You can find more documentation on how to do this here: https://reactnavigation.org/docs/auth-flow
There is no createSwitchNavigator in React-Navigation v5.
So it can be implement as below.
App.js
// login flow
const Auth = createStackNavigator();
const AuthStack =()=> (
<Auth.Navigator
initialRouteName="Login"
screenOptions={{
animationEnabled: false
}}
headerMode='none'
>
<Auth.Screen name="Login" component={LoginScreen} />
<Auth.Screen name="Signup" component={SignupScreen} />
</Auth.Navigator>
)
// drawer use only in authenticated screens
const Drawer = createDrawerNavigator();
const DrawerStack = () => (
<Drawer.Navigator initialRouteName="Home">
<Drawer.Screen name="Home" component={HomeScreen} />
<Drawer.Screen name="Settings" component={SettingsScreen} />
<Drawer.Screen name="Logout" component={LogoutScreen}/>
</Drawer.Navigator>
)
const RootStack = createStackNavigator();
class App extends Component {
constructor() {
super();
this.state = {
loading: true,
hasToken: false,
};
}
componentDidMount(){
AsyncStorage.getItem('jwtToken').then((token) => {
this.setState({ hasToken: token !== null,loading:false})
})
}
render() {
const {loading,hasToken} = this.state;
if (loading) {
return <WelcomeScreen/>
}
else {
return(
<NavigationContainer>
<RootStack.Navigator headerMode="none">
{ !hasToken ?
<RootStack.Screen name='Auth' component={AuthStack}/>
:
RootStack.Screen name='App' component={DrawerStack}/>
}
</RootStack.Navigator>
</NavigationContainer>
);
}
}
}
export default App;
This is only one way of doing authentication.I haven't use Redux or React Hooks here.You can see the example which used React Hooks in the React Navigation v5 documentation.
I think I found a solution for this issue which will block all unwanted navigation from the user and still keep smooth transitions between screens.
You need to add navigation.service.js like this:
import * as React from 'react';
export const navigationRef = React.createRef();
export const navigate = (routeName, params) => {
navigationRef.current?.navigate(routeName, params);
}
export const changeStack = (stackName) => {
resetRoot(stackName)
}
const resetRoot = (routeName) => {
navigationRef.current?.resetRoot({
index: 0,
routes: [{ name: routeName }],
});
}
add that ref from navigationService to NavigationContainer like this:
<NavigationContainer ref={navigationService.navigationRef}>
<Navigation />
</NavigationContainer>
now when you know you need to change stack, just call changeStack instead of navigate.
I have explained this solution in more details here:
Change stacks in react-navigation v5