Getting navigation.state outside of a screen in react-navigation v5/v6 - react-native

I'm trying to upgrade a react-native v4 app to v5/v6. In v4, createAppContainer was used like so:
const AppContainer = createAppContainer(AppNavigator);
and AppNavigator would receive a Navigation prop that could be used like:
type AppNavigatorProps = {
navigation: Navigation;
};
class AppNavigator extends Component<AppNavigatorProps> {
isSplashScreen = (): boolean => {
const { navigation } = this.props;
return navigation.state.index === RouteIndexes.SplashScreen;
};
render(): ReactNode {
// styling set-up here
return (
<>
<SafeAreaView style={topStyle} />
<SafeAreaView style={bottomStyle}>
<StatusBar backgroundColor={barColor} />
<ErrorBoundary
navigation={navigation}>
<RootContainerStackNavigator navigation={navigation} />
</ErrorBoundary>
</SafeAreaView>
</>
);
}
}
I cannot figure out a way to get that navigation.state in this location anymore. I have the AppNavigator wrapped in NavigationContainer instead of the createAppContainer like so:
<NavigationContainer>
<AppNavigator />
</NavigationContainer>

Related

How to set-up structure for react-navigation v6?

I am trying to upgrade a large app from v4 to v6 and I'm having issues even getting the first screen to render. I get the error: Couldn't find a navigation object. Is your component inside a screen in a navigator?. I feel like it's something wrong with the set-up but I can't quite figure it out. I followed through the upgrading docs from v4 to v5 to start out, but the examples do not have the same structure as the app I am working with.
Here's App.js:
import navigationRef from '../Navigation/NavigationService';
export const App = () => {
return (
<Provider store={store}>
<NavigationContainer ref={navigationRef}>
<RootContainer />
</NavigationContainer>
</Provider>
);
};
Here's NavigationService.ts:
export const topLevelNavigation = React.createRef<NavigationContainerRef>();
Here's RootContainer.tsx:
class RootContainer extends Component {
backHandlerSubscription: NativeEventSubscription;
componentDidMount = async (): Promise<void> => {
this.backHandlerSubscription = BackHandler.addEventListener('hardwareBackPress', this.onAndroidBackPress);
};
componentWillUnmount = async (): Promise<void> => {
this.backHandlerSubscription.remove();
};
onAndroidBackPress = (): null => {
NavigationService.navigateBack();
return null;
};
render(): ReactNode {
return (
<>
<View style={Styles.rootContainer}>
<AppNavigator /> //error occurs here!!
<AppStateManager />
</View>
</>
);
}
}
const mapDispatchToProps = () => ({});
export default connect(null, mapDispatchToProps)(RootContainer);
Here's the stack navigator:
const RootStack = createStackNavigator();
const RootContainerStackNavigator = () => {
return (
<RootStack.Navigator initialRouteName="StartupSplashScreen" screenOptions={{ gestureEnabled: false }}>
<RootStack.Screen name="StartupSplashScreen" component={StartupSplashScreen} />
<RootStack.Screen name="LoginScreen" component={LoginScreen} />
</RootStack.Navigator>
);
};
Here is the AppNavigator class:
type AppNavigatorProps = {
navigation?: any;
route?: RouteProp<any, any>;
};
class AppNavigator extends Component<AppNavigatorProps> {
// non-navigation related stuff here
render(): ReactNode {
const { navigation } = this.props;
return (
<>
<SafeAreaView style={topSafeAreaStyle} />
<SafeAreaView style={bottomSafeAreaStyle}>
<StatusBar backgroundColor={barColor} barStyle={barStyle}
<ErrorBoundary
navigation={navigation}
<RootContainerStackNavigator />
</ErrorBoundary>
</SafeAreaView>
</>
);
}
}
export default function (props) {
const navigation = useNavigation();
return <AppNavigator {...props} navigation={navigation} />;
}

Pass navigation from child component to parent getting TypeError

I'm facing an issue whenever i tried to navigate to another screens. I'm using the navigation in child component and it doesn't work even i passed the props to the parent component. This is my first time on using react navigation. I've tried many possible solution yet still can't solve this issue. I'm using react navigation 5 and i need help. I'm getting an error as such :
TypeError: undefined is not an object (evaluating 'this.props.navigation.navigate')
Home.js // Parent Component
class Home extends Component {
render() {
return (
<Cards
title="In Progress"
imgUri={require('../assets/CardProgress.png')}
navigateAction={() =>
this.props.navigation.navigate('SiteAudit')
}
)
}
}
Card.js // Child Component
const Cards = (props) => {
return (
<CardContainer
backgroundColor={props.backgroundColor}
onPress={() => {
props.navigation.navigateAction;
}}>
<CardWrapper>
<CardTitle>{props.title}</CardTitle>
<CardImage source={props.imgUri} />
</CardWrapper>
</CardContainer>
);
};
import React from 'react';
import {NavigationContainer} from '#react-navigation/native';
import {createStackNavigator} from '#react-navigation/stack';
import Dashboard from './screens/Dashboard';
import SiteAudit from './screens/SiteAudit';
const RootStack = createStackNavigator();
const DashboardStack = createStackNavigator();
const SiteAuditStack = createStackNavigator();
const DashboardScreen = () => {
return (
<DashboardStack.Navigator>
<DashboardStack.Screen name="Dashboard" component={Dashboard} />
</DashboardStack.Navigator>
);
};
const SiteAuditScreen = () => {
return (
<SiteAuditStack.Navigator>
<SiteAuditStack.Screen name="SiteAudit" component={SiteAudit} />
</SiteAuditStack.Navigator>
);
};
const Navigation = () => {
return (
<NavigationContainer>
<RootStack.Navigator initialRouteName="Dashboard">
<RootStack.Screen name="Dashboard" component={DashboardScreen} />
<RootStack.Screen name="SiteAudit" component={SiteAuditScreen} />
</RootStack.Navigator>
</NavigationContainer>
);
};
export default Navigation;
Edit your card view as follows
<Cards
title="In Progress"
imgUri={require('../assets/CardProgress.png')}
onPress={() => this.buttonPress()}
/>
const buttonPress = () => {
this.props.navigation.navigate('Site Audit');
};
Edit your button as follows,
<TouchableOpacity
style={styles.cardButton}
onPress={onPress}
<Text style={styles.cardTitle}>{this.props.title}</Text>
<Image style={styles.imageCard} source={this.props.imgUri} />
</TouchableOpacity>
Solved the issue, I just need to re-read the documentation of react navigation. I need to use the useNavigation that has been stated here https://reactnavigation.org/docs/use-navigation/
Cards.js // Child Component
const Cards = (props) => {
return (
<CardContainer
backgroundColor={props.backgroundColor}
onPress={props.navigationAction}>
<CardWrapper>
<CardTitle>{props.title}</CardTitle>
<CardImage source={props.imgUri} />
</CardWrapper>
</CardContainer>
);
};
Home.js // Parent Component
import {useNavigation} from '#react-navigation/native';
const Home = () => {
const navigation = useNavigation();
return (
<Cards
title="Completed"
backgroundColor="#0082C8"
navigationAction={() => {
navigation.navigate('Site Audit');
}}
/>
)
}

Not Able to Navigate from DrawerLayoutAndroid to any other screen in react native (android)

I have the following code
Sidebar.js
import React from 'react'
import {View,Text,StyleSheet,Image,TouchableHighlight} from 'react-native';
import {Dimensions} from 'react-native';
import Feather from 'react-native-vector-icons/Feather';
// const WIDTH = Dimensions.get('window').width;
const HEIGHT = Dimensions.get('window').height;
const logo = require('../assets/logo1.png');
export default class Sidebar extends React.Component {
constructor(props){
super(props);
this.handleNavigation = this.handleNavigation.bind(this);// you should bind this to the method that call the props
}
handleNavigation(){
this.props.navigation.navigate('QuoteDay');
}
render() {
return (
<View style={styles.navigationContainer}>
<View style={styles.logoContainer}>
<Image style={styles.logo}
source={logo} />
</View>
</TouchableHighlight>
<TouchableHighlight
activeOpacity={0.6}
underlayColor="#ffffff"
onPress={() => alert('Pressed!')}>
<Text style={styles.listitem}> <Feather name="edit" size={30} color="#273746"/> Quotes </Text>
</TouchableHighlight>
<TouchableHighlight
activeOpacity={0.6}
underlayColor="#ffffff"
onPress={this.handleNavigation}>
<Text style={styles.listitem}> <Feather name="sunrise" size={30} color="#273746"/> Quote of the Day </Text>
</TouchableHighlight>
</View>
</View>
)
}
}
App.js
import React from 'react';
import {DrawerLayoutAndroid} from 'react-native';
import {AppNavigator} from './screens/Navigation';
import Sidebar from './screens/Sidebar';
export default class App extends React.Component {
render(){
const navigationView = (
<Sidebar/>
);
return (
<DrawerLayoutAndroid
drawerWidth={300}
drawerPosition="left"
statusBarBackgroundColor="#F0B27A"
renderNavigationView={() => navigationView}
>
<AppNavigator />
</DrawerLayoutAndroid>
)
}
}
Navigation.js
import React from 'react';
import 'react-native-gesture-handler';
import { NavigationContainer } from '#react-navigation/native';
import { createStackNavigator } from '#react-navigation/stack';
import { HomeScreen } from './Home';
import { ModalScreen } from './Modal';
import { QuoteScreen } from './QuoteDay';
const { Navigator, Screen } = createStackNavigator();
const HomeNavigator = () => (
<Navigator mode="modal" headerMode='none'>
<Screen name='Home' component={HomeScreen} />
<Screen name='MyModal' component={ModalScreen}/>
<Screen name='QuoteDay' component={QuoteScreen}/>
</Navigator>
);
export const AppNavigator = () => (
<NavigationContainer>
<HomeNavigator/>
</NavigationContainer>
);
But it is giving me the following error
TypeError: undefined is not an object (evaluating 'this.props.navigation.navigate')
whenever I try to navigate from Sidebar.js to any other screen.
How should could I go about solving this sort of this problem.
The problem is that Sidebar is not rendered inside a screen in a navigator and does therefore not receive the navigation prop which explains the error you're getting.
I recommend you to use react navigation's Drawer (https://reactnavigation.org/docs/drawer-navigator/) instead of DrawerLayoutAndroid. You can still use your custom Sidebar component layout this way by passing Sidebar to the drawerContent prop of react navigation's Drawer navigator.
Navigation.js
import {NavigationContainer} from '#react-navigation/native';
import {createStackNavigator} from '#react-navigation/stack';
import {createDrawerNavigator} from '#react-navigation/drawer';
import Sidebar from './path';
// Other imports...
const Home = createStackNavigator();
const Main = createDrawerNavigator();
const MainNavigator = () => {
return (
<Main.Navigator
drawerStyle={{width: 240}}
drawerContent={(props) => <Sidebar {...props} />}>
<Main.Screen name="HomeNavigator" component={HomeNavigator} />
</Main.Navigator>
);
};
const HomeNavigator = () => (
<Home.Navigator mode="modal" headerMode="none">
<Home.Screen name="Home" component={HomeScreen} />
<Home.Screen name="MyModal" component={ModalScreen} />
<Home.Screen name="QuoteDay" component={QuoteScreen} />
</Home.Navigator>
);
export const AppNavigator = () => (
<NavigationContainer>
<MainNavigator />
</NavigationContainer>
);
App.js
// Be sure to import StatusBar from 'react-native' for setting the status bar color
export default class App extends React.Component {
componentDidMount() {
StatusBar.setBackgroundColor('#F0B27A');
}
render() {
return <AppNavigator />;
}
}
So the approach I've taken here is to create a Drawer Navigator and to make this the main navigator. The HomeNavigator is a screen of this MainNavigator. This way every screen inside MainNavigator has access to the drawer and the navigation prop; In this case that means HomeNavigator and every screen it has.

How to emit event to app root in react native

I'm trying to implement toast message (notification) on my React Native app.
I'm Thinking about implement my Toast component inside app root, and when a button is clicked (somewhere in the app), the app root will know about it and make the toast visible.
I don't want to use a library because I have complicated UI for this and I want to include buttons inside the toast.
This is the root component - App.js:
import { Provider } from 'react-redux';
import {Toast} from './src/components/Toast';
import store from './src/store/Store.js';
import AppNavigator from './src/navigation/AppNavigator';
import StatusBar from './src/components/StatusBar';
export default function App(props) {
return (
<Provider store = { store }>
<View style={styles.container}>
<StatusBar barStyle="default"/>
<AppNavigator />
<Toast></Toast>
</View>
</Provider>
);
}
EDIT:
AppNavigator.js:
// this is how I connect each page:
let HomePage = connect(state => mapStateToProps, dispatch => mapDispatchToProps(dispatch))(HomeScreen);
let SearchPage = connect(state => mapStateToProps, dispatch => mapDispatchToProps(dispatch))(SearchScreen);
const HomeStack = createStackNavigator(
{
Home: HomePage,
Search: SearchPage,
},
config
);
const mapStateToProps = (state) => {
return {
// State
}
};
const mapDispatchToProps = (dispatch) => {
return {
// Actions
}
};
export default tabNavigator;
Any ideas how can I do it? Thanks.
For this, i would suggest to use a component to wrap your application where you have your toast. For example:
App.js
render(){
return (
<Provider store = { store }>
<View style={styles.container}>
<AppContainer/>
</View>
</Provider>
)
}
Where your AppContainer would have a render method similar to this:
render(){
return (
<Frament>
<StatusBar barStyle="default"/>
<AppNavigator />
<Toast></Toast>
</Fragment>
)
}
Then (as you are using redux) you can connect your AppContainer. After that, just make this component aware of changes on redux using componentDidUpdate
componentDidUpdate = (prevProps) => {
if(this.props.redux_toast.visible !== prevProps.redux_toast.visible){
this.setState({
toastVisible : this.props.redux_toast.visible,
toastMessage: this.props.redux_toast.message
})
}
}
This is just an example on how it could be done by using redux, I don't know how your toast or redux structure is, but it should be an available solution for your use case.
EDIT.
This is how it should look like:
//CORE
import React from 'react';
//REDUX
import { Provider } from 'react-redux';
import store from './redux/store/store';
import AppContainer from './AppContainer';
export default () => {
return (
<Provider store={store}>
<AppContainer />
</Provider>
)
}
AppContainer.js:
import React, { Component } from "react";
import { View, Stylesheet } from "react-native";
import StatusBar from "path/to/StatusBar";
import AppNavigator from "path/to/AppNavigator";
import Toast from "path/to/Toast";
import { connect } from "react-redux";
class AppContainer extends Component {
constructor(props){
super(props);
this.state={
toastVisible:false,
toastMessage:""
}
}
componentDidUpdate = (prevProps) => {
if(this.props.redux_toast.visible !== prevProps.redux_toast.visible){
this.setState({
toastVisible : this.props.redux_toast.visible,
toastMessage: this.props.redux_toast.message
})
}
}
render(){
return (
<View style={styles.container}>
<StatusBar barStyle="default"/>
<AppNavigator />
<Toast visible={this.state.toastVisible}
message={this.state.toastMessage}
/>
</View>
)
}
}
const styles = StyleSheet.create({
container:{
flex:1
}
})
const mapStateToProps = state => ({ ...yourMapStateToProp })
const mapDispatchToProps = state => ({ ...mapDispatchToProps })
export default connect(mapStateToProps, mapDispatchToProps)(AppContainer)
Rest of the code remains untouched, you need to dispatch an action that changes a props that your appContainer's componentDidUpdate is listening to (in the example i called it redux_toast.visible).

React-navigation Error boundary that can navigate

What would be the cleanest way to wrap all screens managed by react-navigation in an error boundary that can also navigate. My current approach involves a top level component like:
class App extends Component{
navigateTo(routeName) {
this.navigator && this.navigator.dispatch(NavigationActions.navigate({ routeName }));
}
render(){
return (
<Provider store={store}>
<PersistGate persistor={persistor}>
<MenuProvider>
<ErrorBoundary navigateTo={this.navigateTo.bind(this)}>
<AppNavigator
ref={navigator=> {
NavigationService.setTopLevelNavigator(navigator);
this.navigator = navigator;
}}
/>
</ErrorBoundary>
</MenuProvider>
</PersistGate>
</Provider>
)
}
}
with a rather standard ErrorBoundary:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { error: null, info: null };
}
componentDidCatch(error, info) {
this.setState({error, info});
this.props.navigateTo('SomeScreen');
}
render() {
if (this.state.error) {
return (
<Container>
<Content>
<Text> Got error: {JSON.stringify(this.state.error)}, info {JSON.stringify(this.state.info)} </Text>
</Content>
</Container>
)
}
return this.props.children;
}
}
However when an error occurs the navigator gets unmounted and ref is called again with null.
Alternatively, is there a way to have an ErrorBoundary as a descendant of AppNavigator that catches errors from any screen and can also access the navigator, eventually through a NavigationService?
you should be able to do this with custom navigators, below is an example with the new react-navigation V3 createAppContainer api as per, https://reactnavigation.org/docs/en/custom-navigators.html.
We have just implemented a revision in our app to achieve this when upgrading to V3.
That way your AppNavigator will still be mounted when the error boundary hits and will have access to your navigation props.
const StackNavigator = createStackNavigator({..});
class AppNavigator extends React.Component {
static router = StackNavigator.router;
render() {
const { navigation } = this.props;
return (
<ErrorBoundary navigation={navigation}>
<StackNavigator navigation={navigation} />
</ErrorBoundary>
);
}
}
const AppContainer = createAppContainer(AppNavigator);
export default AppContainer;