React Native - Adding logic to Stack Navigation - react-native

I have the following Stack Navigation:
const WelcomeStack = createStackNavigator({
UpdateProfileScreen: {
screen: UpdateProfileScreen,
},
SelectProfileScreen: {
screen: SelectProfileScreen,
},
PresentationModal: {
screen: PresentationModal,
}
}, {
initialRouteName: 'UpdateProfileScreen',
headerMode: 'none'
})
When a user is new, I show "UpdateProfileScreen" first, then I move to "SelectProfileSecreen" and then "PresentationModal".
If for some reason after "UpdateProfileScreen" user closes the app, next time they log in I will show "SelectProfileSecreen" and "PresentationModal". If they complete data, next time, they will only see the "PresentationModal"
Since I have set "UpdateProfileScreen" as initialRoute, it will always load first, even if it does not show.
So I was wondering if programatically I could change the initialRoute and/or if I do:
this.props.navigation.navigate("WelcomeStack")
I can add some logic in the same stack to handle the page to show?

I think your best option is using SwitchNavigator(RouteConfigs, SwitchNavigatorConfig) you can simply create it with createSwitchNavigator as it controls all the switching of navigators used mainly for authentication flow in most react native apps.
With that said, for your case i think its the most suitable way to achieve the desired behavior.
Please be careful if publishing an app using something along the lines of :
this.props.navigation.navigate("WelcomeStack")
as it can be a serious security vulnerability
docs: https://reactnavigation.org/docs/1.x/switch-navigator/
snack example: https://snack.expo.io/#react-navigation/auth-flow

I had a similar problem here how i resolved it:
Added SwitchNavigator like so:
let Navigation = createSwitchNavigator(
{
AuthLoading: AuthLoading,
LoginNavigator: LoginNavigator,
HomeNavTab: tabNavigator,
LoggedInChose: LoggedInChose
},
{
initialRouteName: "AuthLoading"
}
);
The AuthLoading stack is the first to load and decides where the app go next:
import React from "react";
import { connect } from "react-redux";
class AuthLoading extends React.Component {
constructor(props) {
super(props);
this.checkUserSession();
}
checkUserSession = () => {
const userData = this.props.userData;
if (userData) {
const residencesNumber = userData.nbreResidences;
if (residencesNumber == 1) {
this.props.navigation.navigate("HomeNavTab");
} else {
this.props.navigation.navigate("LoggedInChose");
}
} else {
this.props.navigation.navigate("LoginNavigator");
}
};
// Render any loading content that you like here
render() {
return (
null
);
}
}
const mapStateToProps = state => {
return {
userData: state.userData
};
};
export default connect(mapStateToProps)(AuthLoading);
Maybe you get an idea from this.

Related

Going back and forward between different stacks via "Back" key

Here is my code:
const Stack1 = createStackNavigator(
{
Aone: AoneScreen,
Atwo: AtwoScreen,
}
const Stack2 = createStackNavigator(
{
Bone:BoneScreen,
Btwo: BtwoScreen,
}
const Stack3 = createStackNavigator(
{
Cone:ConeScreen,
Ctwo: CtwoScreen,
}
const TabNavigator = createBottomTabNavigator(
Stack1,
Stack2,
Stack3
)
When I'm in a stack like "AoneScreen" and I move into another stack, say "CtwoScreen", and then press "back" button, instead of moving back to the first stack AoneScreen, it moves to the top of the second stack (ConeScreen) As it should! But that's not what I desire. what I want is go back to the original stack as the back button is pressed (in this case "AoneScreen" ) and I was wondering if that's possible.
You can implement custom behaviour for back button on screen "CtwoScreen" with BackHandler from React Native, it only depends on how dynamic this needs to be.
From documentation:
import { BackHandler } from "react-native";
componentDidMount() {
this.backHandler = BackHandler.addEventListener('hardwareBackPress',this.handleBackPress);
}
componentWillUnmount() {
this.backHandler.remove()
}
handleBackPress = () => {
// add some kind of custom check if you want this behaviour only sometimes, nav params are a good way
if (this.props.navigation.state.params.isFromAOneScreen) {
this.props.navigation.setParams({ isFromAOneScreen: false });
this.props.navigation.popToTop(); // to remove COneScreen from stack, from transition
this.props.navigation.navigate("AOneScreen");
return true;
}
return false;
}

Changing state in React native App.js from another component

I'm making authentication in an app, and I'm kind of stuck. I have 2 different navigations. One shows if the user is logged in and another one if not. Basically, a Sign in screen. It's working fine if I change the value manually upon the start. But I can't find a way to change a state when a user signs in, for example. Even though the value in auth module changes, it doesn't update in App.js So how can I update the App.js's state from Sign in screen, for example?
import React, { Component } from 'react';
import { AppRegistry, Platform, StyleSheet, Text, View } from 'react-native';
import DrawerNavigator from './components/DrawerNavigator'
import SignedOutNavigator from './components/SignedOutNavigator'
import auth from './auth'
type Props = {};
export default class App extends Component<Props> {
constructor(props) {
super(props)
this.state = {
isLoggedIn: auth.isLoggedIn
}
}
render() {
return (
(this.state.isLoggedIn) ? <DrawerNavigator /> : <SignedOutNavigator />
);
}
}
AppRegistry.registerComponent('App', () => App)
and my auth module, which is very simple
import { AsyncStorage } from 'react-native';
// try to read from a local file
let api_key
let isLoggedIn = false
function save_user_settings(settings) {
AsyncStorage.mergeItem('user', JSON.stringify(settings), () => {
AsyncStorage.getItem('user', (err, result) => {
isLoggedIn = result.isLoggedIn
api_key = result.api_key
});
isLoggedIn = true
});
}
module.exports.save_user_settings = save_user_settings
module.exports.api_key = api_key
module.exports.isLoggedIn = isLoggedIn
First off, there are loads of ways to approach this problem. Because of this I'm going to try explain to you why what you have now isn't working.
The reason this is happening is because when you assign auth.isLoggedIn to your isLoggedIn state, you are assigning the value once, kind of as a copy. It's not a reference that is stored.
In addition to this, remember, React state is generally only updated with setState(), and that is never being called here, so your state will not update.
The way I would approach this problem without bringing in elements like Redux, which is overkill for this problem by itself, is to look into building an authentication higher order component which handles all the authentication logic and wraps your entire application. From there you can control if you should render the children, or do a redirect.
Auth Component
componentDidMount() {
this._saveUserSettings(settings);
}
_saveUserSettings(settings) {
AsyncStorage.mergeItem('user', JSON.stringify(settings), () => {
AsyncStorage.getItem('user', (err, result) => {
isLoggedIn = result.isLoggedIn
api_key = result.api_key
});
this.setState({isLoggedIn: true});
});
}
render() {
const { isLoggedIn } = this.state;
return isLoggedIn ? this.props.children : null;
}
App.js
render() {
<AuthComponent>
//the rest of authenticated app goes here
</AuthComponent>
}
Here's a really quick, incomplete example. But it should showcase to you how you may want to lay your authentication out. You'll also want to consider error handling and such, however.

How to navigate to a specific screen and disallow going back with react-native-navigation

I am using react-native-navigation and I have a stack of screens.
When I go from screen A to screen B, I don't want to give the user the option to go back to screen A, just forward.
I am trying Navigation.popTo("screen.B.id") but I'm getting this error:
Is there any way to achieve this? Thanks in advance.
React-navigation
You can reset the stack like this:
const resetAction = StackActions.reset({
index: 0,
actions: [NavigationActions.navigate({ routeName: 'screenB' })],
});
this.props.navigation.dispatch(resetAction);
React-native-navigation
A workaround would be to catch the back listener like so:
import {BackHandler} from 'react-native';
export default class RoomType extends Component {
_didFocusSubscription;
_willBlurSubscription;
constructor(props) {
super(props);
this._didFocusSubscription = props.navigation.addListener('didFocus',payload =>
BackHandler.addEventListener('hardwareBackPress', this.onBackButtonPressAndroid)
);
}
}
componentDidMount() {
this._willBlurSubscription = this.props.navigation.addListener('willBlur', payload =>
BackHandler.removeEventListener('hardwareBackPress', this.onBackButtonPressAndroid)
);
}
componentWillUnmount() {
this._didFocusSubscription && this._didFocusSubscription.remove();
this._willBlurSubscription && this._willBlurSubscription.remove();
}
onBackButtonPressAndroid = () => {
//code when you press the back button
};
You could try with setting Screen B to your new root.
setStackRoot(componentId, params)
Maybe you have to popToRoot(componentId, mergeOptions?) if necessary.
Source: react-native-navigation docs
In react-native-navigation, there are 2 options you could choose from to achieve what I believe you are looking for.
try adding the below to your topBar option specifically in the child component you are choosing.
backButton: {
visible: false
}
for a small example, the child you don't want a back option for put:
component: {
id: 'screenB',
name: 'screenB',
options: {
title: {
text: 'screenB'
},
topBar: {
// the other options you want
backButton: {
visible: false
}
}
}
}
you can completely reset the root navigation to the new screen.
In my Opinion,
Option 1. is a simple way to flat out just remove the back button from a specific screen to disable the ability to ever go back to the original screen.
Option 2. is nice when you want to remove the previous screen from the entire equation of the app itself.
my personal use-case for option 2:
I made an app that originally opens to a login/register stack. Once logged in/ registered, I save that information to the AsyncStorage and completely reset the root to the home page.
When opening the app for the second time, it checks for user info from the AsyncStorage. If the app finds user information, It sets the root for the home page and rest of the app. If the app doesn't find user information, it sets the root to the login/register stack and the cycle continues.
I hope this helps!
Use createSwitchNavigator from 'react-navigation' for both screens,which will not let back button of second screen to switch to first screen and also header will not be there with back arrow.
In your App.js,
import * as React from 'react';
import { Text, View, StyleSheet } from 'react-native';
import { createSwitchNavigator,createAppContainer } from 'react-navigation';
import ScreenOne from './components/ScreenOne ';
import ScreenTwo from './components/ScreenTwo ';
const App=createSwitchNavigator({
ScreenOne :{screen:ScreenOne },
ScreenTwo :{screen:ScreenTwo }
});
export default createAppContainer(App);

React-native passing props from App.js to other files

I'm trying to pass device token from my App.js to my Login component in react-native.
I'm trying something like this :
Here's my app.js :
const RootStack = createStackNavigator(
{
Login: {
screen: Login,
navigationOptions :{ headerLeft: null}
},
Tab: {
screen: Tab,
navigationOptions :{ headerLeft: null }
}
},
{
initialRouteName: 'LoginScreen'
}
);
const MyApp = createAppContainer(RootStack);
constructor(props) {
super(props);
this.state = {
token: ''
}
}
async componentDidMount() {
this.state.token = await firebase.messaging().getToken().then(token=> { return token;});
this.checkPermission();
this.createNotificationListeners();
}
render() {
return (
<MyApp token={this.state.token}></MyApp>
);
}
And my Login.js :
export default class LoginScreen extends React.Component {
constructor(props) {
super(props);
this.state = {
mail:"",
password: "",
token: this.props.token
}
}
async Login(){
console.log(this.state.token)
}
}
Of course it's not working, I don't know how to pass the token via components, or via stacknavigator without using .navigate(). Besides even if I fill the const with a single string, it's not working, so what am I doind wrong ? And is it going to be different with the token ?
screenProps
You need to use screenProps. You should check the documentation to see more about it.
This is how I would use it in your case.
<MyApp screenProps={{ token: this.state.token}} />
Then you can access it in your screens using this.props.screenProps.token.
Here is a snack that I created that shows passing values through a navigator. https://snack.expo.io/#andypandy/passing-props-through-a-navigator
In your current LoginScreen you are trying to set the value of the token in the state. Bear in mind, your page may be constructed before the token is created so the initial value for the token may not exist so you may prefer to capture the value in componentDidUpdate, see the docs for more.
Alternatively you could store the token in AsyncStorage.
Firebase
When getting your token from Firebase, you are mixing promises and async/await notation. Choose one notation and stick with it.
You are setting your state incorrectly. You are mutating state with your line
this.state.token = await firebase.messaging().getToken().then(token=> { return token;});
You should not mutate state as it can get overwritten, and it can cause side effects that you don't expect. You should use this.setState({token}); to set your token value into state.
This is how I would refactor your componentDidMount
async componentDidMount() {
// await functions can throw so always wrap them in a try/catch
try {
// get the token from firebase https://rnfirebase.io/docs/v5.x.x/messaging/device-token
let token = await firebase.messaging().getToken();
if (token) {
// if we have a token then set the state and use a callback
// once the state has been the callback calls checkPermissions and createNotificationListeners
this.setState({token}, () => {
this.checkPermission();
this.createNotificationListeners();
});
}
} catch (err) {
console.log(err);
}
}
Additional reading
The following articles by Michael Chan are a great introduction into state.
https://medium.learnreact.com/setstate-is-asynchronous-52ead919a3f0
https://medium.learnreact.com/setstate-takes-a-callback-1f71ad5d2296
https://medium.learnreact.com/setstate-takes-a-function-56eb940f84b6
The following article shows the differences between promises and async/await
https://medium.com/#bluepnume/learn-about-promises-before-you-start-using-async-await-eb148164a9c8

React Native Navigation and Redux Persist

I am trying to integrate redux-persist with wix react-native-navigation. However, I am unable to find any examples or documentation stating the boilerplate code needed to integrate the both libraries.
I was wondering if anyone would like to share their solution if they have solved this issue ?
First of all, the basic setup should be the similar with or without react-native-navigation as described in the documentation in store.js:
import { persistStore, persistCombineReducers } from 'redux-persist'
import storage from 'redux-persist/es/storage' // default:
localStorage if web, AsyncStorage if react-native
import reducers from './reducers' // where reducers is an object of
reducers
const config = {
key: 'root',
storage,
}
const reducer = persistCombineReducers(config, reducers)
function configureStore () {
// ...
let store = createStore(reducer)
return store
// We'll skip persistStore for now
// let persistor = persistStore(store)
//return { persistor, store }
}
The persistStore call is commented out as we'll do it below. The persistStore method takes a callback in its third argument. The callback is executed after the state is restored/rehydrated. This is nice because this means we can delay starting the screen(s) until the state is rehydrated.
Let's assume you have the following bootstrap code in App.js:
store = configureStore()
registerScreens(store, Provider)
Navigation.startTabBasedApp({
tabs: [{...},]
})
Now we can add persistStore and wrap your bootstrap code in it like this:
store = configureStore()
persistStore(store, null, () => {
registerScreens(store, Provider)
Navigation.startTabBasedApp({
tabs: [{...},]
})
})
Note:
In v4, you pass config instead of null: persistStore(store, config, callback)
In case you're looking to integrate it with react-native-navigation v2, in App.js, make sure you call persistStore() inside the registerAppLaunchedListener() :
import { persistStore } from 'redux-persist';
...
Navigation.events().registerAppLaunchedListener(() => {
persistStore(store, null, () => {
Navigation.registerComponentWithRedux(...);
...
Navigation.setRoot({...})
...
})
})
Adding to his solution you can also use subscribe() to check if your user is still logged in. That way they don't need to sign in again if they completely close the app (for those users with a login system) and since it is only called once the store is persisted, you can start your app after this is checked.
import {Platform, AsyncStorage, AppState} from "react-native"
import {Navigation} from "react-native-navigation"
import {registerScreens} from "./routes"
import {Provider} from "react-redux"
import configureStore from "./stores/reduxStore"
import {Component} from "react"
const storage = configureStore()
registerScreens(Provider, storage.store)
let startapp = screen => {
Navigation.startSingleScreenApp({
screen: {
screen, // unique ID registered with Navigation.registerScreen
navigatorStyle: {
navBarHidden: true,
statusBarHidden: false,
statusBarColor: "white",
statusBarTextColorScheme: "dark"
}, // override the navigator style for the screen, see "Styling the navigator" below (optional)
navigatorButtons: {} // override the nav buttons for the screen, see "Adding buttons to the navigator" below (optional)
},
drawer: {
left: {
screen: "Drawer", // unique ID registered with Navigation.registerScreen
passProps: {} // simple serializable object that will pass as props to all top screens (optional)
}
},
tabsStyle: {
// optional, add this if you want to style the tab bar beyond the defaults
tabBarButtonColor: "#ffff00", // optional, change the color of the tab icons and text (also unselected). On Android, add this to appStyle
tabBarSelectedButtonColor: "#ff9900", // optional, change the color of the selected tab icon and text (only selected). On Android, add this to appStyle
tabBarBackgroundColor: "#551A8B", // optional, change the background color of the tab bar
initialTabIndex: 1 // optional, the default selected bottom tab. Default: 0. On Android, add this to appStyle
},
appStyle: {
orientation: "portrait"
}
})
}
storage.persistor.subscribe(() => {
storage.store.getState().user.logged
? startapp("mainscreen")
: startapp("loginscreen")
})
We actually dont need redux-persist. We can make our own redux-persist with:
redux + store.subscribe(handlechange)
handleChange function will run when ever something changes in our store.
Also Using aync-await(promise) we are not blocking the main execution thread.
So Inside create store add something like:
store.subscribe(async ()=>{
try {
await AsyncStorage.setItem("store", JSON.stringify(store.getState()));
} catch (error) {
// Error
}
})
Then inside App.js(first component to load). use AsyncStorage.getItem('store'). Then update the store before app starts.
localstorage on the web is a synchronous function which blocks the main thread.
AsynsStorage in react-native doesn't blocks the main thread.