withNavigation can only be used on a view hierarch

2020-08-17 17:57发布

问题:

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

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



回答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/



回答3:

Vahissan's answer is correct but I could not get it working because of various differences in my code from https://reactnavigation.org/docs/en/navigating-without-navigation-prop.html like my stackNavigator not being a component, it is just an object.

What I managed to do was get the AlertModal component to be a child of the stackNavigator and thus receive the navigation prop by adding it to my StackNavigator's ContentComponent. The code is as above but int the CustomDrawerContentComponent just making it like this:

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

    <AlertModalComponent
          yesClicked={() => {
            updateAlertModalIsOpen(false)
          }}
        />
  </SafeAreaView>
)