Suppose two stack screens in a Tab Navigator:
- Tab A -> Camera
- Tab B -> Profile
In the profile screen, there are other screens of the same type ("Profile") pushed (with different params) in its stack. Now, if you are in the "Camera" screen and do:
navigation.navigate("Profile", { screen: "Profile", params });
You will navigate to the "Profile" screen and those params will be sent to the last screen in the stack. What can I do if I want to navigate to the root of the stack passing the params?
I have tried with:
// In the profile screen
useEffect(() => {
if (navigation.canGoBack())
navigation.popToTop(); // Go back to the root of the stack
showParams(params);
}, [params])
but with this, the "showParams" operation isn't performed in the root, and I am not navigating directly to the root of the stack from the "Camera" screen.
I think I have to do something like this in the Camera screen before navigating:
navigation.dispatch(
CommonActions.reset({
// some stuff
})
);
navigation.navigate("Profile", { screen: "Profile", params });
But I can't find any way to achieve my goal.
Any ideas? Thank you.
UPDATE - My navigation system
STACKS (Here I define multiple stacks: "HomeStacks", "SearchStacks", "ProfileStacks" ...)
const Stack = createStackNavigator();
export function ProfileStacks() { <------ Over this stack I do .push()
return (
<Stack.Navigator
initialRouteName="Profile"
>
<Stack.Screen name="Profile" children={Profile} />
<Stack.Screen name="EditProfile" children={EditProfile} />
</Stack.Navigator>
);
}
...
BOTTOM TAB NAVIGATOR
<Tab.Navigator>
<Tab.Screen
name="Camera"
component={CameraPlaceholder}
listeners={({ navigation }) => ({
tabPress: (event) => {
event.preventDefault();
navigation.navigate("CameraModal");
},
})}
/>
<Tab.Screen
name="Profile"
component={ProfileStacks}
/>
</Tab.Navigator>
ROOT STACK NAVIGATOR (The main navigator of the app)
In this stack I implement the authentication flow and also, I declare some extra stacks (just for look-and-feel purposes).
export default function RootNavigator(props) {
/*
This navigator is implemented using the
'Protected Routes' pattern
*/
const { isUserLoggedIn } = props;
const RootStack = createStackNavigator();
return (
<RootStack.Navigator>
{isUserLoggedIn ? (
<>
<RootStack.Screen
name="BottomTabNavigator"
component={BottomTabNavigator}
/>
<RootStack.Screen
name="CameraModal"
component={Camera}
/>
</>
) : (
<>
<RootStack.Screen name="SignIn" component={SignIn} />
<RootStack.Screen
name="SignUp"
component={SignUp}
/>
<RootStack.Screen
name="ForgotPassword"
component={ForgotPassword}
/>
</>
)}
</RootStack.Navigator>
);
Related problems I have seen
How to reset a Stack in a different Tab using React Navigation 5.x
This is my Profile tab's navigation data
Object {
"key": "Profile-Ty4St1skrxoven-jkZUsx",
"name": "Profile",
"params": undefined,
"state": Object {
"index": 1,
"key": "stack-8nWDnwDJZRK8iDuJok7Hj",
"routeNames": Array [
"Profile",
"EditProfile",
],
"routes": Array [
Object {
"key": "Profile-m0GQkvNk5RjAhGABvOy9n",
"name": "Profile",
"params": undefined,
},
Object {
"key": "Profile-tZAEmSU0eEo1Nt7XC09t1",
"name": "Profile",
"params": Object {
"otherUserData": Object {
"username": "jeffbezos",
},
"post": null,
},
},
],
"stale": false,
"type": "stack",
},
},
],
"stale": false,
"type": "tab",
},
I just need to pop the second route from the stack "Profile" which is in the tab "Profile" from another Tab of my app, and then navigate to this screen.
Suppose two stack screens in a Tab Navigator:
- Tab A -> Camera
- Tab B -> Profile
In the profile screen, there are other screens of the same type ("Profile") pushed (with different params) in its stack. Now, if you are in the "Camera" screen and do:
navigation.navigate("Profile", { screen: "Profile", params });
You will navigate to the "Profile" screen and those params will be sent to the last screen in the stack. What can I do if I want to navigate to the root of the stack passing the params?
I have tried with:
// In the profile screen
useEffect(() => {
if (navigation.canGoBack())
navigation.popToTop(); // Go back to the root of the stack
showParams(params);
}, [params])
but with this, the "showParams" operation isn't performed in the root, and I am not navigating directly to the root of the stack from the "Camera" screen.
I think I have to do something like this in the Camera screen before navigating:
navigation.dispatch(
CommonActions.reset({
// some stuff
})
);
navigation.navigate("Profile", { screen: "Profile", params });
But I can't find any way to achieve my goal.
Any ideas? Thank you.
UPDATE - My navigation system
STACKS (Here I define multiple stacks: "HomeStacks", "SearchStacks", "ProfileStacks" ...)
const Stack = createStackNavigator();
export function ProfileStacks() { <------ Over this stack I do .push()
return (
<Stack.Navigator
initialRouteName="Profile"
>
<Stack.Screen name="Profile" children={Profile} />
<Stack.Screen name="EditProfile" children={EditProfile} />
</Stack.Navigator>
);
}
...
BOTTOM TAB NAVIGATOR
<Tab.Navigator>
<Tab.Screen
name="Camera"
component={CameraPlaceholder}
listeners={({ navigation }) => ({
tabPress: (event) => {
event.preventDefault();
navigation.navigate("CameraModal");
},
})}
/>
<Tab.Screen
name="Profile"
component={ProfileStacks}
/>
</Tab.Navigator>
ROOT STACK NAVIGATOR (The main navigator of the app)
In this stack I implement the authentication flow and also, I declare some extra stacks (just for look-and-feel purposes).
export default function RootNavigator(props) {
/*
This navigator is implemented using the
'Protected Routes' pattern
*/
const { isUserLoggedIn } = props;
const RootStack = createStackNavigator();
return (
<RootStack.Navigator>
{isUserLoggedIn ? (
<>
<RootStack.Screen
name="BottomTabNavigator"
component={BottomTabNavigator}
/>
<RootStack.Screen
name="CameraModal"
component={Camera}
/>
</>
) : (
<>
<RootStack.Screen name="SignIn" component={SignIn} />
<RootStack.Screen
name="SignUp"
component={SignUp}
/>
<RootStack.Screen
name="ForgotPassword"
component={ForgotPassword}
/>
</>
)}
</RootStack.Navigator>
);
Related problems I have seen
How to reset a Stack in a different Tab using React Navigation 5.x
https://github.com/react-navigation/react-navigation/issues/6639
https://github.com/react-navigation/react-navigation/issues/8988
This is my Profile tab's navigation data
Object {
"key": "Profile-Ty4St1skrxoven-jkZUsx",
"name": "Profile",
"params": undefined,
"state": Object {
"index": 1,
"key": "stack-8nWDnwDJZRK8iDuJok7Hj",
"routeNames": Array [
"Profile",
"EditProfile",
],
"routes": Array [
Object {
"key": "Profile-m0GQkvNk5RjAhGABvOy9n",
"name": "Profile",
"params": undefined,
},
Object {
"key": "Profile-tZAEmSU0eEo1Nt7XC09t1",
"name": "Profile",
"params": Object {
"otherUserData": Object {
"username": "jeffbezos",
},
"post": null,
},
},
],
"stale": false,
"type": "stack",
},
},
],
"stale": false,
"type": "tab",
},
I just need to pop the second route from the stack "Profile" which is in the tab "Profile" from another Tab of my app, and then navigate to this screen.
Share Improve this question edited Oct 25, 2020 at 4:16 Victor Molina asked Oct 24, 2020 at 16:09 Victor MolinaVictor Molina 2,6413 gold badges28 silver badges59 bronze badges 5- hello victor you can put stack code, I don't understand at all if you have nested navigators there and what you mean with root of the stack. Also you can show your case in this snack that have react navigation v5 snack.expo.io/@anthowm/drawer-navigation-%7C-react-navigation – anthony willis muñoz Commented Oct 24, 2020 at 21:22
- Okey, I will update it give me a couple minutes. – Victor Molina Commented Oct 24, 2020 at 21:23
- @anthonywillismuñoz done. The problem is that I do .push("Profile", { otherUserParams }); in the ProfileStacks, and I want to "pop to top" all of those pushed screens from the "Camera" route. Just because if I navigate from "Camera" to "Profile" without reseting the stack, I just see the last pushed screen in the Stack, not the screen which is at the "root" of the stack. – Victor Molina Commented Oct 24, 2020 at 21:35
- 1 why you not push directly to the screen that u want instead of let navigator choice what screen to show. you know that you can navigate navigation.navigate('Profile', { screen: 'Profile or EditProfile', params: {...} });. Also if that is not what u want u can try to explain me via dm in discord anthowm#7115 and we can chat in spanish :) – anthony willis muñoz Commented Oct 24, 2020 at 21:55
- Yeah I tried this... I will explain the problem better via discord. Thanks man I am since yesterday triying to solve this problem. – Victor Molina Commented Oct 24, 2020 at 22:01
4 Answers
Reset to default 7UPDATE (Refactored code)
import { useNavigation, CommonActions } from "@react-navigation/native";
export default function useResetProfileStackNavigator() {
const navigation = useNavigation();
return () => {
const bottomTabNavigator = navigation
.getState()
?.routes?.find(({ name }) => name === "BottomTabNavigator");
const profileTab = bottomTabNavigator?.state?.routes?.find(
({ name }) => name === "ProfileStacks"
);
const { key: target, routes } = profileTab?.state ?? {};
if (!target || routes?.length <= 1) return;
routes.length = 1; // popToTop()
navigation.dispatch({
...CommonActions.reset({ routes }),
target,
});
};
}
And here is how to use it:
export default function useSendPostToProfile() {
const navigation = useNavigation();
const isSending = useRef(false);
const resetProfileStackNavigator = useResetProfileStackNavigator();
return (post) => {
if (isSending.current) return;
isSending.current = true;
// Make sure there is only one route open in the profile stack
resetProfileStackNavigator();
navigation.navigate("BottomTabNavigator", {
screen: "ProfileStacks",
params: {
screen: "Profile",
params: {
post,
},
},
});
};
}
Previous solution
After a few hours studying the problem I have found a solution. It's not the best but it works for my use case and it surely works for other people's.
What I have tried to achieve is to reset the routes of the "Profile" screen that is in a stack navigator that in turn is in another tab of the tab navigator in which my current stack screen is. It sounds somewhat confusing, but it is basically something similar to what happens on instagram when you upload a photo.
If in Instagram you navigate to other users profiles from the Home screen and then upload a photo to your account, you will see how you go from the "publish your photo" screen to the root of the stack navigator that is in the Home tab, the feed.
In my use case, I am doing something similar, I can navigate to other users profiles from my own profile, and the photos are uploaded in this screen, with a progress bar.
From the beginning I had in mind to use navigation.popToTop (), but I have not been able to obtain the result I wanted, because as I have commented previously in the question, the parameters (that contained the post) were lost. So I have no choice but to simulate this behavior from my "publish photo" screen.
The steps I have followed are as follows:
- As my "publish photo" screen shares the navigation with my "profile" screen, through the tab navigator (which is obvious, since if it were not like that I could not do the navigation.navigate()), I have followed the navigation path from this to the Stack Navigator of the Profile Tab and then I have tried to take both its key and its routes.
- In case I have found the current key and paths, that means the stack navigator is mounted (in my case, the tab does a lazy initialization of all my pages, that's why I speak of "trying to take"). So it will be necessary to apply steps 3 and 4.
- Simulate the navigation.popToTop() reducing the size of the routes to 1 (note that the root of the stack navigator is the item in the first position of the "routes" array)
- Dispatch the reset operation over the profile's stack navigator using the navigation API.
- The final step, navigate to the stack screen normally passing the photo as param.
Here is the code:
const resetProfileStackNavigator = () => {
const currentNavigationState = navigation.dangerouslyGetState();
// Find the bottom navigator
for (let i = 0; i < currentNavigationState?.routes?.length; i++) {
if (currentNavigationState.routes[i].name === "BottomTabNavigator") {
// Get its state
const bottomNavigationState = currentNavigationState.routes[i].state;
// Find the profile tab
for (let j = 0; j < bottomNavigationState?.routes?.length; j++) {
if (bottomNavigationState.routes[j].name === "Profile") {
// Get its state
const profileTabState = bottomNavigationState.routes[j].state;
// Get the key of the profile tab's stack navigator
var targetKey = profileTabState?.key;
var targetCurrentRoutes = profileTabState?.routes;
break;
}
}
break;
}
}
// Reset the profile tab's stack navigator if it exists and has more than one stacked screen
if (targetKey && targetCurrentRoutes?.length > 1) {
// Set a new size for its current routes array, which is faster than Array.splice to mutate
targetCurrentRoutes.length = 1; // This simulates the navigation.popToTop()
navigation.dispatch({
...CommonActions.reset({
routes: targetCurrentRoutes, // It is necessary to copy the existing root route, with the same key, to avoid the component unmounting
}),
target: targetKey,
});
}
}
/*
Maybe, the stack navigator of the profile tab exists and has changed from its initial state...
In this situation, we will have to find the key of this stack navigator, which is also
nested in the same tab navigator in which this stack screen is.
*/
resetProfileStackNavigator();
// Finally, navigate to the profile stack screen and pass the post as param
navigation.navigate("Profile", {
screen: "Profile",
params: {
post,
},
});
Pd: I know there are some applicable refactorings, but I prefer to display the code this way so that the steps I discussed above are clearly visible.
If anyone who has read this manages to generalize this segment of code into a generic function using ES6, please leave it as an answer, as it can be very useful for me and for other users.
I was struggling with similar issue. My case was that I want to have the same stack navigator and the tabs would be just different starting points, something like 2 home screens. It is the behavior that is seen in Spotify for Android, for example - we have Home, Search and Library and all of them have common screens such as Album Screen and Song Screen. And when the user clicks on one of the tabs, the stack is cleared (like popToTop() should do).
My solution was to have Bottom Tab Navigator with Stack Navigators that have the same screens - DiscoverNavigator and SearchNavigator:
const SearchNavigator = () => (
<Stack.Navigator headerMode='screen'>
<Stack.Screen name='Search' component={SearchScreen} />
<Stack.Screen name='SearchResults' component={SearchResultsScreen} />
<Stack.Screen name='Item' component={ItemScreen} />
</Stack.Navigator>
)
const DiscoverNavigator = () => (
<Stack.Navigator headerMode='screen'>
<Stack.Screen name='Discover' component={DiscoverScreen} />
<Stack.Screen name='Search' component={SearchScreen} />
<Stack.Screen name='SearchResults' component={SearchResultsScreen} />
<Stack.Screen name='Item' component={ItemScreen} />
</Stack.Navigator>
)
And the trick is to add a listener on blur for the Tabs, like that:
<NavigationContainer>
<Tab.Navigator>
<Tab.Screen name='DiscoverNavigator' component={DiscoverNavigator}
listeners={props => tabPressListener({ ...props })}
/>
<Tab.Screen name='SearchNavigator' component={SearchNavigator}
listeners={props => tabPressListener({ ...props })}
/>
</Tab.Navigator>
</NavigationContainer>
That the handler for the blur event will check if the current tab has its own stack navigation and if it should clear it:
const tabPressListener = props => {
const { navigation } = props
return {
blur: e => {
const target = e.target
const state = navigation.dangerouslyGetState()
const route = state.routes.find(r => r.key === target)
// If we are leaving a tab that has its own stack navigation, then clear it
if (route.state?.type === "stack" && route.state.routes?.length > 1) {
navigation.dispatch(StackActions.popToTop())
}
}
}
}
Here is a demo: https://snack.expo.io/@monikamateeva/bottom-tab-navigation-with-poptotop
And this is all of the code:
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
import { NavigationContainer, CommonActions, StackActions } from '@react-navigation/native'
import { createStackNavigator } from '@react-navigation/stack'
import React from 'react'
import { Button, StyleSheet, View } from 'react-native'
import { enableScreens } from 'react-native-screens';
enableScreens();
const Stack = createStackNavigator()
const Tab = createBottomTabNavigator()
const ItemScreen = ({ navigation, route }) => {
React.useLayoutEffect(() => {
navigation.setOptions({
title: `Item ${route.params?.id}`,
})
}, [navigation, route])
return (
<View style={styles.container}>
<Button title='Item 2' onPress={() => navigation.push('Item', { id: 2 })} />
</View>
)
}
const SearchResultsScreen = ({ navigation, route }) => (
<View style={styles.container}>
<Button title={`Item ${route.params?.id}`} onPress={() => navigation.push('Item', { id: route.params?.id })} />
</View>
)
const DiscoverScreen = ({ navigation }) => (
<View style={styles.container}>
<Button title='Search Results 20' onPress={() => navigation.navigate('Search', { screen: 'SearchResults', params: { id: 20 } })} />
<Button title='Item 20' onPress={() => navigation.navigate('Search', { screen: 'Item', params: { id: 20 } })} />
</View>
)
const SearchScreen = ({ navigation }) => (
<View style={styles.container}>
<Button title='Search Results 10' onPress={() => navigation.push('SearchResults', { id: 20 })} />
<Button title='Item 10' onPress={() => navigation.push('Item', { id: 10 })} />
</View>
)
const SearchNavigator = () => (
<Stack.Navigator headerMode='screen'>
<Stack.Screen name='Search' component={SearchScreen} />
<Stack.Screen name='SearchResults' component={SearchResultsScreen} />
<Stack.Screen name='Item' component={ItemScreen} />
</Stack.Navigator>
)
const DiscoverNavigator = () => (
<Stack.Navigator headerMode='screen'>
<Stack.Screen name='Discover' component={DiscoverScreen} />
<Stack.Screen name='Search' component={SearchScreen} />
<Stack.Screen name='SearchResults' component={SearchResultsScreen} />
<Stack.Screen name='Item' component={ItemScreen} />
</Stack.Navigator>
)
const tabPressListener = props => {
const { navigation } = props
return {
blur: e => {
const target = e.target
const state = navigation.dangerouslyGetState()
const route = state.routes.find(r => r.key === target)
// If we are leaving a tab that has its own stack navigation, then clear it
if (route.state?.type === "stack" && route.state.routes?.length > 1) {
navigation.dispatch(StackActions.popToTop())
}
},
// Log the state for debug only
state: e => {
const state = navigation.dangerouslyGetState()
console.log(`state`, state)
}
}
}
const AppNavigator = () => {
return (
<NavigationContainer name="BottomTabNavigator">
<Tab.Navigator>
<Tab.Screen
name='DiscoverNavigator'
component={DiscoverNavigator}
listeners={props => tabPressListener({ ...props })}
/>
<Tab.Screen
name='SearchNavigator'
component={SearchNavigator}
listeners={props => tabPressListener({ ...props })}
/>
</Tab.Navigator>
</NavigationContainer>
)
}
export default AppNavigator
const styles = StyleSheet.create({
container: {
flex: 1,
},
})
This code is enough and it works
<Tab.Screen name='SearchNavigator' component={SearchNavigator}
listeners={props => tabPressListener({ ...props })}/>
and this
const tabPressListener = props => {
const { navigation } = props
return {
blur: e => {
const target = e.target
const state = navigation.dangerouslyGetState()
const route = state.routes.find(r => r.key === target)
// If we are leaving a tab that has its own stack navigation, then clear it
if (route.state?.type === "stack" && route.state.routes?.length > 1) {
navigation.dispatch(StackActions.popToTop())
}
}
}
}
As of React Navigation 6.x you can easily achieve this with CommonActions.reset
. I wrote some example code based on the original question assuming only one stack, haven't tested for nested stacks but the solution might be something similar.
import {CommonActions} from '@react-navigation/native';
navigation.dispatch((state) => {
const params = state.routes[state.routes.length - 1].params;
return CommonActions.reset({
index: 1,
routes: [{name: 'Home', params}]
});
});