Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React Navigation 5 - Reset a stack (similar to popToTop()) from another stack in a different tab before navigating to it

Suppose two stack screens in a Tab Navigator:

  1. Tab A -> Camera
  2. 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.

like image 233
Victor Molina Avatar asked Oct 24 '20 16:10

Victor Molina


People also ask

What is the difference between React navigation stack and react navigation stack?

A key difference between how this works in a web browser and in React Navigation is that React Navigation's native stack navigator provides the gestures and animations that you would expect on Android and iOS when navigating between routes in the stack.

How do I reset a stack in React Native?

To reset the navigation stack for the home screen with React Navigation and React Native, we can use the navigation. dispatch and the CommonActions. reset methods. import { CommonActions } from "@react-navigation/native"; navigation.


2 Answers

UPDATE (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:

  1. 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.
  1. 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.
  1. 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)
  1. Dispatch the reset operation over the profile's stack navigator using the navigation API.
  1. 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.

like image 123
Victor Molina Avatar answered Oct 31 '22 20:10

Victor Molina


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,
    },
})

like image 41
Monika Mateeva Avatar answered Oct 31 '22 20:10

Monika Mateeva