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

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

Related

React navigation 6 ( navigate to another stack and specific screen)

I'm using react navigate V6 and I want to move between stacks , but this code is not working :
navigation.navigate("Stack Name", { screen: "screen Name" });
#react-navigation/native is not working with V6
work for me
import { createStackNavigator } from '#react-navigation/stack';
const MyStack = createStackNavigator();
const SettingsStack = createStackNavigator();
function HomeStackScreen() {
return (
<MyStack.Navigator>
<MyStack.Screen name="Home" component={HomeScreen} />
<MyStack.Screen name="Details" component={DetailsScreen} />
</MyStack.Navigator>
);
}
function SettingsStackScreen() {
return (
<SettingsStack.Navigator>
<SettingsStack.Screen name="Settings" component={SettingsScreen} />
<SettingsStack.Screen name="Profile" component={ProfileScreen} />
</SettingsStack.Navigator>
);
}
import { useNavigation } from '#react-navigation/native';
function HomeScreen() {
const navigation = useNavigation();
const handlePress = () => {
navigation.navigate('Settings', { screen: 'Profile' });
};
return (
<Button title="Go to Profile" onPress={handlePress} />
);
}

React native trying to use navigation before mounted

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>

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 reset tab stack when you come back to a tab from another tab react navigation v5

I have 3 tabs and each tab contains a set of stack navigators.
Home Stack
const HomeNavigator = createStackNavigator();
const HomeStackNavigator = ({navigation, route}) => {
return (
<HomeNavigator.Navigator>
<HomeNavigator.Screen
name="Home"
component={Home}
/>
<HomeNavigator.Screen
name="Profile"
component={Profile}
/>
<HomeNavigator.Screen
name="Settings"
component={Settings}
/>
</HomeNavigator.Navigator>
);
};
Store Stack
const StoreNavigator = createStackNavigator();
const StoreStackNavigator = ({navigation, route}) => {
return (
<StoreNavigator.Navigator>
<StoreNavigator.Screen
name="OurStore"
component={Store}
/>
</StoreNavigator.Navigator>
);
};
Community Stack
const CommunityNavigator = createStackNavigator();
const CommunityStackNavigator = ({navigation, route}) => {
return (
<CommunityNavigator.Navigator>
<CommunityNavigator.Screen
name="Community"
component={Community}
/>
<CommunityNavigator.Screen
name="CommunityReply"
component={CommunityReply}
options={communityReplyOptions}
/>
<CommunityNavigator.Screen
name="AskCommunity"
component={AskCommunity}
/>
</CommunityNavigator.Navigator>
);
};
Tab Navigator
const MainNavigator = createBottomTabNavigator();
const MainTabNavigator = () => {
return (
<MainNavigator.Navigator
screenOptions={tabScreenOptions}
tabBarOptions={tabBarOptions}>
<MainNavigator.Screen
name="HomeTab"
component={HomeStackNavigator}
options={{tabBarLabel: 'Home'}}
/>
<MainNavigator.Screen
name="StoreTab"
component={StoreStackNavigator}
options={{tabBarLabel: 'Store'}}
/>
<MainNavigator.Screen
name="CommunityTab"
component={CommunityStackNavigator}
options={{tabBarLabel: 'Community'}}
/>
</MainNavigator.Navigator>
);
};
I navigated to CommunityReply Screen which is inside CommunityTab tab from HomeTab by clicking a button using the below approach
props.navigation.navigate('CommunityTab', {
screen: 'CommunityReply',
params: {postId: postId},
});
It's working fine, when I again come back to CommunityTab it will always be in CommunityReply Screen. How to reset tab stacks when you come back to a CommunityTab tab
React Navigation Versions
"#react-navigation/bottom-tabs": "^5.8.0"
"#react-navigation/native": "^5.7.3"
"#react-navigation/stack": "^5.9.0"
There is a property called unmountOnBlur designed for this purpose.
https://reactnavigation.org/docs/bottom-tab-navigator#unmountonblur
Changes to make in Tab Navigator :
const MainNavigator = createBottomTabNavigator();
const MainTabNavigator = () => {
return (
<MainNavigator.Navigator
- screenOptions={tabScreenOptions}
+ screenOptions={{...tabScreenOptions, unmountOnBlur: true }}
tabBarOptions={tabBarOptions}>
<MainNavigator.Screen
name="HomeTab"
component={HomeStackNavigator}
options={{tabBarLabel: 'Home'}}
/>
<MainNavigator.Screen
name="StoreTab"
component={StoreStackNavigator}
options={{tabBarLabel: 'Store'}}
/>
<MainNavigator.Screen
name="CommunityTab"
component={CommunityStackNavigator}
options={{tabBarLabel: 'Community'}}
/>
</MainNavigator.Navigator>
);
};
Summary:
unmountOnBlur: true will solve the problem mentioned.
If you don't want the CommunityReply screen to show when you navigate back to the CommunityTab you need to add an initial option within your navigate function.
props.navigation.navigate(
'CommunityTab',
{
screen: 'CommunityReply',
initial: false,
params: { postId: postId },
}
);
Explained here in the docs
I made a more generic approach to Jeison GuimarĂ£es solution (react navigation 6). This approach resets any stack navigation within a tab when the tab index changes. This also makes sure the active stack is not reset when a popover is shown.
<Tab.Screen
name="My tab"
listeners={resetTabStacksOnBlur}
/>
/**
* Resets tabs with stackNavigators to the first route when navigation to another tab
*/
const resetTabStacksOnBlur = ({navigation}) => ({
blur: () => {
const state = navigation.getState();
state.routes.forEach((route, tabIndex) => {
if (state?.index !== tabIndex && route.state?.index > 0) {
navigation.dispatch(StackActions.popToTop());
}
});
},
});
If the solution above not working for you, try this solution, its work for me.
import { CommonActions } from '#react-navigation/native';
<Tab.Screen listeners={({navigation,route})=>({
blur:()=>{
navigation.dispatch(
CommonActions.reset({
index:4,
routes:[{name:'Profile'}]
})
)
},
})} name="Profile"/>