React Navigation: Navigating outside a component - react-native

I am trying to navigate my app from outside of a component. Specifically, I am using a fetch interceptor and I want to navigate whenever an error response is received.
I followed the example here: https://reactnavigation.org/docs/navigating-without-navigation-prop/
However, my app is still giving me an error saying that either a navigator isn't rendered or the navigator hasn't finished mounting:
Screenshot of app with error message
As far as I can tell, neither of those situations apply. The app is loaded and rendered with a navigator in place before I try to actually navigate
My App.jsx:
// ... imports and so on ...
fetchIntercept.register({
response: (response) => {
if (response.status === 401) {
// Unverified subscription
RootNavigation.reset({ index: 0, routes: [{ name: 'Intercept' }] });
}
return response;
},
});
{ ... }
const InterceptNavigator = createStackNavigator(
{
Application: {
screen: ApplicationScreen,
},
Intercept: {
screen: SubscriptionInterceptScreen,
},
},
{
initialRouteKey: 'Application',
},
);
const App = createAppContainer(InterceptNavigator);
export default () => {
React.useEffect(() => {
RootNavigation.isMountedRef.current = true;
return () => { RootNavigation.isMountedRef.current = false; };
}, []);
return (
<NavigationContainer ref={RootNavigation.navigationRef}>
<App />
</NavigationContainer>
);
};
RootNavigation.js:
import * as React from 'react';
export const isMountedRef = React.createRef();
export const navigationRef = React.createRef();
export function navigate(name, params) {
if (isMountedRef.current && navigationRef.current) {
navigationRef.current.navigate(name, params);
}
}
export function reset(options) {
if (isMountedRef.current && navigationRef.current) {
navigationRef.current.reset(options);
}
}
I also inserted a number of console logs throughout and all of them showed that the app is loaded, that the navigationRef is current, and that the isMountedRef is also current before the app tries to navigate

Try .resetRoot() instead of .reset(). I think .reset() needs a state as an argument.

Found the solution. The issue is that I had a mixture of version 4 and version 5 code (and was referring to mixed documentation).
To fix the issue I removed references to version 5 code and then followed the steps on this page to get the navigator working: https://reactnavigation.org/docs/4.x/navigating-without-navigation-prop/

Related

React Navigation dynamic screens and deep linking

I want to create dynamically tab screens with react navigation, but I don't know how to figure it out with the deep linking.
I would like to have my tabs screens accessible with deeplinking like : /tabs/:tabId but I don't know how to deal with the linking config.
If there is someone who can help me, I have created this snack :
https://snack.expo.dev/#natinho68/getting-started-%7C-react-navigation
First you need to set it up via documentation
This is minimal example. Root stack is a stack navigator which has 3 screens: Splash, Login and App. And App is another stack navigator(nested) with 2 screens: Dashboard and Login.
const deepLinkDevPrefix = "https://custom.server.org"
const deepLinkLocalProtocol = "customprotocol://"
const deepLinkConfig = {
screens: {
Splash: "Splash",
Login: "Login",
App: {
path: "App",
screens: {
Dashboard: "Dashboard",
Profile: "Profile,
},
},
}
}
Inside of getInitialURL you can handle url that opened the app.
const App = () => {
const getInitialURL = async () => {
// Check if app was opened from a deep link
const url = await Linking.getInitialURL();
console.log('This is url')
console.log(url)
if (url !== null) {
return url;
}
return undefined
}
const linking = {
prefixes: [deepLinkDevPrefix, deepLinkLocalProtocol],
deepLinkConfig,
getInitialURL
};
return (
<NavigationContainer linking={linking}>
<RootStack />
</NavigationContainer>
);
}
Here you can find out how to navigate without navigation prop.

How Redirect to Login if page is protected and the user is not signed in?

In my App I have some public screens that are accessible even if the user is not logged in, and some screens are protected (you must be logged in to access them).
My solution to the problem is to check the component willFocus Listener and if not logged in, the user should be redirected to the loginPage.
export async function ProtectRoute(navigation){
//if page will enter and the user is not authenticated return to login
navigation.addListener(
'willFocus',
async () => {
let token = await getTokenAsync();
if(!token){
navigation.navigate('Login');
}
})
}
In my screen I Call this function in ComponentWillMount lifecycle.
The issue is that it takes like a second to verify the token and the page is displayed briefly.
How can I make it so that he goes directly to the Login Page without that lag ?
I wrote a quick example below. You can examine and use it.
import React, { Component } from "react";
import { Text, View } from "react-native";
const withAuth = WrappedComponent => {
class AuthenticationScreen extends Component {
constructor(props) {
super(props);
this.state = {
isAuthenticated: false
};
props.navigation.addListener("willFocus", async () => {
await this.checkAuth();
});
}
remoteReuqest = async () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(true);
}, 2000);
});
};
checkAuth = async () => {
const result = await this.remoteReuqest();
if (result) {
this.setState({
isAuthenticated: true
});
} else {
this.props.navigation.navigate("Login");
}
};
render() {
if (!this.state.isAuthenticated) {
return <Text>Waiting...</Text>;
}
return <WrappedComponent {...this.props} />;
}
}
return AuthenticationScreen;
};
export default withAuth;
You can use it as follows.
import React, { Component } from "react";
import { Text, StyleSheet, View } from "react-native";
import withAuth from "./withAuth";
class ContactScreen extends Component {
render() {
return (
<View>
<Text> Contact Screen </Text>
</View>
);
}
}
const styles = StyleSheet.create({});
const extendedComponent = withAuth(ContactScreen);
extendedComponent.navigationOptions = {
title: "Contact"
};
export default extendedComponent;
The issue is that it takes like a second to verify the token and the page is displayed briefly.
The reason is because reading/writing from/to AsyncStorage is an asychronous operation.
In my screen I Call this function in ComponentWillMount lifecycle.
I suggest you to not use ComponentWillMount lifecycle because it's deprecated and it will be removed from React (https://reactjs.org/docs/react-component.html#unsafe_componentwillmount)
After this introduction, now i show you how I have achieved this in my app: CONTEXT API! (https://reactjs.org/docs/context.html)
Context provides a way to pass data through the component tree without having to pass props down manually at every level.
How to implement context api:
the context will be the 'state' of your App.js file. You root App.js will be the provider of the context, while other views which will need the context are called the consumers of the context.
First of all, you need to create a 'skeleton' of your context into a separate file, something like this:
// AuthContext.js
import React from 'react'
const AuthContext = React.createContext({
isLogged: false,
login: () => {},
logout: () => {}
})
export default AuthContext
Your App.js will import, contain and initialize the context:
// App.js
// all necessary imports
import AuthContext from '....'
export default class App extends React.Component {
constructor (props) {
super(props)
this.state = {
isAuth: false,
login: this.login,
logout: this.logout
}
login = async userData => {
// do your login stuff and then
this.setState({ isAuth: true })
}
logout = async () => {
// do your logout stuff and then
this.setState({ isAuth: false })
}
async ComponentDidMount () {
// check the asyncStorage here and then, if logged:
this.setState({ isAuth: true })
}
render () {
return (
<AuthContext.Provider value={this.state}>
<AppContainer />
</AuthContext.Provider>
)
}
Then, into the View contained into AppContainer, you could access context like this:
import AuthContext from '.....'
// all necessary imports
export default class YourView extends React.Component<Props, State> {
constructor (props) {
super(props)
this.props = props
this.state = { ... }
}
// THIS IS IMPORTANT
static contextType = AuthContext
// with this, you can access the context through 'this.context'
ComponentDidMount () {
if (!this.context.isAuth) this.props.navigation.navigate('login')
}
Advantages of this approach:
Checking a boolean is so fast that you will not notice a blank screen.
Sharing an Authentication Context everywhere in you app
Making the access to asyncstorage only the first time that app mounts and not everytime you need to check if the user is logged
Sharing methods to login/logout everywhere in your app

How can I change a parent state through a react navigation component?

I'm new to React Native and having trouble figuring out how to accomplish this. Currently I have an app structure something like this:
App.js -> Authentication.js -> if(state.isAuthenticated) Homepage.js, else Login.js
I'm currently changing the isAuthenticated state on a logout button on the homepage. I'm now trying to add in a drawer navigator to the app, which would get returned to the authentication page in place of the homepage. So I'm not sure how to pass the state change through the drawernavigator component to the Authentication page.
Currently my Homepage has a button that has:
onPress={() => this.props.logout()}
And the authentication page has:
export default class Authentication extends Component {
constructor(props){
super(props);
this.state = {
isAuthenticated: false,
isLoading: false
}
this.login = this.login.bind(this);
this.logout = this.logout.bind(this);
}
login() {
AsyncStorage.setItem("user", JSON.stringify({email: this.state.email, password: this.state.password}))
.then(results => {
this.setState({isAuthenticated: true});
});
}
logout() {
AsyncStorage.clear()
.then(result => {
this.setState({isAuthenticated: false});
});
}
componentDidMount(){
this.setState({isLoading: true});
AsyncStorage.getItem("user")
.then(results => {
const data = JSON.parse(results);
if (data) {
this.setState({isAuthenticated: true});
}
this.setState({isLoading: false});
});
}
render() {
if (this.state.isLoading){
return(
<Splashpage />
);
}
if (!this.state.isAuthenticated){
return (
<Login login={this.login}/>
);
}
return (
<Homepage logout={this.logout}/>
);
}
}
So I made a Navigation.js page where I'm creating a drawernavigator and going to be returning this instead of the Homepage.
export default Navigation = createDrawerNavigator({
Home: {
screen: Homepage,
},
WebView: {
screen: WebView,
},
});
But I'm not sure how to pass along the state change from the homepage, through the Navigation component to the parent Authentication page. Any help would be much appreciated.
You could pass a callback through navigate:
this.props.navigation.navigate('yourTarget',{callback:function});
In yourTraget you can access it via:
this.props.navigation.state.params.callback(isAuthenticated)
Here the documentation: https://reactnavigation.org/docs/en/params.html
I hope this is what you were looking for! Oh now I see you asked that already a while ago. Maybe you already moved on...

ignite 2.0: Android back button closing app react-navigation

I am using default ignite 2.0 boilerplate code.
Suppose I open a new screen using this.props.navigation.navigate('SecondScreen')
After opening the 'SecondScreen' if I am pressing android hardware back button it is closing app. 'SecondScreen' ui back button at top of screen is working fine.
App navigation code:
const PrimaryNav = StackNavigator({
Home: { screen: Home },
SecondScreen: { screen: SecondScreen }
}, {
// Default config for all screens
initialRouteName: 'Home',
navigationOptions: {
headerStyle: styles.header
}
})
I'm guessing you're using Redux integration, which means you'll need to handle the back button yourself as pointed out here. Also check out the reference for Backhandler.
What I've done is implement it like this:
import { BackHandler } from 'react-native';
import { NavigationActions } from 'react-navigation';
/* subscribe this to the redux store, should look something like this subscriber(store.dispatch, store.getState); */
export const subscriber = (dispatch, getState) => {
BackHandler.addEventListener('hardwareBackPress', () => {
const appShouldExit = true || false; //depending on which screen you're at
if (appShouldExit) {
BackHandler.exitApp();
}
const backAction = NavigationActions.back();
dispatch(backAction);
return true;
});
});
In my component I added this code and it is working fine now.
import { BackHandler } from 'react-native'
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this.props.navigation.goBack);
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.props.navigation.goBack);
}

sending the navigation params as props to other component

I am using RN v0.46.4 , react-navigation and sendbird.
I am developing a chat application.
What I am trying to do is that navigating the user to Msg screen with two params item (contain user info) and createdChannel .
They are passed but only once i.e. each time I navigate to Msg screen for different users , I receive the same value on which I have presses first.
Chat Screen
_chat(item){
// console.log(item);
const { navigate } = this.props.navigation
var userIds = [item.id,1];
sb = new SendBird({appId: APP_ID});
sb.connect(1, function(user, error) {
//console.log(user);
sb.GroupChannel.createChannelWithUserIds(userIds, true, item.firstname, function(createdChannel, error) {
if (error) {
console.error(error);
return;
}
//console.log(createdChannel);
navigate('Msg', { item, createdChannel })
});
Also, when I console createdChannel in _chat function , it gives the roght information as expected.
But when I console it in Msg screen , I receive only the first createdChannel created, already told above.
Msg Screen
super(props);
console.log(props.navigation.state.routes[1].params.createdChannel);
My router structure:
const CustomTabRouter = TabRouter(
{
Chat: {
screen: ChatStack,
path: ""
}
}
ChatStack
const ChatStack= StackNavigator({
Chat:{screen: Chats,
navigationOptions: {
header: null,
}},
Msg: {
screen: Msg,
navigationOptions: ({ navigation}) => ({
title: `${navigation.state.params.item.firstname} ${navigation.state.params.item.lastname}`,
tabBarVisible: false
})
},
})
In your Msg screen's constructor function, try accessing the item and createdChannel by calling props.navigation.state.params.createdChannel.
super(props);
console.log(props.navigation.state.params.createdChannel);
console.log(props.navigation.state.params.item);
I found myself in a similar situation. In my case the problem was because passing params to nested screen was intentionally removed from the lib (1.0.0-beta21 to 1.0.0-beta22).
Note: But as of now, react-navigation version is v1.5.2
https://github.com/react-navigation/react-navigation/issues/3252
Anyway, my quick and dirty solution is to use screenProps to inject props into screen.
const ChatStack= StackNavigator({
Chat:{ screen: Chats, ... },
Msg: { screen: Msg, ... },
});
// Instead of exporting ChatStack directly, create a wrapper and pass navigation.state.params to screenProps
export default (props) => {
const params = props.navigation.state.params;
// const params = _.get(props, 'navigation.state.params');
return (
<ChatStack screenProps={params} />
);
};
Now all screens under ChatStack can access those props injected.