BROAD PROBLEM (for individuals searching for the same solution):
I have an app with a switch-based StackNavigator ("MainStack"). I would like to reset MainStack as {index: 1, routes: [{name: 'Home', name: 'NestedStack'}]}, and I would like to call this from within the MainStack component (see below)
function App() {
return (
<NavigationContainer>
<Stack.Navigator>
{
anon ?
<Stack.Screen name="Auth" component={AuthScreen} />
<Stack.Screen name="MainStack" component={MainStackScreen} />
}
</Stack.Navigator>
</NavigationContainer>
);
}
const MainStack = createStackNavigator();
function MainStackScreen({navigation, route}) {
*** An event occurs here ***
*** And I'd like to respond to the event with a navigation.reset() ***
return (
<MainStack.Navigator>
<MainStack.Screen name="Home" component={Home} />
<MainStack.Screen name="NestedStack" component={NestedStackScreen} />
</MainStack.Navigator>
);
}
I have tried several different combinations of navigation.dispatch({...CommonActions.reset({}), target}), producing different errors from the screen not existing to issues with screens.options.
I would also greatly appreciate advice on how to specify which screen to start with for NestedStack... would this just be {name: 'NestedStack', screen: 'NestedScreen'}??
SPECIFIC USE-CASE:
What I am really trying to achieve is Linking. I have users opening links when the app is closed (which I am accessing via Linking.getInitialURL) and from Camera screens within the app (which I am listening to via Linking.addEventListener). I have placed both of these in the MainStack component to intercept links as the app switches from the auth flow and as a QR code is scanned within the app.
I have to listen to changes in the MainStackScreen route, then set a variable isNavigatorMounted to true once the route is fully mounted, and then that triggers my check for Linking.getInitialURL.
Please address the Broad Question for general StackOverflow users, but if this is a terribly inefficient method, please let me know.
This answer was provided https://discord.com/channels/102860784329052160/288388742929055765/770038660220715028
You do not need CommonActions or anything. Simply apply the following to the StackNavigator's navigation object:
navigation.reset({
routes: [{ name: 'Home' }, { name: 'NestedStack' }],
});
Related
I have a React Native Expo application using React Navigation. I think I have all the navigation routes (links) not well setted up.
So in my app let's say I have a Profile screen which has 2 list items to navigate. Application settings & User settings. These two have some more items inside each one.
My navigation tree would be something like:
ProfileScreen (screen with 2 items => User settings | Application settings)
UserSettings:
Language (Navigation to LanguageScreen)
Change password (Navigation to ChangePasswordScreen)
Personal data (Navigation to PersonalDataScreen)
ApplicationSettings:
...
My LinkingConfiguration is configured like:
profileStack = {
initialRouteName: "profile",
screens: {
profile: "profile",
userSettings: "profile/userSettings",
language: "profile/userSettings/language",
changePassword: "profile/userSettings/password",
personalData: "profile/userSettings/personalData",
applicationSettings: "profile/applicationSettings"
}
}
profileStack is a navigation stack. The rest are screens.
I'm wondering how I need to setup well the tree, because if I deep link to myApp.com/profile/userSettings/language, if I go back, I go directly to the initialRouteName which is profile, and I guess this is because the tree is not well generated.
In angular router would be setted as
profile: path: "...", children: [ userSettings: path: "...", children: [ {...}]]
How does navigation tree have to be setted up correctly in my example? Maybe using many navigation stacks?
Can anybody enlight me?
FYI:
React Navigation: v6 (it was also happenning in v5)
React Native: v0.69.5
Expo: v46.0.9
React Navigation allows you to set up the same hierarchy you showed in your example. You want to nest the navigators just as in your depiction.
https://reactnavigation.org/docs/nesting-navigators
Something like:
function Profile() {
return (
<Stack.Navigator>
<Stack.Screen name="UserSettings" component={UserSettings} />
<Stack.Screen name="ApplicationSettings" component={ApplicationSettings} />
</Stack.Navigator>
);
}
function UserSettings() {
return (
<Stack.Navigator>
<Stack.Screen name="Language" component={Language} />
<Stack.Screen name="ChangePassword" component={ChangePassword} />
<Stack.Screen name="PersonalData" component={PersonalData} />
</Stack.Navigator>
);
}
function App() {
return (
<NavigationContainer>
<Profile />
</NavigationContainer>
);
}
The structure of navigation is created by how you nest components.
What I want is to protect specific screens where the user can run specific actions and only at that point redirect him to a login screen, if the user is successfully authenticated, redirect him to the point where he left off the flow.
We can do this in React JS(with react-router), so I would like to know if its posible to implement a similar solution for react-native(with react-navigation):
React JS approach with React Router
function RequireAuth({ children }) {
const { authed } = useAuth();
const location = useLocation();
return authed === true ? (
children
) : (
<Navigate to="/login" replace state={{ path: location.pathname }} />
);
}
This is possible in react native and is documented in the authentication workflow section of the documentation.
In summary we conditionally need to render screens inside the navigator. Here is a minimal example using a Stack.Navigator which is similar to your posted code snippet. The workflow is identical for other navigators (nested or not). I will use the context api in my example, but there are other ways to achieve the same as well. The overall pattern is the same.
const AppContext = React.createContext()
function App() {
const [isSignedIn, setIsSignedIn] = React.useState(false)
const appContextValue = useMemo(
() => ({
isSignedIn,
setIsSignedIn,
}),
[isSignedIn]
)
return (
<AppContext.Provider value={appContextValue}>
<Stack.Navigator>
{!isSignedIn ? (
<Stack.Screen
name="Login"
component={LoginScreen}
/>
) : (
// whatever screens if user is logged in
<Stack.Screen name="Home" component={HomeScreen} />
)}
</Stack.Navigator>
</AppContext.Provider>
);
In the LoginScreen, you then need to change the authed state to true if the login was successful. The first screen in the conditional will then be initially rendered automatically (in this case HomeScreen).
function LoginScreen() {
const { setIsSignedIn } = useContext(AppContext)
// do whatever, then setIsSignedIn to true
}
I'm using react navigation 5 (with react native) and my navigation set up looks like this:
// Root
<NavigationContainer>
<RootStack.Navigator mode="modal" headerMode="none">
<RootStack.Screen name="Auth" component={AuthStackNavigator} />
<RootStack.Screen name="App" component={BottomTabsNavigator} />
</RootStack.Navigator>
</NavigationContainer>
// App
<Tab.Navigator tabBar={bottomTabBar}>
<Tab.Screen name="ScreenA1" component={StackA} />
<Tab.Screen name="ScreenB1" component={StackB} />
</Tab.Navigator>
// TAB A
<StackA.Navigator headerMode="none">
<StackA.Screen name="ScreenA1" component={ScreenA1} />
<StackA.Screen name="ScreenA2" component={ScreenA2} />
</StackA.Navigator>
// TAB B
<StackB.Navigator headerMode="none">
<StackB.Screen name="ScreenB1" component={ScreenB2} />
<StackB.Screen name="ScreenB2" component={ScreenB2} />
</StackB.Navigator>
So inside my App i have 2 Bottom Tabs (Tab A and Tab B) each having 2 nested screens (Screen A1>A2 and B1>B2).
So when i tap on Tab A i go to ScreenA1 then in there i can move on to ScreenA2. Same for Tab B.
Now on ScreenB2 from Tab B i have a button that should navigate the user to Screen A2 with some data to prefill on that screen.
If i do it like this:
navigation.navigate('ScreenA1', {
screen: 'ScreenA2',
params: { data },
});
I'll land on ScreenA2 with the data, but:
If i visited ScreenA2 before, the previous state persists and i can see its old state and data.
If i never visited ScreenA2 before it's now not inside the Tab A Stack, but instead pushed on top of ScreenB2.
So i guess i would need to reset the screen before i navigate to it (or unmount whenever i leave it) and also make sure that it gets put inside the Tab A Stack on creation, so when i try to call goBack from Screen A2 i land on Screen A1. Maybe navigate back to root first and call the screen from there without the user seeing all screens flashing up.
You can replace
navigation.navigate('ScreenA1'...
By
navigate.push('ScreenA1'...
That way, a new route will be added to your navigation stack, with new state and props.
Resolved it with this:
navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [
{
name: 'ScreenA1',
state: {
routes: [
{
name: 'ScreenA1',
},
{
name: 'ScreenA2',
params: { data },
},
],
},
},
],
}),
);
Notice how i have 'ScreenA1' twice in there. I guess that's necessary because the Navigator and the first Screen inside the Navigator share the same route name
I have a tabbar looking like the one below. inside every tab I have a nested stack navigator.
I want to hide the tabbar whenever the stack is not on its initial route. In the documentation, it is stated like one is supposed to place the stack in the initial stack navigator.
https://reactnavigation.org/docs/hiding-tabbar-in-screens/
However, this does not make sense and as I have my signup and all the routes where the customer is not logged in there I think it is counter-intuitive to place the entire application in one stack navigator. Further, it diminishes the purpose of having a tab-navigation in the first place.
I have also tried the tabBarVisible prop and change this conditionally. However, this provides me with some issues.
First, it does not seem to work on mu custom tabBar. Further, it gives me the state of every tab for every render. Thus it causes some unwanted behavior. lastly, it is not recommended from the documentation.
<Tab.Navigator
tabBar={props => (
<TabBar
onPress={tabIndex => changeTab(tabIndex, props)}
display={false}
values={[
{ title: "Balance", icon: 'view-dashboard'},
{ title: "Faktura", icon: 'file-pdf'},
{ title: "Rabatter", icon: 'gift'},
{ title: "Profil", icon: 'account-details'},
{ title: "Mere", icon: 'dots-horizontal' }
]}/>
)}>
<Tab.Screen
name="Balance"
component={BalanceStackNavigator}
/>
<Tab.Screen
name="Invoice"
component={InvoiceStackNavigator}
/>
<Tab.Screen
name="BlueBenefit"
component={BlueBenefitStackNavigator}
/>
<Tab.Screen
name="User"
component={UserStackNavigator}
/>
<Tab.Screen
name="CrossSale"
component={CrossSaleStackNavigator}
/>
</Tab.Navigator>
So basically my question is, how I achieve this behavior of hiding the tabs when not on the initial route in the best way?
I ended up using what was recommended by the documentation, which is to place all of my screens in my app stack instead of the individual nested stacks.
I have a login screen which I have placed in stack. After user logs in successfully he is redirected to home screen which is a drawer screen. One of the options of drawer screen is logout, so on click of it user should be logged out. Following is my code for logout screen. I am just showing a progress bar in logout screen in ui but in useEffect hook, I am calling the following code
navigation.reset({
routes: [{name: LOGIN_SCREEN}],
});
I also tried calling the above method in useLayoutEffect but then the logout button just hangs.
My Navigation stack looks something as follows
<Stack.Navigator>
<Stack.Screen
name={LOGIN_SCREEN}
component={LoginScreen}
options={{headerShown: false}}
/>
<Stack.Screen
name={HOME_STACK_SCREEN}
component={DrawerStack}
options={{headerShown: false}}
/>
// ...
</Stack.Navigator>
My drawer component as follows
<Drawer.Navigator
drawerStyle={{backgroundColor: BLUE_COLOR_1}}
drawerContentOptions={{labelStyle: {color: '#FFF'}}}
>
<Drawer.Screen
name={HOME_SCREEN}
component={Home}
options={{
// ...
}}
/>
<Drawer.Screen
name={LOGOUT_SCREEN}
component={Logout}
options={{
// ...
}}
/>
// ...
</Drawer.Navigator>
Following is my logout component
const Logout = ({navigation}) => {
async function logout() {
try {
await AsyncStorage.clear();
navigation.reset({
index: 0,
routes: [{name: LOGIN_SCREEN}],
});
} catch (e) {
Alert.alert(e.toString());
console.log(e.toString());
}
}
useEffect(() => {
logout();
}, []);
return <ProgressBar />;
};
Invariant Violation: Maximum update depth exceeded using react Navigation v5 occurs when something going in an infinite loop
navigation.reset({
routes: [{name: LOGIN_SCREEN}],
});
When you say you put the above code in useEffect, I suspect that hook is getting called multiple times.
I saw you code, to be honest it does't look good to me if we think of auth flow, Here are reason why I am saying this. Do check this link step by step , you get to know how simply you can achieve this auth flow in proper manner. React Navigation Auth Flow, this problem comes when we try to navigate screen from android back button , user can go back to login after you navigate user to home page on success login
Second you can try one condition in you current code.
You need to check one condition before calling the logout function in you screen
useEffect(()=>{
if(check if user value if still there in async storage ) {
**Here you can excute you code for log out **
}
}
And instead of using reset action from navigation use popToTop , Here you can find the ref how you can us PopToTop or use this
navigation.popToTop()
Cheers !!