React-navigation Error boundary that can navigate - react-native

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;

Related

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

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>

React navigation drawer v5x useNavigation() with Component Class

I have tried many times to navigate using useNavigation() in my Component class. But I couldn't because according to the error I should use this method inside of a body function. I tried it inside render() method. It wasn't also helpful. Could you possibly help me if anyone knows?
import { useNavigation } from '#react-navigation/native';
export default class MenuDrawer extends React.Component{
render(){
const navigation = useNavigation();
return(
<View>
<Button onPress={()=>{navigation.navigate('detail')}} />
</View>
);
}
}
According to the docs here, you can wrap the component inside a function to use it
import { useNavigation } from '#react-navigation/native';
export default yourFunction(props) {
const navigation = useNavigation();
return <MenuDrawer navigation={navigation} />;
}
Your new MenuDrawer
export default class MenuDrawer extends React.Component {
render(){
const { navigation } = this.props;
return(
<View>
<Button onPress={()=>{navigation.navigate('detail')}} />
</View>
);
}
}

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 Native Navigation const { navigate } = this.props.navigation;

I am learning react-native navigation https://reactnavigation.org/docs/intro/ . I see in examples there
class HomeScreen extends React.Component {
static navigationOptions = {
title: 'Welcome',
};
render() {
const { navigate } = this.props.navigation;
return (
<View>
<Text>Hello, Chat App!</Text>
<Button
onPress={() => navigate('Chat')}
title="Chat with Lucy"
/>
</View>
);
}
}
I could not understand what exactly this line of code is for const { navigate } = this.props.navigation;
syntax has nothing to do with React Native
it is called Destructuring assignment in es6 / es2015
const { navigate } = this.props.navigation;
is equivilent to with exception to var and const .
var navigate = this.props.navigation.navigate
the example without Destructuring should look like this
class HomeScreen extends React.Component {
static navigationOptions = {
title: 'Welcome',
};
render() {
return (
<View>
<Text>Hello, Chat App!</Text>
<Button
onPress={() => this.props.navigation.navigate('Chat')}
title="Chat with Lucy"
/>
</View>
);
}
}
Include on your ServiceAction the this.props.navigation something like this:
<HomeScreen navigation={this.props.navigation}/>
because the props.navigation are by default on your parent component
and on HomeScreen component you will access to navition like:
..
goToSignUp() {
this.props.navigation.navigate('SignUp');
}
..
For me also was confusing before. Cheers!

Infinite loop with React-Redux and Navigation Experimental

I have an infinite loop that seems to be occurring when I use react-redux. I use Navigation Experimental which loads connectRouteScreen as the scene to be rendered through NavigationCardStack. I'm using RN 0.30. But also could reproduce this in 0.31-rc.0
[...]
Use Navigation Experimental to transition and load connectRouteScreen as a Scene
export default function connectRouteScreen(Scene, sceneProps){
class RouteScreen extends React.Component{
[...]
render() {
const { navigator, pathVariables } = this.props;
return (
<View style={styles.container}>
<Scene
navigator={navigator}
{...pathVariables.toJS()}
/>
</View>);
}
}
RouteScreen.propTypes = {...RouteScreenPropTypes};
const routeScreenProperties = extractSceneRendererProps(sceneProps);
/*return <Scene
navigator={routeScreenProperties.navigator}
{...routeScreenProperties.pathVariables.toJS()}
/>;
*/
return <RouteScreen
{...routeScreenProperties}
/>;
}
LoadingScreen is loaded as "Scene".
#connect(
() => {return {}},
(dispatch) => {
return {
loginActions: bindActionCreators(loginActions, dispatch),
}
})
export default class LoadingScreen extends React.Component {
constructor(props){
super(props);
}
shouldComponentUpdate(nextProps){
return false;
}
componentDidMount(){
const { navigator } = this.props;
this.props.loginActions.executeLoginFlow();
}
render() {
const Animatable = require('react-native-animatable');
return (
<Animatable.View
animation="pulse"
easing="ease-out"
iterationCount="infinite"
style={localStyle.container}>
<Icon name="logo" style={localStyle.iconStyle} size={150}/>
</Animatable.View>
);
}
};
So, If I return the Scene directly instead of RouteScreen, no problem.
If I remove the #connect syntax and escape this.props.loginActions..., no problem.
If I return RouteScreen and remove everything it does and just return the Scene => infinite loop.
Does anybody have any suggestions how to deal with this?