react-navigation / react-navigation

Routing and navigation for your React Native apps
https://reactnavigation.org
23.47k stars 5.01k forks source link

Cannot use separate components for navigator groups #9987

Closed roryabraham closed 2 years ago

roryabraham commented 2 years ago

Current behavior

I am trying to organize my app under a single navigator, where there are three groups of screens:

  1. Public screens available to non-logged-in users
  2. Private screens available to logged-in users
  3. Shared screens available to both.

And to implement this I:

  1. Created a shared stack navigator like export default createStackNavigator()
  2. Created three components, where each component returns a <RootStack.Group>
  3. In the main component with the <RootStack.Navigator>, conditionally render the various groups.

However, I'm seeing the following error:

A navigator can only contain 'Screen', 'Group' or 'React.Fragment' as its direct children (found '_default'). To render this component in the navigator, pass it in the 'component' prop to 'Screen'.

I created a very simple minimal reproduction to give you an idea of what I'm attempting – and it seems like it should work.

Expected behavior

I should be able to define groups in separate components and then use those as top-level children of the navigator.

Reproduction

https://snack.expo.dev/@roryabraham/react-navigation-6-boilerplate

Platform

Packages

Environment

package version
@react-navigation/native 6.0.2
@react-navigation/stack 6.0.7
react-native-safe-area-context 3.2.0
react-native-screens ~3.4.0
react-native-gesture-handler ~1.10.2
react-native 0.64
expo 0.42
node 14.16.0
npm or yarn npm
github-actions[bot] commented 2 years ago

Couldn't find version numbers for the following packages in the issue:

Can you update the issue to include version numbers for those packages? The version numbers must match the format 1.2.3.

The versions mentioned in the issue for the following packages differ from the latest versions on npm:

Can you verify that the issue still exists after upgrading to the latest versions of these packages?

satya164 commented 2 years ago

It's not possible due to limitations in React. You can just make them regular functions instead of components if you want to extract them out.

github-actions[bot] commented 2 years ago

Hey! This issue is closed and isn't watched by the core team. You are welcome to discuss the issue with others in this thread, but if you think this issue is still valid and needs to be tracked, please open a new issue with a repro.

roryabraham commented 2 years ago

@satya164 Are you sure? I'm not sure but I think it might be possible to fix this by adjusting this code like so:

if (React.isValidElement(child)) {
  // Note – changed conditional here
  if (child.type === Screen || (typeof child.type === 'function' && child.type().type === Screen)) {
    // We can only extract the config from `Screen` elements
    // If something else was rendered, it's probably a bug
    acc.push([
      options,
      child.props as RouteConfig<
        ParamListBase,
        string,
        State,
        ScreenOptions,
        EventMap
      >,
    ]);
    return acc;
  }

  // Note – changed conditional here
  if ([React.Fragment, Group].includes(child.type) || (typeof child.type === 'function' && [React.Fragment, Group].includes(child.type().type))) {
    // When we encounter a fragment or group, we need to dive into its children to extract the configs
    // This is handy to conditionally define a group of screens
    acc.push(
      ...getRouteConfigsFromChildren<State, ScreenOptions, EventMap>(
        child.props.children,
        child.type !== Group
          ? options
          : options != null
          ? [...options, child.props.screenOptions]
          : [child.props.screenOptions]
      )
    );
    return acc;
  }
}

throw new Error(
  `A navigator can only contain 'Screen', 'Group' or 'React.Fragment' as its direct children (found ${
    React.isValidElement(child)
      ? `'${
          typeof child.type === 'string' ? child.type : child.type?.name
        }'${
          child.props?.name ? ` for the screen '${child.props.name}'` : ''
        }`
      : typeof child === 'object'
      ? JSON.stringify(child)
      : `'${String(child)}'`
  }). To render this component in the navigator, pass it in the 'component' prop to 'Screen'.`
);
}, []);
roryabraham commented 2 years ago

Asking because in my actual application there are more things I need to happen when using the components that provide the various groups. (i.e: In PrivateScreens load user data, etc...). So the only other alternative for now would be to have three nested navigators, but as a consequence the transition between my public screens and private screens cannot be animated.

nihilenz commented 2 years ago

@roryabraham I think it can work with {isLoggedIn ? PrivateScreens({}) : PublicScreens({})} https://snack.expo.dev/@spirales/react-navigation-6-boilerplate but I don't know it is an approach recommended by React Navigation team (@satya164)

ha3 commented 1 year ago

@satya164 hi. I want to wrap some of my screens in the same navigator under a context provider. I think this is the same use case mentioned in this issue. Is @nihilenz's solution safe and applicable for this?