React Navigation 5 - 在导航到另一个选项卡之前从另一个堆栈重置堆栈(类似于 popToTop())

IT技术 javascript reactjs react-native react-navigation
2021-04-24 10:43:11

假设 Tab Navigator 中有两个堆栈屏幕:

  1. 选项卡 A -> 相机
  2. 选项卡 B -> 个人资料

在配置文件屏幕中,在其堆栈中推送了其他相同类型(“配置文件”)(具有不同参数)的屏幕。现在,如果您在“相机”屏幕中并执行以下操作:

    navigation.navigate("Profile", { screen: "Profile", params });

您将导航到“配置文件”屏幕,这些参数将被发送到堆栈中的最后一个屏幕。如果我想导航到传递参数的堆栈的根,我该怎么办?

我试过:

   // In the profile screen
   useEffect(() => {
       if (navigation.canGoBack())
            navigation.popToTop(); // Go back to the root of the stack

       showParams(params);
   }, [params])

但是有了这个,“showParams”操作不在根目录中执行,我也没有从“相机”屏幕直接导航到堆栈的根目录。

我想我必须在导航之前在“相机”屏幕中执行以下操作:

  navigation.dispatch(
        CommonActions.reset({
          // some stuff
        })
  );

  navigation.navigate("Profile", { screen: "Profile", params });

但是我找不到任何方法来实现我的目标。

有任何想法吗?谢谢你。

更新 - 我的导航系统

堆栈(这里我定义了多个堆栈:“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>
  );
}

...

底部标签导航器

<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(应用程序的主导航器)

在这个堆栈中,我实现了身份验证流程,并且还声明了一些额外的堆栈(仅用于外观目的)。

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>
  );

我见过的相关问题

如何使用 React Navigation 5.x 在不同的选项卡中重置堆栈

https://github.com/react-navigation/react-navigation/issues/6639

https://github.com/react-navigation/react-navigation/issues/8988

这是我的个人资料选项卡的导航数据

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

我只需要从我的应用程序的另一个选项卡的“配置文件”选项卡中的堆栈“配置文件”中弹出第二条路由,然后导航到此屏幕。

4个回答

我正在努力解决类似的问题。我的情况是,我想要相同的堆栈导航器,而选项卡只是不同的起点,例如 2 个主屏幕。例如,这是在 Android 版 Spotify 中看到的行为 - 我们有主页、搜索和图书馆,并且所有这些都有通用屏幕,例如专辑屏幕和歌曲屏幕。当用户单击其中一个选项卡时,堆栈会被清除(就像 popToTop() 应该做的那样)。

我的解决方案是让底部选项卡导航器与具有相同屏幕的堆栈导航器 - DiscoverNavigator 和 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>
)

诀窍是为选项卡添加一个关于模糊的侦听器,如下所示:

<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>

模糊事件的处理程序将检查当前选项卡是否有自己的堆栈导航以及是否应该清除它:

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

这是一个演示:https : //snack.expo.io/@monikamateeva/bottom-tab-navigation-with-poptotop

这是所有的代码:

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

在研究了这个问题几个小时后,我找到了解决方案。这不是最好的,但它适用于我的用例,它肯定适用于其他人的用例。

我试图实现的是重置堆栈导航器中“配置文件”屏幕的路线,而堆栈导航器又位于我当前堆栈屏幕所在的选项卡导航器的另一个选项卡中。这听起来有点令人困惑,但它基本上类似于上传照片时在 Instagram 上发生的事情。

如果在 Instagram 中,您从主屏幕导航到其他用户的个人资料,然后将照片上传到您的帐户,您将看到如何从“发布您的照片”屏幕转到主页选项卡中堆栈导航器的根目录,饲料。

在我的用例中,我正在做类似的事情,我可以从我自己的个人资料导航到其他用户的个人资料,并且照片会上传到此屏幕中,并带有进度条。

从一开始我就想使用 navigation.popToTop(),但是我一直无法获得我想要的结果,因为正如我之前在问题中所评论的,参数(包含帖子的)丢失了。所以我别无选择,只能从我的“发布照片”屏幕模拟这种行为。

我遵循的步骤如下:

  1. 由于我的“发布照片”屏幕与我的“个人资料”屏幕共享导航,通过选项卡导航器(这是显而易见的,因为如果不是那样,我就无法执行 navigation.navigate()),我已经按照导航从这个路径到配置文件选项卡的堆栈导航器,然后我尝试使用它的密钥和路由。
  1. 如果我找到了当前的键和路径,这意味着堆栈导航器已安装(在我的情况下,该选项卡对我的所有页面进行了延迟初始化,这就是我所说的“尝试获取”的原因)。因此,有必要应用第 3 步和第 4 步。
  1. 模拟 navigation.popToTop() 将路由的大小减少到 1(注意堆栈导航器的根是“路由”数组第一个位置的项目)
  1. 使用导航 API 通过配置文件的堆栈导航器分派重置操作。
  1. 最后一步,导航到堆栈屏幕,通常将照片作为参数传递。

这是代码:

  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:我知道有一些适用的重构,但我更喜欢以这种方式显示代码,以便我上面讨论的步骤清晰可见。

如果任何读过这篇文章的人设法使用 ES6 将这段代码概括为通用函数,请将其作为答案,因为它对我和其他用户非常有用。

这段代码就够用了

 <Tab.Screen name='SearchNavigator' component={SearchNavigator}
            listeners={props => tabPressListener({ ...props })}/>

还有这个

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

从 React Navigation 6.x 开始,您可以使用CommonActions.reset. 我根据原始问题编写了一些示例代码,假设只有一个堆栈,尚未测试嵌套堆栈,但解决方案可能类似。

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}]
            });
          });