withNavigation can only be used on a view hierarchy of a navigator

13,335

Solution 1

In react-navigation, the main StackNavigator creates a context provider and the navigation prop will be made available to any component below its level in the component tree if they use the context consumer.

Two ways to access the navigation prop using context consumer is to either add the component to the StackNavigator, or use the withNavigation function. However because of how React's context API works, any component that uses withNavigation function must be below the StackNavigator in the component tree.

If you still want to access the navigation prop regardless of the position in component tree, you will have to store the ref to the StackNavigator in a module. Following guide from react-navigation will help you do that https://reactnavigation.org/docs/en/navigating-without-navigation-prop.html

Solution 2

If you are using react-navigation version 5, use useNavigation hook with a functional component. This hook injects the navigation object into the functional component. Here is the link to the docs:

https://reactnavigation.org/docs/use-navigation/

Share:
13,335

Related videos on Youtube

BeniaminoBaggins
Author by

BeniaminoBaggins

I dabble in developing web and mobile apps.

Updated on June 04, 2022

Comments

  • BeniaminoBaggins
    BeniaminoBaggins almost 2 years

    I'm getting the error:

    Invariant Violation: withNavigation can only be used on a view hierarchy of a navigator. The wrapped component is unable to get access to navigation from props or context

    I don't know why, because I'm using withNavigation in other components in my app and it works. I don't see a difference in the components that it works on to the one that causes the error.

    Code:

    the component:

    const mapStateToProps = (state: State): Object => ({
      alertModal: state.formControls.alertModal
    })
    
    const mapDispatchToProps = (dispatch: Dispatch<*>): Object => {
      return bindActionCreators(
        {
          updateAlertModalHeight: updateAlertModalHeight,
          updateAlertModalIsOpen: updateAlertModalIsOpen,
          updateHasYesNo: updateAlertModalHasYesNo
        },
        dispatch
      )
    }
    
    class AlertModalView extends Component<AlertModalProps, State> {
      render(): Node {
        return (
          <View style={alertModalStyle.container}>
            <PresentationalModal
              style={presentationalModalStyle}
              isOpen={this.props.alertModal.isOpen}
              title={this.props.alertModal.title}
              navigation={this.props.navigation}
              updateHasYesNo={this.props.updateHasYesNo}
              message={this.props.alertModal.message}
              updateAlertModalHeight={this.props.updateAlertModalHeight}
              viewHeight={this.props.alertModal.viewHeight}
              hasYesNo={this.props.alertModal.hasYesNo}
              yesClicked={this.props.alertModal.yesClicked}
              updateAlertModalIsOpen={this.props.updateAlertModalIsOpen}
            />
          </View>
        )
      }
    }
    
    // $FlowFixMe
    const AlertModalViewComponent = connect(
      mapStateToProps,
      mapDispatchToProps
    )(AlertModalView)
    
    export default withNavigation(AlertModalViewComponent)
    

    the stackNavigator:

    import React from 'react'
    import { View, SafeAreaView } from 'react-native'
    import Icon from 'react-native-vector-icons/EvilIcons'
    import Add from '../product/add/view'
    import Login from '../user/login/view'
    import Search from '../product/search/query/view'
    import { Image } from 'react-native'
    import { StackNavigator, DrawerNavigator, DrawerItems } from 'react-navigation'
    
    const AddMenuIcon = ({ navigate }) => (
      <View>
        <Icon
          name="plus"
          size={30}
          color="#FFF"
          onPress={() => navigate('DrawerOpen')}
        />
      </View>
    )
    
    const SearchMenuIcon = ({ navigate }) => (
      <Icon
        name="search"
        size={30}
        color="#FFF"
        onPress={() => navigate('DrawerOpen')}
      />
    )
    
    const Stack = {
      Login: {
        screen: Login
      },
      Search: {
        screen: Search
      },
      Add: {
        screen: Add
      }
    }
    
    
    const DrawerRoutes = {
      Login: {
        name: 'Login',
        screen: Login
      },
      'Search Vegan': {
        name: 'Search',
        screen: StackNavigator(Stack.Search, {
          headerMode: 'none'
        }),
        navigationOptions: ({ navigation }) => ({
          drawerIcon: SearchMenuIcon(navigation)
        })
      },
      'Add vegan': {
        name: 'Add',
        screen: StackNavigator(Stack.Add, {
          headerMode: 'none'
        }),
        navigationOptions: ({ navigation }) => ({
          drawerIcon: AddMenuIcon(navigation)
        })
      }
    }
    
    const CustomDrawerContentComponent = props => (
      <SafeAreaView style={{ flex: 1, backgroundColor: '#3f3f3f', color: 'white' }}>
        <View>
          <Image
            style={{
              marginLeft: 20,
              marginBottom: 0,
              marginTop: 0,
              width: 100,
              height: 100,
              resizeMode: 'contain'
            }}
            square
            source={require('../../images/logo_v_white.png')}
          />
        </View>
        <DrawerItems {...props} />
      </SafeAreaView>
    )
    
    const Menu = StackNavigator(
        {
          Drawer: {
            name: 'Drawer',
            screen: DrawerNavigator(DrawerRoutes, {
              initialRouteName: 'Login',
              drawerPosition: 'left',
              contentComponent: CustomDrawerContentComponent,
              contentOptions: {
                activeTintColor: '#27a562',
                inactiveTintColor: 'white',
                activeBackgroundColor: '#3a3a3a'
              }
            })
          }
        },
        {
          headerMode: 'none',
          initialRouteName: 'Drawer'
        }
      )
    
    
    export default Menu
    

    Here I render the StackNavigator which is Menu in my app component:

    import React, { Component } from 'react'
    import Menu from './menu/view'
    import Props from 'prop-types'
    import { Container } from 'native-base'
    import { updateAlertModalIsOpen } from './formControls/alertModal/action'
    import AlertModalComponent from './formControls/alertModal/view'
    import UserLoginModal from './user/login/loginModal/view'
    
    class Vepo extends Component {
      componentDidMount() {
        const { store } = this.context
        this.unsubscribe = store.subscribe(() => {})
        store.dispatch(this.props.fetchUserGeoCoords())
        store.dispatch(this.props.fetchSearchQueryPageCategories())
        store.dispatch(this.props.fetchCategories())
      }
    
      componentWillUnmount() {
        this.unsubscribe()
      }
    
      render(): Object {
        return (
          <Container>
            <Menu store={this.context} />
            <AlertModalComponent
              yesClicked={() => {
                updateAlertModalIsOpen(false)
              }}
            />
    
            <UserLoginModal />
          </Container>
        )
      }
    }
    Vepo.contextTypes = {
      store: Props.object
    }
    
    export default Vepo
    

    and my root component:

    export const store = createStore(
      rootReducer,
      vepo,
      composeWithDevTools(applyMiddleware(createEpicMiddleware(rootEpic)))
    )
    
    import NavigationService from './navigationService'
    
    export const App = () => (
      <Provider store={store}>
          <Vepo
            fetchUserGeoCoords={fetchUserGeoCoords}
            fetchSearchQueryPageCategories={fetchSearchQueryPageCategories}
            fetchCategories={fetchCategories}
          />
      </Provider>
    )
    AppRegistry.registerComponent('vepo', () => App)
    

    I have changed my Vepo component to this to implement the answer by vahissan:

    import React, { Component } from 'react'
    import Menu from './menu/view'
    import Props from 'prop-types'
    import { Container } from 'native-base'
    import { updateAlertModalIsOpen } from './formControls/alertModal/action'
    import AlertModalComponent from './formControls/alertModal/view'
    import UserLoginModal from './user/login/loginModal/view'
    
    import NavigationService from './navigationService'
    
    class Vepo extends Component {
      componentDidMount() {
        const { store } = this.context
        this.unsubscribe = store.subscribe(() => {})
        store.dispatch(this.props.fetchUserGeoCoords())
        store.dispatch(this.props.fetchSearchQueryPageCategories())
        store.dispatch(this.props.fetchCategories())
      }
    
      componentWillUnmount() {
        this.unsubscribe()
      }
    
      render(): Object {
        return (
          <Container>
            <Menu
              store={this.context}
              ref={navigatorRef => {
                NavigationService.setTopLevelNavigator(navigatorRef)
              }}>
              <AlertModalComponent
                yesClicked={() => {
                  updateAlertModalIsOpen(false)
                }}
              />
            </Menu>
            <UserLoginModal />
          </Container>
        )
      }
    }
    Vepo.contextTypes = {
      store: Props.object
    }
    
    export default Vepo
    

    No errors, but the alertModal no longer displays

  • kwoxer
    kwoxer over 3 years
    Worked perfectly, thanks. Was heavily searching for this.
  • Casey L
    Casey L over 3 years
    Thank you! Despite having a class component as a deeply nested child of my one of my StackNavigator screens (per @vahissan's suggestion), I could not get past this error using withNavigation. I changed my class component to a functional component, and used the useNavigation hook with no issues. Did not have to move anything around in my component tree.