React Navigation - How to pass data across different screens in TabNavigator? - react-native

I have a TabNavigator, and in each tab is a StackNavigator. Inside the StackNavigator, I have screens. The screens in each Tab do not call each other directly; the TabNavigator handles the screen changes when a tab is pressed.
In the first tab, if the user clicks a button, some data is created. If the user then navigates to the second Tab, I would like to pass this data to the screen in the second Tab.
Here is a demo of the code:
import React from 'react';
import { Button, Text, View } from 'react-native';
import {
createBottomTabNavigator,
createStackNavigator,
} from 'react-navigation';
class HomeScreen extends React.Component {
doIt = () => {
this.props.navigation.setParams({results: ['one', 'two']}); // <--- set data when user clicks button.
}
render() {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
{/* other code from before here */}
<Button
title="Set Results"
onPress={this.doIt}
/>
</View>
);
}
}
class SettingsScreen extends React.Component {
render() {
console.log(this.props.navigation); // <--- console out when user clicks on this tab
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Settings</Text>
</View>
);
}
}
const HomeStack = createStackNavigator({
Home: HomeScreen,
});
const SettingsStack = createStackNavigator({
Settings: SettingsScreen,
});
export default createBottomTabNavigator(
{
Home: HomeStack,
Settings: SettingsStack,
},
{
}
);
The this.props.navigation.state.params never gets the data results in the second Tab. There isn't even a key for it, so if I try to access this.props.navigation.state.params.results, it will be undefined.
This is confusing because I thought props.navigation is passed to all screens automatically.
How can I pass data from one screen to another through the TabNavigator, using just react-navigation? I have seen answers that say to use Redux, but I would not like to import another library if all I want is to keep some state across screens in different react navigators.

It may seem that this.props.navigation.state.params is only able to old one parameter? Possibly? Try this:
doIt = () => {
this.props.navigation.setParams({results: 'one'}); // <--- set data when user clicks button.
}
console.log(this.props.navigation.state.params.results);

Setting props did not work when passing data across different tabs. I even tried playing with AsyncStorage, trying to save and retrieve them in different tabs.
I ended up using Redux to save my states, and that has worked well so far.

I came across a similar problem. I had a multi page form that the client insisted on having each step be enclosed in a tab on a tab bar. I used the react navigation createMaterialTopTabNavigator to create the navigator and couldn't find an easy way to pass the form data between tabs.
What I end up doing was using react's Context API and wrapped the tab navigator in a root form container that provides the context value to the navigator and routes inside. Here is how I did it:
Root form container
// MultiScreenForm.js
imports...
import MultiScreenFormNavigator from './MultiScreenFormNavigator'
export const FormContext = React.createContext()
class MultiScreenForm extends Component {
constructor(props) {
super(props)
this.state = {
// formDataHere
formUpdaters: {
onToggleOptIn: this.handleToggleOptIn // example updater method
// other
}
}
}
handleToggleOptIn = () => {
// toggle opt in form data with this.setState
}
render() {
return (
<FormContext.Provider value={this.state}>
<MultiScreenFormNavigator />
</FormContext.Provider>
)
}
}
export default MultiScreenForm
Example form page
// ProfileForm.js
imports...
import { FormContext } from './MultiScreenForm'
class ProfileForm extends Component {
render() {
// FormContext.Consumer uses function as child pattern
return (
<FormContext.Consumer>
{ (context) => (
// our form can now use anything that we pass through the context
// earlier, we passed the root form's state including an updater
<button onPress={context.formUpdaters.onToggleOptIn} />
// ...
)
}
</FormContext.Consumer>
)
}
}
export default ProfileForm
Tab navigator
// MultiScreenFormNavigator.js
imports...
import ProfileForm from './ProfileForm'
import { createMaterialTopTabNavigator } from 'react-navigation'
const MultiScreenFormNavigator = createMaterialTopTabNavigator(
{
Profile: ProfileForm,
// AnotherForm: AnotherForm
},
// { navigator options here... }
)
export default MultiScreenFormNavigator
We then render the MultiScreenForm instead of the tab navigator directly.
This worked for me but I feel there should be an easier way to do this. I hope people who read this can share their approaches.

#tempomax
tried same with AsyncStorage but data came in with a delay.
Sometimes you don't need Redux if your app stays small.
So tried to find a way without Redux.
Here is what I came up with
I hope it's not too late to answer.
Solved it with NavigationEvents and setting params to Route.
The problem with tab is that you canĀ“t pass params to screen because navigation.navigate will be triggered automatically if createMaterialTopTabNavigator is swiped or clicked on non-active TabBar Button.
This can be solved with NavigationEvent like follow.
import React from 'react';
import { View } from 'react-native';
import { NavigationEvents } from 'react-navigation';
const MyScreen = () => (
<View>
<NavigationEvents
onWillFocus={payload => console.log('will focus',payload)}
onDidFocus={payload => console.log('did focus',payload)}
onWillBlur={payload =>
/*
if screen is about to change this will be triggred
In screen 'MyScreen2' you can get it with navigation.params
*/
this.props.navigation.navigate('MyScreen2', { name: 'Brent' })
}
onDidBlur={payload => console.log('did blur',payload)}
/>
{/*
Your view code
*/}
</View>
);
export default MyScreen;
Now you can get the data in MyScreen2
/* 2. Get the param, provide a fallback value if not available */
const { navigation } = this.props;
const itemId = navigation.getParam('name', 'DefaultName');
const otherParam = navigation.getParam('otherParam', 'some default value');

If you are using React Native Navigation Version 5.x with a DrawerNavigation, you can do this using
in screen 1:
<Button
onPress={() => {
this.props.navigation.navigate(<ScreenNameOfDrawerScreen>,
{screen:'<ScreenNameInTabDrawer>',params:{your_json_Data}});
}} />
in screen 2:
............
render() {
if(this.props.route.params!=undefined){
if(this.props.route.params.your_json_Data!=null){
// Use this.props.route.params.your_json_Data. It is your json data.
}
}
return (
..............

Related

NativeBase: Button does not work, but ReactNative's Button does

Experiencing a strange issue in my React Native project for Android.
Using React-Navigation, I have a component with a button inside. This button should navigate to a new screen.
Thing is, the built-in button of React Native works like a charm, while the button of Native Base does not. I am completely confused, even more because I use this Native Base Button in another location, too. And there it works fine.
What is going on here?
Here, you see the application works with the built-in React Native button:
On the opposite, using the button of Native Base, it not only does not work, even styles are not applied.
Here is the code with the React Native button:
import React from "react";
import { Button, View, Text, StyleSheet } from "react-native";
import { withNavigation } from "react-navigation";
type Props = { navigation: any };
const ButtonTestScreen: React.FC<Props> = ({ navigation }) => {
return (
<View>
<Button
title="Hi i am a button"
onPress={() => navigation.navigate("Details")}
></Button>
</View>
);
};
export default withNavigation(ButtonTestScreen);
And the code with Native Base button:
import React from "react";
import { Button, View, Text, StyleSheet } from "react-native";
import { withNavigation } from "react-navigation";
import ButtonNavigate from "../../components/atoms/ButtonNavigate/ButtonNavigate";
type Props = { navigation: any };
const ButtonTestScreen: React.FC<Props> = ({ navigation }) => {
return (
<View>
<ButtonNavigate
title="Hi i am a button"
navigateTo="Details"
></ButtonNavigate>
</View>
);
};
const styles = StyleSheet.create({
button_style: {
backgroundColor: "red"
},
text_style: {
color: "#000",
fontSize: 30
}
});
export default withNavigation(ButtonTestScreen);
And the respective ButtonNavigate component itself:
import React from "react";
import { StyleSheet } from "react-native";
import { withNavigation } from "react-navigation";
import { Button, Text } from "native-base";
type Props = {
title: string,
navigateTo: string,
navigation: any
};
const ButtonNavigate: React.FC<Props> = ({ title, navigateTo, navigation }) => {
return (
<Button
rounded
transparent
style={styles.button_style}
onPress={() => navigation.navigate(navigateTo)}
>
<Text style={styles.text_style}>{title}</Text>
</Button>
);
};
const styles = StyleSheet.create({
button_style: {
backgroundColor: "red"
},
text_style: {
color: "#151414"
}
});
export default withNavigation(ButtonNavigate);
I have just tested you code in expo.snack but without navigation and its ok,
see it here
You can test in your app to remove navigation and go step by step until you find the bug.
Folks, reason for this strange behavior is the "rounded" property of Native Base's button. In my application, somehow it causes the button to become non-clickable.
Maybe contributors of Native Base know what to do with this problem, so if you read this, maybe you have an idea.
Solution for my now was simply removing "rounded".
Native Base: 2.13.8
React-Navigation: 4.0.10
In my case it was the "top" in the container property of the button causing this issue. Removed it and adding "marginBottom" to the container above it solved the issue

React Native Open Tab Bar in Modal (using expo)

I'm trying to have one of the tab bar items open as modal when clicked, I'm currently using expo. I've read this: How do i make a TabNavigator button push a modal screen with React Navigation. However, I'm still learning React Native and I'm honestly not sure how to use this using expo navigation. Currently, I have created a stack navigator using "createStackNavigator" for each of the screens. And lastly, I have exported a bottom tab navigator including all of the stacks:
export default createBottomTabNavigator({
Tab1Stack,
Tab2Stack,
Tab3Stack,
Tab4Stack
});
I need Tab4 to open as a modal. Can someone people help me with this? Thank you in advance!!
Note this was built for "react-navigation": "3.3.0" so your mileage my vary for more recent versions of Expo and react-navigation.
To make a modal appear when you tap on the last tab in a TabNavigator requires you to nest your TabNavigator inside a StackNavigator.
So we could set up something like this:
#App.js
A simple App.js.
import React from 'react';
import AppContainer from './MainNavigation';
export default class App extends React.Component {
constructor (props) {
super(props);
this.state = {
};
}
render () {
return (
<AppContainer />
);
}
}
#MainNavigation.js
This file contains two navigators. A TabNavigator and a StackNavigator. The TabNavigator is nested inside the StackNavigator.
To be able to show the ModalScreen we have to override the tabBarOnPress function inside the defaultNavigationOptions which is inside the config for the TabNavigator.
We need to check the navigation.state.key to see where we are navigating too. If we are going to Tab3 we can intercept the call and navigate to the ModalScreen instead. Otherwise we use the defaultHandler and go to the tab that was tapped.
import Screen1 from './Screen1';
import Screen2 from './Screen2';
import Screen3 from './Screen3';
import ModalScreen from './ModalScreen';
import { createBottomTabNavigator, createAppContainer, createStackNavigator } from 'react-navigation';
const screens = {
Tab1: {
screen: Screen1
},
Tab2: {
screen: Screen2
},
Tab3: {
screen: Screen3
}
};
const config = {
headerMode: 'none',
initialRouteName: 'Tab1',
defaultNavigationOptions: {
tabBarOnPress: (data) => {
// this is where the magic happens
const { navigation, defaultHandler } = data;
// we check to see if the navigation key is going to be on Tab3
if (navigation.state.key === 'Tab3') {
// if it is we show the ModalScreen by navigating to it
navigation.navigate('ModalScreen');
} else {
// otherwise we call the defaultHandler and navigate to the tab we pressed
defaultHandler(navigation.state.key);
}
}
}
};
const TabNavigator = createBottomTabNavigator(screens, config);
const stackScreens = {
Tabs: {
screen: TabNavigator
},
ModalScreen: {
screen: ModalScreen
}
};
//we need to set the mode to be modal
const stackConfig = {
headerMode: 'none',
initialRouteName: 'Tabs',
mode: 'modal'
};
const MainNavigator = createStackNavigator(stackScreens, stackConfig);
export default createAppContainer(MainNavigator);
#Screen.js
A simple screen for each tab
import React from 'react';
import { View, StyleSheet, Text } from 'react-native';
export default class Screen extends React.Component {
render () {
return (
<View style={styles.container}>
<Text>Tab Screen</Text>
</View>
);
}
}
#ModalScreen
This screen is the modal that will appear when the tab for the third screen is tapped.
As it is part of the StackNavigator, defined above, it has access to the navigation prop. We set up a simple button that when pressed calls this.props.navigation.goBack() This will dismiss the modal.
import React from 'react';
import { View, StyleSheet, Text, Button } from 'react-native';
export default class Screen extends React.Component {
render () {
return (
<View style={styles.container}>
<Text>Modal Screen</Text>
<Button
title={'close modal'}
onPress={() => this.props.navigation.goBack()}
/>
</View>
);
}
}
Here is a snack with it working, https://snack.expo.io/#andypandy/show-modal-on-tab-press, hopefully it will show you how to set it up.

Navigation.goBack() with an API call in React-Native

In my application, I have few cases where navigation.goBack() cannot be used. I use react-navigation for navigation. When i'm in the detail screen, When I go back, I want to send an API call to get the latest records to the parent screen. So I used, navigation.navigate() instead of navigation.goBack(); But, this makes my app slow if I navigate and navigate back few times. It gets very slow if I do this few more times. What is the reason behind this? How the navigation.navigate() differs from navigation.goBack()?
What is the preferred way of handling this kind of scenario?
is there a way to pass param from navigate.goback() and parent can listen to the params and update its state?
You can pass a callback function as parameter (as mentioned in other answers).
Here is a more clear example, when you navigate from A to B and you want B to communicate information back to A you can pass a callback (here onSelect):
ViewA.js
import React from "react";
import { Button, Text, View } from "react-native";
class ViewA extends React.Component {
state = { selected: false };
onSelect = data => {
this.setState(data);
};
onPress = () => {
this.props.navigate("ViewB", { onSelect: this.onSelect });
};
render() {
return (
<View>
<Text>{this.state.selected ? "Selected" : "Not Selected"}</Text>
<Button title="Next" onPress={this.onPress} />
</View>
);
}
}
ViewB.js
import React from "react";
import { Button } from "react-native";
class ViewB extends React.Component {
goBack() {
const { navigation } = this.props;
navigation.goBack();
navigation.state.params.onSelect({ selected: true });
}
render() {
return <Button title="back" onPress={this.goBack} />;
}
}
Hats off for debrice - Refer to https://github.com/react-navigation/react-navigation/issues/288#issuecomment-315684617

React navigation. Render extra views on top of navigator

I'm trying to create a React Native app that renders an area with interchangeable screens and fixed view with some additional data/menu. I tried this solution form official React Navigation, but I can't make it work.
import React, { Component } from "react";
import {createStackNavigator} from 'react-navigation';
import { Text, View,} from 'react-native';
import Details from './DetailsScreen';
import MainScreen from './MainScreen';
const RootStack = createStackNavigator({
Main: {screen: MainScreen,
navigationOptions: {
header: null
}
},
Details: {
screen: Details,
},);
class App extends Component {
static router = {
...RootStack.router,
getStateForAction: (action, lastState) => {
return MyStack.router.getStateForAction(action, lastState);
}
};
render() {
const { navigation } = this.props;
return(
<View>
<Text>Foo</Text> //this is rendering
<RootStack navigation={navigation}/> //this is not
</View>);
}
}
export default App;
Article form link suggests that I can wrap object created with createStackNavigator() in a parent View, but it renders only when it is the only thing returned from render(), otherwise it seems to be ignored. Example from link claims that it is possible to "render additional things", but I can't make it work. Is it possible to do this way?
You can try this:
//wrapper.js
function wrapNavigator(Navigator, Wrapper, wrapperProps = {}) {
const WrappedComponent = props => (
<Wrapper { ...props, ...wrapperProps}>
<Navigator {...props} />
</Wrapper>
);
WrappedComponent.router = Navigator.router;
WrappedComponent.navigationOptions = Navigator.navigationOptions;
return WrappedComponent;
};
//app.js
const App = (props) => <View>
<Text>Some text...</Text>
{children}
</View>
export default wrapNavigator(Stack, App);
But this will not work if there are no parent navigator for App component.

React-Navigation with Login Screen

I am trying to use react-navigation to create an initial LOGIN screen that has no tabbar and header, and once the user has been successfully authenticated will navigate to another screen called LISTRECORD which has a tabbar, header and no back button option. Anyone has experience in this and can share?
In summary, what I'm trying to achieve with react-navigation is described below...
Screen 1: Login Screen (No Header & Tabbar)
Authenticated...
Screen 2: LISTRECORD (Header, Tabbar and No Back Button)
The tabbar contains other tabs too for navigation to Screen 3, Screen 4...
Oct 2017
I found this ridiculously confusing, so here is my solution starting from the top down:
I recommend starting a new project and literally just paste all this in and study it after. I commented the code big-time, so if you are stuck on any specific area, maybe the context can help you get back on track.
This post shows how to:
completely setup React Native to run react-navigation
Properly integrate with Redux
Handle Android Back Button
Nest Stack Navigators
Navigate from child to parent navigators
Reset the Navigation Stack
Reset the Navigation Stack while navigating from child to parent (nested)
index.js
import { AppRegistry } from 'react-native'
import App from './src/App'
AppRegistry.registerComponent('yourappname', () => App)
src/App.js (this is the most important file because it brings all the shreds together)
import React, { Component } from 'react'
// this will be used to make your Android hardware Back Button work
import { Platform, BackHandler } from 'react-native'
import { Provider, connect } from 'react-redux'
import { addNavigationHelpers } from 'react-navigation'
// this is your root-most navigation stack that can nest
// as many stacks as you want inside it
import { NavigationStack } from './navigation/nav_reducer'
// this is a plain ol' store
// same as const store = createStore(combinedReducers)
import store from './store'
// this creates a component, and uses magic to bring the navigation stack
// into all your components, and connects it to Redux
// don't mess with this or you won't get
// this.props.navigation.navigate('somewhere') everywhere you want it
// pro tip: that's what addNavigationHelpers() does
// the second half of the critical logic is coming up next in the nav_reducers.js file
class App extends Component {
// when the app is mounted, fire up an event listener for Back Events
// if the event listener returns false, Back will not occur (note that)
// after some testing, this seems to be the best way to make
// back always work and also never close the app
componentWillMount() {
if (Platform.OS !== 'android') return
BackHandler.addEventListener('hardwareBackPress', () => {
const { dispatch } = this.props
dispatch({ type: 'Navigation/BACK' })
return true
})
}
// when the app is closed, remove the event listener
componentWillUnmount() {
if (Platform.OS === 'android') BackHandler.removeEventListener('hardwareBackPress')
}
render() {
// slap the navigation helpers on (critical step)
const { dispatch, nav } = this.props
const navigation = addNavigationHelpers({
dispatch,
state: nav
})
return <NavigationStack navigation={navigation} />
}
}
// nothing crazy here, just mapping Redux state to props for <App />
// then we create your root-level component ready to get all decorated up
const mapStateToProps = ({ nav }) => ({ nav })
const RootNavigationStack = connect(mapStateToProps)(App)
const Root = () => (
<Provider store={store}>
<RootNavigationStack />
</Provider>
)
export default Root
src/navigation/nav_reducer.js
// NavigationActions is super critical
import { NavigationActions, StackNavigator } from 'react-navigation'
// these are literally whatever you want, standard components
// but, they are sitting in the root of the stack
import Splash from '../components/Auth/Splash'
import SignUp from '../components/Auth/SignupForm'
import SignIn from '../components/Auth/LoginForm'
import ForgottenPassword from '../components/Auth/ForgottenPassword'
// this is an example of a nested view, you might see after logging in
import Dashboard from '../components/Dashboard' // index.js file
const WeLoggedIn = StackNavigator({
LandingPad: { // if you don't specify an initial route,
screen: Dashboard // the first-declared one loads first
}
}, {
headerMode: 'none'
initialRouteName: LandingPad // if you had 5 components in this stack,
}) // this one would load when you do
// this.props.navigation.navigate('WeLoggedIn')
// notice we are exporting this one. this turns into <RootNavigationStack />
// in your src/App.js file.
export const NavigationStack = StackNavigator({
Splash: {
screen: Splash
},
Signup: {
screen: SignUp
},
Login: {
screen: SignIn
},
ForgottenPassword: {
screen: ForgottenPassword
},
WeLoggedIn: {
screen: WeLoggedIn // Notice how the screen is a StackNavigator
} // now you understand how it works!
}, {
headerMode: 'none'
})
// this is super critical for everything playing nice with Redux
// did you read the React-Navigation docs and recall when it said
// most people don't hook it up correctly? well, yours is now correct.
// this is translating your state properly into Redux on initialization
const INITIAL_STATE = NavigationStack.router.getStateForAction(NavigationActions.init())
// this is pretty much a standard reducer, but it looks fancy
// all it cares about is "did the navigation stack change?"
// if yes => update the stack
// if no => pass current stack through
export default (state = INITIAL_STATE, action) => {
const nextState = NavigationStack.router.getStateForAction(action, state)
return nextState || state
}
src/store/index.js
// remember when I said this is just a standard store
// this one is a little more advanced to show you
import { createStore, compose, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import { persistStore, autoRehydrate } from 'redux-persist'
import { AsyncStorage } from 'react-native'
// this pulls in your combinedReducers
// nav_reducer is one of them
import reducers from '../reducers'
const store = createStore(
reducers,
{},
compose(
applyMiddleware(thunk),
autoRehydrate()
)
)
persistStore(store, { storage: AsyncStorage, whitelist: [] })
// this exports it for App.js
export default store
src/reducers.js
// here is my reducers file. I don't want any confusion
import { combineReducers } from 'redux'
// this is a standard reducer, same as you've been using since kindergarten
// with action types like LOGIN_SUCCESS, LOGIN_FAIL
import loginReducer from './components/Auth/login_reducer'
import navReducer from './navigation/nav_reducer'
export default combineReducers({
auth: loginReducer,
nav: navReducer
})
src/components/Auth/SignUpForm.js
I will show you a sample here. This isn't mine, I just typed it out for you in this rickety StackOverflow editor. Please give me thumbs up if you appreciate it :)
import React, { Component } from 'react'
import { View, Text, TouchableOpacity } from 'react-native
// notice how this.props.navigation just works, no mapStateToProps
// some wizards made this, not me
class SignUp extends Component {
render() {
return (
<View>
<Text>Signup</Text>
<TouchableOpacity onPress={() => this.props.navigation.navigate('Login')}>
<Text>Go to Login View</Text>
</TouchableOpacity>
</View>
)
}
}
export default SignUp
src/components/Auth/LoginForm.js
I'll show you a dumb style one also, with the super dope back button
import React from 'react'
import { View, Text, TouchableOpacity } from 'react-native
// notice how we pass navigation in
const SignIn = ({ navigation }) => {
return (
<View>
<Text>Log in</Text>
<TouchableOpacity onPress={() => navigation.goBack(null)}>
<Text>Go back to Sign up View</Text>
</TouchableOpacity>
</View>
)
}
export default SignIn
src/components/Auth/Splash.js
Here is a splash screen you can play around with. I am using it like a higher-order component:
import React, { Component } from 'react'
import { StyleSheet, View, Image, Text } from 'react-native'
// https://github.com/oblador/react-native-animatable
// this is a library you REALLY should be using
import * as Animatable from 'react-native-animatable'
import { connect } from 'react-redux'
import { initializeApp } from './login_actions'
class Splash extends Component {
constructor(props) {
super(props)
this.state = {}
}
componentWillMount() {
setTimeout(() => this.props.initializeApp(), 2000)
}
componentWillReceiveProps(nextProps) {
// if (!nextProps.authenticated) this.props.navigation.navigate('Login')
if (nextProps.authenticated) this.props.navigation.navigate('WeLoggedIn')
}
render() {
const { container, image, text } = styles
return (
<View style={container}>
<Image
style={image}
source={require('./logo.png')}
/>
<Animatable.Text
style={text}
duration={1500}
animation="rubberBand"
easing="linear"
iterationCount="infinite"
>
Loading...
</Animatable.Text>
<Text>{(this.props.authenticated) ? 'LOGGED IN' : 'NOT LOGGED IN'}</Text>
</View>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F0F0F0'
},
image: {
height: 110,
resizeMode: 'contain'
},
text: {
marginTop: 50,
fontSize: 15,
color: '#1A1A1A'
}
})
// my LOGIN_SUCCESS action creator flips state.auth.isAuthenticated to true
// so this splash screen just watches it
const mapStateToProps = ({ auth }) => {
return {
authenticated: auth.isAuthenticated
}
}
export default connect(mapStateToProps, { initializeApp })(Splash)
src/components/Auth/login_actions.js
I'm just going to show you initializeApp() so you get some ideas:
import {
INITIALIZE_APP,
CHECK_REMEMBER_ME,
TOGGLE_REMEMBER_ME,
LOGIN_INITIALIZE,
LOGIN_SUCCESS,
LOGIN_FAIL,
LOGOUT
} from './login_types'
//INITIALIZE APP
// this isn't done, no try/catch and LOGIN_FAIL isn't hooked up
// but you get the idea
// if a valid JWT is detected, they will be navigated to WeLoggedIn
export const initializeApp = () => {
return async (dispatch) => {
dispatch({ type: INITIALIZE_APP })
const user = await AsyncStorage.getItem('token')
.catch((error) => dispatch({ type: LOGIN_FAIL, payload: error }))
if (!user) return dispatch({ type: LOGIN_FAIL, payload: 'No Token' })
return dispatch({
type: LOGIN_SUCCESS,
payload: user
})
// navigation.navigate('WeLoggedIn')
// pass navigation into this function if you want
}
}
In other use cases, you may prefer the higher-order component. They work exactly the same as React for web. Stephen Grider's tutorials on Udemy are the best, period.
src/HOC/require_auth.js
import React, { Component } from 'react'
import { connect } from 'react-redux'
export default function (ComposedComponent) {
class Authentication extends Component {
componentWillMount() {
if (!this.props.authenticated) this.props.navigation.navigate('Login')
}
componentWillUpdate(nextProps) {
if (!nextProps.authenticated) this.props.navigation.navigate('Login')
}
render() {
return (
<ComposedComponent {...this.props} />
)
}
}
const mapStateToProps = ({ auth }) => {
return {
authenticated: auth.isAuthenticated
}
}
return connect(mapStateToProps)(Authentication)
}
You use it just like this:
import requireAuth from '../HOC/require_auth'
class RestrictedArea extends Component {
// ... normal view component
}
//map state to props
export default connect(mapStateToProps, actions)(requireAuth(RestrictedArea))
There, that is everything I wish someone told and showed me.
TLDR The App.js, and nav_reducer.js files are absolutely the most important to get right. The rest is old familiar. My examples should accelerate you into a savage productivity machine.
[Edit] Here is my logout action creator. You will find it very useful if you wish to wipe off your navigation stack so the user cannot press Android Hardware Back Button and go back to a screen that requires authentication:
//LOGOUT
export const onLogout = (navigation) => {
return async (dispatch) => {
try {
await AsyncStorage.removeItem('token')
navigation.dispatch({
type: 'Navigation/RESET',
index: 0,
actions: [{ type: 'Navigate', routeName: 'Login' }]
})
return dispatch({ type: LOGOUT })
} catch (errors) {
// pass the user through with no error
// this restores INITIAL_STATE (see login_reducer.js)
return dispatch({ type: LOGOUT })
}
}
}
// login_reducer.js
case LOGOUT: {
return {
...INITIAL_STATE,
isAuthenticated: false,
}
}
[bonus edit] How do I navigate from a child Stack Navigator to a parent Stack Navigator?
If you want to navigate from one of your child Stack Navigators and reset the stack, do this:
Be inside a component adding code, where you have this.props.navigation available
Make a component like <Something />
Pass navigation into it, like this: <Something navigation={this.props.navigation} />
Go into the code for that component
Notice how you have this.props.navigation available inside this child component
Now you're done, just call this.props.navigation.navigate('OtherStackScreen') and you should watch React Native magically go there without problem
But, I want to RESET the whole stack while navigating to a parent stack.
Call an action creator or something like this (starting off from step 6): this.props.handleSubmit(data, this.props.navigation)
Go into the action creator and observe this code that could be there:
actionCreators.js
// we need this to properly go from child to parent navigator while resetting
// if you do the normal reset method from a child navigator:
this.props.navigation.dispatch({
type: 'Navigation/RESET',
index: 0,
actions: [{ type: 'Navigate', routeName: 'SomeRootScreen' }]
})
// you will see an error about big red error message and
// screen must be in your current stack
// don't worry, I got your back. do this
// (remember, this is in the context of an action creator):
import { NavigationActions } from 'react-navigation'
// notice how we passed in this.props.navigation from the component,
// so we can just call it like Dan Abramov mixed with Gandolf
export const handleSubmit = (token, navigation) => async (dispatch) => {
try {
// lets do some operation with the token
await AsyncStorage.setItem('token#E1', token)
// let's dispatch some action that doesn't itself cause navigation
// if you get into trouble, investigate shouldComponentUpdate()
// and make it return false if it detects this action at this moment
dispatch({ type: SOMETHING_COMPLETE })
// heres where it gets 100% crazy and exhilarating
return navigation.dispatch(NavigationActions.reset({
// this says put it on index 0, aka top of stack
index: 0,
// this key: null is 9001% critical, this is what
// actually wipes the stack
key: null,
// this navigates you to some screen that is in the Root Navigation Stack
actions: [NavigationActions.navigate({ routeName: 'SomeRootScreen' })]
}))
} catch (error) {
dispatch({ type: SOMETHING_COMPLETE })
// User should login manually if token fails to save
return navigation.dispatch(NavigationActions.reset({
index: 0,
key: null,
actions: [NavigationActions.navigate({ routeName: 'Login' })]
}))
}
}
I am using this code inside an enterprise-grade React Native app, and it works beautifully.
react-navigation is like functional programming. It is designed to be handled in small "pure navigation" fragments that compose well together. If you employ the strategy described above, you will find yourself creating re-useable navigation logic that you can just paste around as needed.
Although what Manjeet suggests will work, it is not a good navigational structure.
What you should do is take a step back and handle everything on another level.
Top level navigator should be a stack navigator that renders a login screen. Another screen within this top-most navigator should be your app's Main-Navigator. When your login state is satisfied, you reset the main stack to just the Main-Navigator.
The reason for this structure is:
A- What if you need to add on-boarding information before the Login the future?
B- What if you need to navigate outside of the Main-Navigation environment (eg: your main nav is tabs and you want a non-tab view)?
If your top-most navigator is a Stack-Navigator that presents Login screens and other Navigators, then your app's navigation structure can properly scale.
I do not believe the conditional rendering of a login screen or stack navigator, as suggested above, is a good idea....trust me...I've gone down that road.
this is how I achived this functionality.
File 0)index.android.js
'use strict'
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View
} from 'react-native';
import Root from 'src/containers/Root'
AppRegistry.registerComponent('Riduk', () => Root);
File 1)my Root.js
class Root extends Component {
constructor(props) {
super(props);
this.state = {
authenticated:false,
isLoading:true,
store: configureStore(() => this.setState({isLoading: false})),
};
}
componentDidMount() {
//you can do check with authentication with fb, gmail and other right here
/* firebase.auth().onAuthStateChanged((user) => {
if (user) {
api.resetRouteStack(dispatch, "UserProfile");
console.log("authenticated", user);
} else {
api.resetRouteStack(dispatch, "Landing");
console.log("authenticated", false);
}
});*/
}
render() {
if (this.state.isLoading) { //checking if the app fully loaded or not, splash screen can be rendered here
return null;
}
return (
<Provider store={this.state.store}>
<App/>
</Provider>
);
}
}
module.exports = Root;
2)App.js
import AppWithNavigationState,{AppBeforeLogin} from './AppNavigator';
class App extends Component{
constructor(props){
super(props);
}
render(){
let {authenticated} = this.props;
if(authenticated){
return <AppWithNavigationState/>;
}
return <AppBeforeLogin/>
}
}
export default connect(state =>({authenticated: state.user.authenticated}))(App);
3)AppNavigator.js
'use strict';
import React, {Component} from 'react';
import { View, BackAndroid, StatusBar,} from 'react-native';
import {
NavigationActions,
addNavigationHelpers,
StackNavigator,
} from 'react-navigation';
import { connect} from 'react-redux';
import LandingScreen from 'src/screens/landingScreen';
import Login from 'src/screens/login'
import SignUp from 'src/screens/signUp'
import ForgotPassword from 'src/screens/forgotPassword'
import UserProfile from 'src/screens/userProfile'
import Drawer from 'src/screens/drawer'
const routesConfig = {
//Splash:{screen:SplashScreen},
Landing:{screen:LandingScreen},
Login: { screen: Login },
SignUp: { screen: SignUp },
ForgotPassword: { screen: ForgotPassword },
UserProfile:{screen:UserProfile},
};
export const AppNavigator = StackNavigator(routesConfig, {initialRouteName:'UserProfile'}); //navigator that will be used after login
export const AppBeforeLogin = StackNavigator(routesConfig); //naviagtor for before login
class AppWithNavigationState extends Component{
constructor(props) {
super(props);
this.handleBackButton = this.handleBackButton.bind(this);
}
componentDidMount() {
BackAndroid.addEventListener('hardwareBackPress', this.handleBackButton);
}
componentWillUnmount() {
BackAndroid.removeEventListener('hardwareBackPress', this.handleBackButton);
}
//added to handle back button functionality on android
handleBackButton() {
const {nav, dispatch} = this.props;
if (nav && nav.routes && nav.routes.length > 1) {
dispatch(NavigationActions.back());
return true;
}
return false;
}
render() {
let {dispatch, nav} = this.props;
return (
<View style={styles.container}>
{(api.isAndroid()) &&
<StatusBar
backgroundColor="#C2185B"
barStyle="light-content"
/>
}
<AppNavigator navigation={addNavigationHelpers({ dispatch, state: nav })}/>
</View>
);
}
};
export default connect(state =>({nav: state.nav}))(AppWithNavigationState);
//module.exports = AppWithNavigationState;
This is my solution based on #parker recommendation:
Create a top level navigator and it should be a stack navigator that
renders a login screen.
Another screen within this top level
navigator should be your app's Main-Navigator.
When your login
state is satisfied, you reset the main stack to just the
Main-Navigator.
This code does the bare minimum to accomplish the above.
Create a new react-native project, then copy the code below into index.ios.js and/or index.android.js to see it working.
import React, { Component } from 'react';
import {
AppRegistry,
Text,
Button
} from 'react-native';
import { StackNavigator, NavigationActions } from 'react-navigation';
const resetAction = NavigationActions.reset({
index: 0,
actions: [
NavigationActions.navigate({ routeName: 'Main' })
]
});
class LoginScreen extends Component {
login() {
this.props.navigation.dispatch(resetAction);
}
render() {
return <Button title='Login' onPress={() => {this.login()}} />;
}
}
class FeedScreen extends Component {
render() {
return <Text>This is my main app screen after login</Text>;
}
}
//Create the navigation
const MainNav = StackNavigator({
Feed: { screen: FeedScreen },
});
const TopLevelNav = StackNavigator({
Login: { screen: LoginScreen },
Main: { screen: MainNav },
}, {
headerMode: 'none',
});
AppRegistry.registerComponent('ReactNav2', () => TopLevelNav);
There is now good documentation on the react-navigation site about the authentication flow.
react-navigation now has a SwitchNavigator which helps desired behavior and switching between navigators. Currently there is not much documentation about it but there is a really good example snack created by the library which shows a simple authentication flow implementation. You can check it here.
SwitchNavigator reference
SwitchNavigator(RouteConfigs, SwitchNavigatorConfig)
Example from docs
const AppStack = StackNavigator({ Home: HomeScreen, Other: OtherScreen });
const AuthStack = StackNavigator({ SignIn: SignInScreen });
export default SwitchNavigator(
{
AuthLoading: AuthLoadingScreen,
App: AppStack,
Auth: AuthStack,
},
{
initialRouteName: 'AuthLoading',
}
);
Its good that you are using react-navigation which has a good support for most of the features your app requires. Here's my advice
1) On Authentication
React-native has this nice feature state variables which when changed views are re-rendered. You can use state variables to understand the "state" (authenticated/visitor) of the users of your app.
Here is a simple implementation where a user logs in by pressing a login button
Entry page where user logs in
import React from 'react';
import Home from './layouts/users/home/Home';
import Login from './layouts/public/login/Login';
class App extends React.Component {
state = {
isLoggedIn: false
}
componentDidMount() {
//Do something here like hide splash screen
}
render(){
if (this.state.isLoggedIn)
return <Home />;
else
return <Login onLoginPress={() => this.setState({isLoggedIn: true})} />;
}
}
export default App;
2) Login with header
Login View
import React from 'react';
//Non react-native import
import { TabNavigator } from 'react-navigation'
import Icon from 'react-native-vector-icons/MaterialIcons'
import LoginStyles from './Style'
//Do all imports found in react-native here
import {
View,
Text,
TextInput,
StyleSheet,
TouchableOpacity,
} from 'react-native';
class Login extends React.Component {
render(){
return (
<View>
<Text>
Login area
</Text>
<TouchableOpacity style={LoginStyles.touchable} onPress={this.props.onLoginPress}>
<Text style={LoginStyles.button}>
Login
</Text>
</TouchableOpacity>
</View>
);
}
}
export default Login;
Remember to remove the style attributes in the login screen and add yours including import, I am leaving them there as it can help you have and idea how you can arrange you react project
However it still works without the styles so you can take them off, clicking the login button will take you to the Home screen, since the state changed and the view has to be re-rendered according to new state
The login screen is without a header as you required
Home screen with tabs
3) Tabs with header
The general method to achieve this functionality it to add a TabNavigator in a StackNavigator.
import React from 'react';
import {
DrawerNavigator,
StackNavigator,
TabNavigator,
TabBarBottom,
NavigationActions
} from 'react-navigation'
import Icon from 'react-native-vector-icons/MaterialIcons'
//Do all imports found in react-native here
import {
View,
Text,
TextInput,
StyleSheet,
TouchableOpacity,
} from 'react-native';
class PicturesTab extends React.Component {
static navigationOptions = {
tabBarLabel: 'Pictures',
// Note: By default the icon is only shown on iOS. Search the showIcon option below.
tabBarIcon: ({ tintColor }) => (<Icon size={30} color={tintColor} name="photo" />),
};
render() { return <Text>Pictures</Text> }
}
class VideosTab extends React.Component {
static navigationOptions = {
tabBarLabel: 'Videos',
tabBarIcon: ({ tintColor }) => (<Icon size={30} color={tintColor} name="videocam" />),
};
render() { return <Text>Videos</Text> }
}
const HomeTabs = TabNavigator({
Pictures: {
screen: PicturesTab,
},
Videos: {
screen: VideosTab,
},
}, {
tabBarComponent: TabBarBottom,
tabBarPosition: 'bottom',
tabBarOptions: {
//Thick teal #094545
activeTintColor: '#094545',
showLabel: false,
activeBackgroundColor: '#094545',
inactiveTintColor: '#bbb',
activeTintColor: '#fff',
}
});
const HomeScreen = StackNavigator({
HomeTabs : { screen: HomeTabs,
navigationOptions: ({ navigation }) => ({
// title :'title',
// headerRight:'put some component here',
// headerLeft:'put some component here',
headerStyle: {
backgroundColor: '#094545'
}
})
},
});
export default HomeScreen;
Disclaimer : Code may return errors as some files may be missing or some typos may be present you should check for details carefully and change where neccesary if you have to copy this code. Any problems can be pasted as comments. Hope this helps someone.
You may also remove the icons in the tab configurations or install the react-native-vector icons which makes tabs great!
Make tabbar and header separate components and only include them in other components. About disabling "BACK", there is a section about "blocking navigation actions" in the docs: https://reactnavigation.org/docs/routers/
You should be able to use that for screen 2.
I needed this, but none of the other solutions worked for me. So here is my solution for a Login with a drawer (the latter accessible only after proper authentication, and each of the screens inside have there own navigation stack). My code has a DrawerNavigator, but the same could be used for a TabNavigator (createBottomTabNavigator).
wrapScreen = stackNavigator =>
createStackNavigator(stackNavigator, {
defaultNavigationOptions: ({ navigation }) => ({
headerStyle: { backgroundColor: "white" },
headerLeft: MenuButton(navigation)
})
});
const DrawerStack = createDrawerNavigator(
{
// Menu Screens
firstSection: wrapScreen({ FirstScreen: FirstScreen }),
secondSection: wrapScreen({
SecondHomeScreen: SecondHomeScreen,
SecondOptionScreen: SecondOptionScreen
}),
settingSection: wrapScreen({ SettingScreen: SettingScreen }),
aboutSection: wrapScreen({ AboutScreen: AboutScreen })
},
{
initialRouteName: "firstSection",
gesturesEnabled: false,
drawerPosition: "left",
contentComponent: DrawerContainer
}
);
const PrimaryNav = createSwitchNavigator(
{
loginStack: LoginScreen,
appStack: DrawerStack
},
{ initialRouteName: "loginStack" }
);
export default createAppContainer(PrimaryNav);
If you want no back button from your LIST page to LOGIN page, you can do this:
static navigationOptions = {
title: 'YOUR TITLE',
headerLeft : null,
};