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

Posted

技术标签:

【中文标题】React Navigation 5 - 在导航到它之前从另一个选项卡中的另一个堆栈重置堆栈(类似于 popToTop())【英文标题】:React Navigation 5 - Reset a stack (similar to popToTop()) from another stack in a different tab before navigating to it 【发布时间】:2021-02-07 10:17:30 【问题描述】:

假设标签导航器中有两个堆栈屏幕:

    选项卡 A -> 相机 选项卡 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 );

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

有什么想法吗?谢谢。

更新 - 我的导航系统

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


...

底部标签导航器

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

我见过的相关问题

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

这是我的个人资料标签的导航数据

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

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

【问题讨论】:

你好胜利者,你可以放堆栈代码,我完全不明白你是否在那里嵌套了导航器以及你对堆栈根的含义。你也可以在这个有反应导航 v5 snack.expo.io/@anthowm/drawer-navigation-%7C-react-navigation 的小吃中展示你的案例 好的,我会在几分钟内更新它。 @anthonywillismuñoz 完成。问题是我做了 .push("Profile", otherUserParams );在 ProfileStacks 中,我想从“相机”路线“弹出”所有这些推送的屏幕。仅仅因为如果我在不重置堆栈的情况下从“相机”导航到“配置文件”,我只会看到堆栈中最后推送的屏幕,而不是位于堆栈“根”的屏幕。 为什么你不直接推送到你想要的屏幕而不是让导航器选择要显示的屏幕。你知道你可以导航 navigation.navigate('Profile', screen: 'Profile or EditProfile', params: ... );。此外,如果这不是您想要的,您可以尝试通过不和谐 anthowm#7115 中的 dm 向我解释,我们可以用西班牙语聊天 :) 是的,我试过这个......我将通过不和谐更好地解释这个问题。谢谢伙计,我从昨天开始就在努力解决这个问题。 【参考方案1】:

更新(重构代码)

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

下面是如何使用它:

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


以前的解决方案

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

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

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

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

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

我遵循的步骤如下:

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

代码如下:

  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 的通用函数,请将其作为答案,因为它对我和其他用户都非常有用。

【讨论】:

【参考方案2】:

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

【讨论】:

【参考方案3】:

这段代码足够了,它可以工作

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

【讨论】:

【参考方案4】:

我正在努力解决类似的问题。我的情况是我希望拥有相同的堆栈导航器,并且选项卡只是不同的起点,例如 2 个主屏幕。这是在 Spotify for android 中看到的行为,例如 - 我们有主页、搜索和图书馆,它们都有共同的屏幕,如专辑屏幕和歌曲屏幕。当用户点击其中一个选项卡时,堆栈会被清除(就像 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,
    ,
)

【讨论】:

以上是关于React Navigation 5 - 在导航到它之前从另一个选项卡中的另一个堆栈重置堆栈(类似于 popToTop())的主要内容,如果未能解决你的问题,请参考以下文章

React Navigation 路由导航库升级 5.x

在 React Native 中始终显示底部选项卡导航器 React Navigation 5

Redux动作已触发,但无法导航(React-Navigation)

如何重置嵌套的堆栈导航导航器 react-navigation v5

React Native Navigation:从 React 组件外部导航到屏幕

在react-navigation中从嵌套导航器导航到父屏幕